From b2fcbb828960ebfe532edd700ef9acdcf7467f58 Mon Sep 17 00:00:00 2001 From: Patryk <43276401+Zaptyp@users.noreply.github.com> Date: Fri, 2 Apr 2021 06:55:27 +0200 Subject: [PATCH 001/240] Correction of spelling and punctuation errors Correction of spelling and punctuation errors and improved text appearance. --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0726f161..91a3e22a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Nieoficjalna aplikacja do obsługi najpopularniejszych dzienników elektroniczny ## Ważna informacja -Jak zapewne już wiecie, we wrześniu 2020r. **firma Librus zabroniła nam** publikowania w sklepie Google Play naszej aplikacji z obsługą dziennika Librus® Synergia. Prowadziliśmy rozmowy, aby **umożliwić Wam wygodny, bezpłatny dostęp do Waszych ocen, wiadomości, zadań domowych**, jednak oczekiwania firmy Librus zdecydowanie przekroczyły wszelkie nasze możliwości finansowe. Mając na uwadze powyższe względy, zdecydowaliśmy się opublikować kod źródłowy aplikacji Szkolny.eu. Liczymy, że dzięki temu aplikacja będzie mogła dalej funkcjonować, być rozwijana, pomagając Wam w czasie zdalnego nauczania i przez kolejne lata nauki. +Jak zapewne już wiecie, we wrześniu 2020 r. **firma Librus zabroniła nam** publikowania w sklepie Google Play naszej aplikacji z obsługą dziennika Librus® Synergia. Prowadziliśmy rozmowy, aby **umożliwić Wam wygodny, bezpłatny dostęp do Waszych ocen, wiadomości, zadań domowych**, jednak oczekiwania firmy Librus zdecydowanie przekroczyły wszelkie nasze możliwości finansowe. Mając na uwadze powyższe względy, zdecydowaliśmy się opublikować kod źródłowy aplikacji Szkolny.eu. Liczymy, że dzięki temu aplikacja będzie mogła dalej funkcjonować, być rozwijana, pomagając Wam w czasie zdalnego nauczania i przez kolejne lata nauki. __Zachęcamy do [przeczytania całej informacji](https://szkolny.eu/informacja) na naszej stronie.__ @@ -30,17 +30,17 @@ Szkolny.eu jest nieoficjalną aplikacją, umożliwiającą rodzicom i uczniom do - plan lekcji, terminarz, oceny, wiadomości, zadania domowe, uwagi, frekwencja - wygodne **widgety** na ekran główny -- łatwa komunikacja z nauczycielami - **odbieranie, wyszukiwanie i wysyłanie wiadomości** +- łatwa komunikacja z nauczycielami-**odbieranie, wyszukiwanie i wysyłanie wiadomości** - pobieranie **załączników wiadomości i zadań domowych** - **powiadomienia** o nowych informacjach na telefonie lub na komputerze -- organizacja zadań domowych i sprawdzianów - łatwe oznaczanie jako wykonane +- organizacja zadań domowych i sprawdzianów-łatwe oznaczanie jako wykonane - obliczanie **średniej ocen** ze wszystkich przedmiotów, oceny proponowane i końcowe -- Symulator edycji ocen - obliczanie średniej z przedmiotu po zmianie dowolnych jego ocen +- Symulator edycji ocen-obliczanie średniej z przedmiotu po zmianie dowolnych jego ocen - **dodawanie własnych wydarzeń** i zadań do terminarza - nowoczesny i intuicyjny interfejs użytkownika -- **obsługa wielu profili** uczniów - jeżeli jesteś Rodzicem, możesz skonfigurować wszystkie swoje konta uczniowskie i łatwo między nimi przełączać +- **obsługa wielu profili** uczniów-jeżeli jesteś Rodzicem, możesz skonfigurować wszystkie swoje konta uczniowskie i łatwo między nimi przełączać - opcja **automatycznej synchronizacji** z E-dziennikiem -- opcja Ciszy nocnej - nigdy więcej budzących Cię dźwięków z telefonu +- opcja Ciszy nocnej-nigdy więcej budzących Cię dźwięków z telefonu [Zobacz porównanie funkcji z innymi aplikacjami](https://szkolny.eu/funkcje) @@ -53,7 +53,7 @@ Najnowsze wersje możesz pobrać z Google Play lub bezpośrednio z naszej strony ### Kompilacja -Aby uruchomić aplikację "ze źródeł" należy użyć Android Studio w wersji co najmniej 4.2 Beta 6. Wersja `debug` może wtedy zostać zainstalowana np. na emulatorze Androida. +Aby uruchomić aplikację „ze źródeł” należy użyć Android Studio w wersji co najmniej 4.2 Beta 6. Wersja `debug` może wtedy zostać zainstalowana np. na emulatorze Androida. Aby zbudować wersję produkcyjną, tzn. `release` należy użyć wariantu `mainRelease` oraz podpisać wyjściowy plik .APK sygnaturą w wersji V1 i V2. @@ -68,15 +68,15 @@ __Jeśli masz jakieś pytania, zapraszamy na [nasz serwer Discord](https://szkol ## Licencja Szkolny.eu publikowany jest na licencji [GNU GPLv3](LICENSE). W szczególności, deweloper: -- może modyfikować oraz usprawniać kod aplikacji -- może dystrybuować wersje produkcyjne -- musi opublikować wszelkie wprowadzone zmiany, tzn. publiczny fork tego repozytorium -- nie może zmieniać licencji ani copyrightu aplikacji +- Może modyfikować oraz usprawniać kod aplikacji +- Może dystrybuować wersje produkcyjne +- Musi opublikować wszelkie wprowadzone zmiany, tzn. publiczny fork tego repozytorium +- Nie może zmieniać licencji ani copyrightu aplikacji Dodatkowo: -- zabronione jest modyfikowanie lub usuwanie kodu odpowiedzialnego za zgodność wersji produkcyjnych z licencją +- Zabronione jest modyfikowanie lub usuwanie kodu odpowiedzialnego za zgodność wersji produkcyjnych z licencją. -- **wersje skompilowane nie mogą być dystrybuowane za pomocą Google Play oraz żadnej platformy, na której istnieje oficjalna wersja aplikacji** +- **Wersje skompilowane nie mogą być dystrybuowane za pomocą Google Play oraz żadnej platformy, na której istnieje oficjalna wersja aplikacji**. **Autorzy aplikacji nie biorą odpowiedzialności za używanie aplikacji, modyfikowanie oraz dystrybuowanie.** From cb4b168b2a201324955bd719f28c817dde25d445 Mon Sep 17 00:00:00 2001 From: Patryk <43276401+Zaptyp@users.noreply.github.com> Date: Fri, 2 Apr 2021 10:12:13 +0200 Subject: [PATCH 002/240] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 91a3e22a..6b0ca3bb 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,17 @@ Szkolny.eu jest nieoficjalną aplikacją, umożliwiającą rodzicom i uczniom do - plan lekcji, terminarz, oceny, wiadomości, zadania domowe, uwagi, frekwencja - wygodne **widgety** na ekran główny -- łatwa komunikacja z nauczycielami-**odbieranie, wyszukiwanie i wysyłanie wiadomości** +- łatwa komunikacja z nauczycielami — **odbieranie, wyszukiwanie i wysyłanie wiadomości** - pobieranie **załączników wiadomości i zadań domowych** - **powiadomienia** o nowych informacjach na telefonie lub na komputerze -- organizacja zadań domowych i sprawdzianów-łatwe oznaczanie jako wykonane +- organizacja zadań domowych i sprawdzianów — łatwe oznaczanie jako wykonane - obliczanie **średniej ocen** ze wszystkich przedmiotów, oceny proponowane i końcowe -- Symulator edycji ocen-obliczanie średniej z przedmiotu po zmianie dowolnych jego ocen +- Symulator edycji ocen — obliczanie średniej z przedmiotu po zmianie dowolnych jego ocen - **dodawanie własnych wydarzeń** i zadań do terminarza - nowoczesny i intuicyjny interfejs użytkownika -- **obsługa wielu profili** uczniów-jeżeli jesteś Rodzicem, możesz skonfigurować wszystkie swoje konta uczniowskie i łatwo między nimi przełączać +- **obsługa wielu profili** uczniów — jeżeli jesteś Rodzicem, możesz skonfigurować wszystkie swoje konta uczniowskie i łatwo między nimi przełączać - opcja **automatycznej synchronizacji** z E-dziennikiem -- opcja Ciszy nocnej-nigdy więcej budzących Cię dźwięków z telefonu +- opcja Ciszy nocnej — nigdy więcej budzących Cię dźwięków z telefonu [Zobacz porównanie funkcji z innymi aplikacjami](https://szkolny.eu/funkcje) From bf595dd09c20016d43268d1563efc73d845e7c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 3 Apr 2021 15:13:10 +0200 Subject: [PATCH 003/240] [App] Fix detecting correct remote repository name. --- app/git-info.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/git-info.gradle b/app/git-info.gradle index 929d5307..2ddd8c2d 100644 --- a/app/git-info.gradle +++ b/app/git-info.gradle @@ -84,7 +84,7 @@ private def buildGitInfo() { .stream() .map { it.name + "(" + it.URIs.stream() - .map { it.rawPath } + .map { it.rawPath.stripMargin('/').replace(".git", "") } .toArray() .join(", ") + ")" } From 5301b4efadfed0f0c5d9ccbd86a9ab020e092606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 3 Apr 2021 15:35:00 +0200 Subject: [PATCH 004/240] [Gradle] Add moving app bundles to release folder. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 7d22ea41..a166e834 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,7 +98,7 @@ tasks.whenTaskAdded { task -> if (flavor != "") { tasks.create(renameTaskName, Copy) { - from file("${projectDir}/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/") + from file("${projectDir}/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/"), file("${buildDir}/outputs/apk/${flavor}/release/") include "*.aab", "*.apk", "mapping.txt", "output-metadata.json" destinationDir file("${projectDir}/release/") rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1' From e7cb699bcfdc4395bb9fadef8c8e2a4eb27f8f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 3 Apr 2021 15:58:59 +0200 Subject: [PATCH 005/240] [App] Fix unofficial build notice formatting. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9a9befb..13528d64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1419,7 +1419,7 @@ Nie możesz modyfikować tego rodzaju kompilacji aplikacji Szkolny.eu.\n\nAby wprowadzić własne zmiany, skorzystaj z kodu źródłowego dostępnego na GitHubie oraz zapoznaj się z README i informacją o licencji.\n\nhttps://szkolny.eu/github/android Ta kompilacja zawiera zmiany niezatwierdzone do żadnej rewizji. Zapisz oraz opublikuj wszystkie zmiany przed wydaniem wersji \"release\".\n\nDla bezpieczeństwa oraz ze względów zgodności z licencją, korzystanie z aplikacji zostało zablokowane. Korzystasz z kompilacji typu \"debug\". Ta informacja zostanie wyświetlona tylko jeden raz dla aktualnego urządzenia. - Korzystasz z nieoficjalnej kompilacji aplikacji Szkolny.eu. Zalecamy używanie wyłącznie oficjalnych wersji aplikacji.\n\nOstatnie zmiany w tej wersji zostały wprowadzone przez %3$s w repozytorium %2$s (%1$s).\n\nTo okno nie wyświetli się ponownie. + Korzystasz z nieoficjalnej kompilacji aplikacji Szkolny.eu. Zalecamy używanie wyłącznie oficjalnych wersji aplikacji.\n\nOstatnie zmiany w tej wersji zostały wprowadzone przez:\n%3$s\nw repozytorium:\n%1$s (%2$s).\n\nTo okno nie wyświetli się ponownie. Informacja dotycząca wersji aplikacji Szczegóły wersji Informacje o kompilacji From fc4c297beff16dfdc5072d5bd49707a8a5368b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 3 Apr 2021 22:22:18 +0200 Subject: [PATCH 006/240] [App] Add more info to build details dialog. --- .../edziennik/utils/managers/BuildManager.kt | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt index 8608d35a..3deaabfd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt @@ -56,9 +56,9 @@ class BuildManager(val app: App) : CoroutineScope { } val versionBadge = when { - isOfficial && isNightly -> + isSigned && isNightly -> "Nightly\n" + BuildConfig.VERSION_NAME.substringAfterLast('.') - isOfficial && isDaily -> + isSigned && isDaily -> "Daily\n" + BuildConfig.VERSION_NAME.substringAfterLast('.') isDebug -> "Debug\n" + BuildConfig.VERSION_BASE @@ -78,10 +78,21 @@ class BuildManager(val app: App) : CoroutineScope { val fields = mapOf( R.string.build_version to BuildConfig.VERSION_BASE, - R.string.build_official to if (isOfficial) - yes.asColoredSpannable(mtrlGreen) - else - no.asColoredSpannable(mtrlRed), + R.string.build_official to when { + isOfficial -> yes.asColoredSpannable(mtrlGreen) + isSigned -> TextUtils.concat( + yes.asColoredSpannable(mtrlYellow), + when { + isNightly -> " (nightly build)" + isDaily -> " (daily build)" + else -> no.asColoredSpannable(mtrlYellow) + } + ) + else -> TextUtils.concat( + no.asColoredSpannable(mtrlRed), + if (gitAuthor != null) " ($gitAuthor)" else "" + ) + }, R.string.build_platform to when { isPlayRelease -> activity.getString(R.string.build_platform_play) isApkRelease -> activity.getString(R.string.build_platform_apk) @@ -229,8 +240,10 @@ class BuildManager(val app: App) : CoroutineScope { val validation = Signing.appCertificate + gitHash + gitRemotes?.join(";") // app already validated - if (app.config.validation == validation.md5()) + if (app.config.validation?.substringBefore(":") == validation.md5()){ + gitAuthor = app.config.validation?.substringAfter(":") return@launch + } val dialog = MaterialAlertDialogBuilder(activity) .setTitle(R.string.please_wait) @@ -251,7 +264,7 @@ class BuildManager(val app: App) : CoroutineScope { } // release, unofficial, published build - app.config.validation = validation.md5() + app.config.validation = validation.md5() + ":" + gitAuthor invalidateBuild(activity, dialog, InvalidBuildReason.VALID) } } From 5b35e3500ea01302bd8c567b6b3646fc96a5678b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 4 Apr 2021 20:37:02 +0200 Subject: [PATCH 007/240] [Gradle] Fix moving app bundles to release folder. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a166e834..2cd3922a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,7 +98,7 @@ tasks.whenTaskAdded { task -> if (flavor != "") { tasks.create(renameTaskName, Copy) { - from file("${projectDir}/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/"), file("${buildDir}/outputs/apk/${flavor}/release/") + from file("${projectDir}/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/"), file("${buildDir}/outputs/apk/${flavor}Release/") include "*.aab", "*.apk", "mapping.txt", "output-metadata.json" destinationDir file("${projectDir}/release/") rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1' From 8f9861bac606124332a8e03d43493885f049a14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 4 Apr 2021 21:39:42 +0200 Subject: [PATCH 008/240] [Actions] Add workflow utilities scripts. --- .github/utils/.gitignore | 2 + .github/utils/_get_password.py | 57 +++++++++++ .github/utils/_utils.py | 142 ++++++++++++++++++++++++++++ .github/utils/bump_nightly.py | 69 ++++++++++++++ .github/utils/bump_version.py | 41 ++++++++ .github/utils/extract_changelogs.py | 59 ++++++++++++ .github/utils/rename_artifacts.py | 26 +++++ .github/utils/save_version.py | 122 ++++++++++++++++++++++++ .github/utils/sign.py | 84 ++++++++++++++++ .github/utils/webhook_discord.py | 118 +++++++++++++++++++++++ 10 files changed, 720 insertions(+) create mode 100644 .github/utils/.gitignore create mode 100644 .github/utils/_get_password.py create mode 100644 .github/utils/_utils.py create mode 100644 .github/utils/bump_nightly.py create mode 100644 .github/utils/bump_version.py create mode 100644 .github/utils/extract_changelogs.py create mode 100644 .github/utils/rename_artifacts.py create mode 100644 .github/utils/save_version.py create mode 100644 .github/utils/sign.py create mode 100644 .github/utils/webhook_discord.py diff --git a/.github/utils/.gitignore b/.github/utils/.gitignore new file mode 100644 index 00000000..d50a09fc --- /dev/null +++ b/.github/utils/.gitignore @@ -0,0 +1,2 @@ +.env +__pycache__/ diff --git a/.github/utils/_get_password.py b/.github/utils/_get_password.py new file mode 100644 index 00000000..33071b6c --- /dev/null +++ b/.github/utils/_get_password.py @@ -0,0 +1,57 @@ +import base64 +import secrets +from hashlib import sha256 +from typing import Tuple + +import mysql.connector as mysql +from Crypto.Cipher import AES + + +def get_password( + version_name: str, + version_code: int, + db_host: str, + db_user: str, + db_pass: str, + db_name: str, +) -> Tuple[str, bytes]: + db = mysql.connect( + host=db_host, + user=db_user, + password=db_pass, + database=db_name, + auth_plugin="mysql_native_password", + ) + + print(f"Generating passwords for version {version_name} ({version_code})") + + password = base64.b64encode(secrets.token_bytes(16)).decode() + iv = secrets.token_bytes(16) + + key = f"{version_name}.{password}.{version_code}" + key = sha256(key.encode()).digest() + data = "ThisIsOurHardWorkPleaseDoNotCopyOrSteal(c)2019.KubaSz" + data = sha256(data.encode()).digest() + data = data + (chr(16) * 16).encode() + + aes = AES.new(key=key, mode=AES.MODE_CBC, iv=iv) + + app_password = base64.b64encode(aes.encrypt(data)).decode() + + c = db.cursor() + c.execute( + "INSERT IGNORE INTO _appPasswords (versionCode, appPassword, password, iv) VALUES (%s, %s, %s, %s);", + (version_code, app_password, password, iv), + ) + db.commit() + + c = db.cursor() + c.execute( + "SELECT password, iv FROM _appPasswords WHERE versionCode = %s;", + (version_code,), + ) + row = c.fetchone() + + db.close() + + return (row[0], row[1]) diff --git a/.github/utils/_utils.py b/.github/utils/_utils.py new file mode 100644 index 00000000..09b59f4a --- /dev/null +++ b/.github/utils/_utils.py @@ -0,0 +1,142 @@ +import re +import subprocess +import sys +from datetime import datetime +from typing import Tuple + +VERSION_NAME_REGEX = r'versionName: "(.+?)"' +VERSION_CODE_REGEX = r"versionCode: ([0-9]+)" +VERSION_NAME_FORMAT = 'versionName: "{}"' +VERSION_CODE_FORMAT = "versionCode: {}" + + +def get_project_dir() -> str: + project_dir = sys.argv[1] + if project_dir[-1:] == "/" or project_dir[-1:] == "\\": + project_dir = project_dir[:-1] + return project_dir + + +def read_gradle_version(project_dir: str) -> Tuple[int, str]: + GRADLE_PATH = f"{project_dir}/build.gradle" + + with open(GRADLE_PATH, "r") as f: + gradle = f.read() + + version_name = re.search(VERSION_NAME_REGEX, gradle).group(1) + version_code = int(re.search(VERSION_CODE_REGEX, gradle).group(1)) + + return (version_code, version_name) + + +def write_gradle_version(project_dir: str, version_code: int, version_name: str): + GRADLE_PATH = f"{project_dir}/build.gradle" + + with open(GRADLE_PATH, "r") as f: + gradle = f.read() + + gradle = re.sub( + VERSION_NAME_REGEX, VERSION_NAME_FORMAT.format(version_name), gradle + ) + gradle = re.sub( + VERSION_CODE_REGEX, VERSION_CODE_FORMAT.format(version_code), gradle + ) + + with open(GRADLE_PATH, "w") as f: + f.write(gradle) + + +def build_version_code(version_name: str) -> int: + version = version_name.split("+")[0].split("-") + version_base = version[0] + version_suffix = version[1] if len(version) == 2 else "" + + base_parts = version_base.split(".") + major = int(base_parts[0]) or 0 + minor = int(base_parts[1]) if len(base_parts) > 1 else 0 + patch = int(base_parts[2]) if len(base_parts) > 2 else 0 + + beta = 9 + rc = 9 + if "dev" in version_suffix: + beta = 0 + rc = 0 + elif "beta." in version_suffix: + beta = int(version_suffix.split(".")[1]) + rc = 0 + elif "rc." in version_suffix: + beta = 0 + rc = int(version_suffix.split(".")[1]) + + version_code = beta + rc * 10 + patch * 100 + minor * 10000 + major * 1000000 + return version_code + + +def get_changelog(project_dir: str, format: str) -> Tuple[str, str]: + with open( + f"{project_dir}/app/src/main/assets/pl-changelog.html", "r", encoding="utf-8" + ) as f: + changelog = f.read() + + title = re.search(r"

(.+?)

", changelog).group(1) + content = re.search(r"(?s)
    (.+)
", changelog).group(1).strip() + content = "\n".join(line.strip() for line in content.split("\n")) + + if format != "html": + content = content.replace("
  • ", "- ") + content = content.replace("
    ", "\n") + if format == "markdown": + content = re.sub(r"(.+?)", "__\\1__", content) + content = re.sub(r"(.+?)", "*\\1*", content) + content = re.sub(r"(.+?)", "**\\1**", content) + content = re.sub(r"", "", content) + + return (title, content) + + +def get_commit_log(project_dir: str, format: str, max_lines: int = None) -> str: + last_tag = ( + subprocess.check_output("git describe --tags --abbrev=0".split(" ")) + .decode() + .strip() + ) + + log = subprocess.run( + args=f"git log {last_tag}..HEAD --format=%an%x00%at%x00%h%x00%s%x00%D".split(" "), + cwd=project_dir, + stdout=subprocess.PIPE, + ) + log = log.stdout.strip().decode() + + commits = [line.split("\x00") for line in log.split("\n")] + if max_lines: + commits = commits[:max_lines] + + output = [] + valid = False + + for commit in commits: + if not commit[0]: + continue + if "origin/" in commit[4]: + valid = True + if not valid: + continue + date = datetime.fromtimestamp(float(commit[1])) + date = date.strftime("%Y-%m-%d %H:%M:%S") + if format == "html": + output.append(f"
  • {commit[3]} - {commit[0]}
  • ") + elif format == "markdown": + output.append(f"[{date}] {commit[0]}\n {commit[3]}") + elif format == "markdown_full": + output.append( + f"_[{date}] {commit[0]}_\n` `__`{commit[2]}`__ **{commit[3]}**" + ) + elif format == "plain": + output.append(f"- {commit[3]}") + + if format == "markdown": + output.insert(0, "```") + output.append("```") + + return "\n".join(output) diff --git a/.github/utils/bump_nightly.py b/.github/utils/bump_nightly.py new file mode 100644 index 00000000..888a087f --- /dev/null +++ b/.github/utils/bump_nightly.py @@ -0,0 +1,69 @@ +import json +import os +import re +import sys +from datetime import datetime, timedelta + +import requests + +from _utils import ( + get_commit_log, + get_project_dir, + read_gradle_version, + write_gradle_version, +) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("usage: bump_nightly.py ") + exit(-1) + + repo = os.getenv("GITHUB_REPOSITORY") + sha = os.getenv("GITHUB_SHA") + + if not repo or not sha: + print("Missing GitHub environment variables.") + exit(-1) + + with requests.get( + f"https://api.github.com/repos/{repo}/actions/runs?per_page=1&status=success" + ) as r: + data = json.loads(r.text) + runs = [run for run in data["workflow_runs"] if run["head_sha"] == sha] + if runs: + print("::set-output name=hasNewChanges::false") + exit(0) + + print("::set-output name=hasNewChanges::true") + + project_dir = get_project_dir() + + (version_code, version_name) = read_gradle_version(project_dir) + version_name = version_name.split("+")[0] + + date = datetime.now() + if date.hour > 6: + version_name += "+daily." + date.strftime("%Y%m%d-%H%M") + else: + date -= timedelta(days=1) + version_name += "+nightly." + date.strftime("%Y%m%d") + + print("::set-output name=appVersionName::" + version_name) + print("::set-output name=appVersionCode::" + str(version_code)) + + write_gradle_version(project_dir, version_code, version_name) + + commit_log = get_commit_log(project_dir, format="html", max_lines=10) + + with open( + f"{project_dir}/app/src/main/assets/pl-changelog.html", "r", encoding="utf-8" + ) as f: + changelog = f.read() + + changelog = re.sub(r"

    (.+?)

    ", f"

    {version_name}

    ", changelog) + changelog = re.sub(r"(?s)
      (.+)
    ", f"
      \n{commit_log}\n
    ", changelog) + + with open( + f"{project_dir}/app/src/main/assets/pl-changelog.html", "w", encoding="utf-8" + ) as f: + f.write(changelog) diff --git a/.github/utils/bump_version.py b/.github/utils/bump_version.py new file mode 100644 index 00000000..80d36519 --- /dev/null +++ b/.github/utils/bump_version.py @@ -0,0 +1,41 @@ +import os + +from dotenv import load_dotenv + +from _get_password import get_password +from _utils import build_version_code, write_gradle_version +from sign import sign + +if __name__ == "__main__": + version_name = input("Enter version name: ") + version_code = build_version_code(version_name) + + print(f"Bumping version to {version_name} ({version_code})") + + project_dir = "../.." + + load_dotenv() + DB_HOST = os.getenv("DB_HOST") + DB_USER = os.getenv("DB_USER") + DB_PASS = os.getenv("DB_PASS") + DB_NAME = os.getenv("DB_NAME") + + write_gradle_version(project_dir, version_code, version_name) + (password, iv) = get_password( + version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME + ) + + sign(project_dir, version_name, version_code, password, iv, commit=False) + + print("Writing mock passwords") + os.chdir(project_dir) + os.system( + "sed -i -E 's/\/\*([0-9a-f]{2} ?){16}\*\//\/*secret password - removed for source code publication*\//g' app/src/main/cpp/szkolny-signing.cpp" + ) + os.system( + "sed -i -E 's/\\t0x.., 0x(.)., 0x.(.), 0x.(.), 0x.., 0x.., 0x.., 0x.(.), 0x.., 0x.(.), 0x(.)., 0x(.)., 0x.., 0x.., 0x.., 0x.(.)/\\t0x\\3\\6, 0x\\7\\4, 0x\\1\\8, 0x\\2\\5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff /g' app/src/main/cpp/szkolny-signing.cpp" + ) + os.system( + "sed -i -E 's/param1\..(.).(.).(.).(.)..(.)..(.)..(.)..(.).../param1.MTIzNDU2Nzg5MD\\5\\2\\7\\6\\1\\3\\4\8==/g' app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt" + ) + input("Press any key to finish") diff --git a/.github/utils/extract_changelogs.py b/.github/utils/extract_changelogs.py new file mode 100644 index 00000000..07524bad --- /dev/null +++ b/.github/utils/extract_changelogs.py @@ -0,0 +1,59 @@ +import os +import sys + +from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_version + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("usage: extract_changelogs.py ") + exit(-1) + + project_dir = get_project_dir() + + (version_code, version_name) = read_gradle_version(project_dir) + + print("::set-output name=appVersionName::" + version_name) + print("::set-output name=appVersionCode::" + str(version_code)) + + dir = f"{project_dir}/app/release/whatsnew-{version_name}/" + os.makedirs(dir, exist_ok=True) + + print("::set-output name=changelogDir::" + dir) + + (title, changelog) = get_changelog(project_dir, format="plain") + with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f: + f.write(changelog) + print("::set-output name=changelogPlainFile::" + dir + "whatsnew-pl-PL") + + with open(dir + "whatsnew-titled.txt", "w", encoding="utf-8") as f: + f.write(title) + f.write("\n") + f.write(changelog) + print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew-titled.txt") + + print("::set-output name=changelogTitle::" + title) + + (_, changelog) = get_changelog(project_dir, format="markdown") + with open(dir + "whatsnew.md", "w", encoding="utf-8") as f: + f.write(changelog) + print("::set-output name=changelogMarkdownFile::" + dir + "whatsnew.md") + + (_, changelog) = get_changelog(project_dir, format="html") + with open(dir + "whatsnew.html", "w", encoding="utf-8") as f: + f.write(changelog) + print("::set-output name=changelogHtmlFile::" + dir + "whatsnew.html") + + changelog = get_commit_log(project_dir, format="plain", max_lines=10) + with open(dir + "commit_log.txt", "w", encoding="utf-8") as f: + f.write(changelog) + print("::set-output name=commitLogPlainFile::" + dir + "commit_log.txt") + + changelog = get_commit_log(project_dir, format="markdown", max_lines=10) + with open(dir + "commit_log.md", "w", encoding="utf-8") as f: + f.write(changelog) + print("::set-output name=commitLogMarkdownFile::" + dir + "commit_log.md") + + changelog = get_commit_log(project_dir, format="html", max_lines=10) + with open(dir + "commit_log.html", "w", encoding="utf-8") as f: + f.write(changelog) + print("::set-output name=commitLogHtmlFile::" + dir + "commit_log.html") diff --git a/.github/utils/rename_artifacts.py b/.github/utils/rename_artifacts.py new file mode 100644 index 00000000..4eeabfaf --- /dev/null +++ b/.github/utils/rename_artifacts.py @@ -0,0 +1,26 @@ +import glob +import os +import sys + +from _utils import get_project_dir + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("usage: rename_artifacts.py ") + exit(-1) + + project_dir = get_project_dir() + + files = glob.glob(f"{project_dir}/app/release/*.*") + for file in files: + file_relative = file.replace(os.getenv("GITHUB_WORKSPACE") + "/", "") + if "-aligned.apk" in file: + os.unlink(file) + elif "-signed.apk" in file: + new_file = file.replace("-signed.apk", ".apk") + if os.path.isfile(new_file): + os.unlink(new_file) + os.rename(file, new_file) + elif ".apk" in file or ".aab" in file: + print("::set-output name=signedReleaseFile::" + file) + print("::set-output name=signedReleaseFileRelative::" + file_relative) diff --git a/.github/utils/save_version.py b/.github/utils/save_version.py new file mode 100644 index 00000000..01de17a7 --- /dev/null +++ b/.github/utils/save_version.py @@ -0,0 +1,122 @@ +import glob +import os +import sys +from datetime import datetime +from time import time + +import mysql.connector as mysql +from dotenv import load_dotenv + +from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_version + + +def save_version( + project_dir: str, + db_host: str, + db_user: str, + db_pass: str, + db_name: str, + apk_server_release: str, + apk_server_nightly: str, +): + db = mysql.connect( + host=db_host, + user=db_user, + password=db_pass, + database=db_name, + auth_plugin="mysql_native_password", + ) + + (version_code, version_name) = read_gradle_version(project_dir) + (_, changelog) = get_changelog(project_dir, format="html") + + types = ["dev", "beta", "nightly", "daily", "rc", "release"] + build_type = [x for x in types if x in version_name] + build_type = build_type[0] if build_type else "release" + + if "+nightly." in version_name or "+daily." in version_name: + changelog = get_commit_log(project_dir, format="html") + build_type = "nightly" + elif "-dev" in version_name: + build_type = "dev" + elif "-beta." in version_name: + build_type = "beta" + elif "-rc." in version_name: + build_type = "rc" + + build_date = int(time()) + apk_name = None + bundle_name_play = None + + files = glob.glob(f"{project_dir}/app/release/*.*") + output_apk = f"Edziennik_{version_name}_official.apk" + output_aab_play = f"Edziennik_{version_name}_play.aab" + for file in files: + if output_apk in file: + build_date = int(os.stat(file).st_mtime) + apk_name = output_apk + if output_aab_play in file: + build_date = int(os.stat(file).st_mtime) + bundle_name_play = output_aab_play + + build_date = datetime.fromtimestamp(build_date).strftime("%Y-%m-%d %H:%M:%S") + + if build_type in ["nightly", "daily"]: + download_url = apk_server_nightly + apk_name if apk_name else None + else: + download_url = apk_server_release + apk_name if apk_name else None + + cols = [ + "versionCode", + "versionName", + "releaseDate", + "releaseNotes", + "releaseType", + "downloadUrl", + "apkName", + "bundleNamePlay", + ] + updated = { + "versionCode": version_code, + "downloadUrl": download_url, + "apkName": apk_name, + "bundleNamePlay": bundle_name_play, + } + + values = [ + version_code, + version_name, + build_date, + changelog, + build_type, + download_url, + apk_name, + bundle_name_play, + ] + values.extend(val for val in updated.values() if val) + + updated = ", ".join(f"{col} = %s" for (col, val) in updated.items() if val) + + sql = f"INSERT INTO updates ({', '.join(cols)}) VALUES ({'%s, ' * (len(cols) - 1)}%s) ON DUPLICATE KEY UPDATE {updated};" + + c = db.cursor() + c.execute(sql, tuple(values)) + db.commit() + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("usage: save_version.py ") + exit(-1) + + project_dir = get_project_dir() + + load_dotenv() + DB_HOST = os.getenv("DB_HOST") + DB_USER = os.getenv("DB_USER") + DB_PASS = os.getenv("DB_PASS") + DB_NAME = os.getenv("DB_NAME") + APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE") + APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY") + + save_version(project_dir, DB_HOST, DB_USER, DB_PASS, DB_NAME, APK_SERVER_RELEASE, APK_SERVER_NIGHTLY) diff --git a/.github/utils/sign.py b/.github/utils/sign.py new file mode 100644 index 00000000..026d819b --- /dev/null +++ b/.github/utils/sign.py @@ -0,0 +1,84 @@ +import os +import re +import sys + +from dotenv import load_dotenv + +from _get_password import get_password +from _utils import get_project_dir, read_gradle_version + + +def sign( + project_dir: str, + version_name: str, + version_code: int, + password: str, + iv: bytes, + commit: bool = False, +): + SIGNING_PATH = f"{project_dir}/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt" + CPP_PATH = f"{project_dir}/app/src/main/cpp/szkolny-signing.cpp" + + with open(SIGNING_PATH, "r") as f: + signing = f.read() + + with open(CPP_PATH, "r") as f: + cpp = f.read() + + SIGNING_REGEX = r"\$param1\..*\.\$param2" + CPP_REGEX = r"(?s)/\*.+?toys AES_IV\[16\] = {.+?};" + + SIGNING_FORMAT = "$param1.{}.$param2" + CPP_FORMAT = "/*{}*/\nstatic toys AES_IV[16] = {{\n\t{} }};" + + print(f"Writing passwords for version {version_name} ({version_code})") + + iv_hex = " ".join(["{:02x}".format(x) for x in iv]) + iv_cpp = ", ".join(["0x{:02x}".format(x) for x in iv]) + + signing = re.sub(SIGNING_REGEX, SIGNING_FORMAT.format(password), signing) + cpp = re.sub(CPP_REGEX, CPP_FORMAT.format(iv_hex, iv_cpp), cpp) + + with open(SIGNING_PATH, "w") as f: + f.write(signing) + + with open(CPP_PATH, "w") as f: + f.write(cpp) + + if commit: + os.chdir(project_dir) + os.system("git add .") + os.system( + f'git commit -m "[{version_name}] Update build.gradle, signing and changelog."' + ) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("usage: sign.py [commit]") + exit(-1) + + project_dir = get_project_dir() + + load_dotenv() + DB_HOST = os.getenv("DB_HOST") + DB_USER = os.getenv("DB_USER") + DB_PASS = os.getenv("DB_PASS") + DB_NAME = os.getenv("DB_NAME") + + (version_code, version_name) = read_gradle_version(project_dir) + (password, iv) = get_password( + version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME + ) + + print("::set-output name=appVersionName::" + version_name) + print("::set-output name=appVersionCode::" + str(version_code)) + + sign( + project_dir, + version_name, + version_code, + password, + iv, + commit="commit" in sys.argv, + ) diff --git a/.github/utils/webhook_discord.py b/.github/utils/webhook_discord.py new file mode 100644 index 00000000..3c1404ae --- /dev/null +++ b/.github/utils/webhook_discord.py @@ -0,0 +1,118 @@ +import os +import sys +from datetime import datetime + +import requests +from dotenv import load_dotenv + +from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_version + + +def post_webhook( + project_dir: str, + apk_file: str, + apk_server_release: str, + apk_server_nightly: str, + webhook_release: str, + webhook_testing: str, +): + (_, version_name) = read_gradle_version(project_dir) + + types = ["dev", "beta", "nightly", "daily", "rc", "release"] + build_type = [x for x in types if x in version_name] + build_type = build_type[0] if build_type else None + + testing = ["dev", "beta", "nightly", "daily"] + testing = build_type in testing + + apk_name = os.path.basename(apk_file) + if build_type in ["nightly", "daily"]: + download_url = apk_server_nightly + apk_name + else: + download_url = apk_server_release + apk_name + + if testing: + build_date = int(os.stat(apk_file).st_mtime) + if build_date: + build_date = datetime.fromtimestamp(build_date).strftime("%Y-%m-%d %H:%M") + + # untagged release, get commit log + if build_type in ["nightly", "daily"]: + changelog = get_commit_log(project_dir, format="markdown", max_lines=5) + else: + changelog = get_changelog(project_dir, format="markdown") + + webhook = get_webhook_testing( + version_name, build_type, changelog, download_url, build_date + ) + requests.post(url=webhook_testing, json=webhook) + else: + changelog = get_changelog(project_dir, format="markdown") + webhook = get_webhook_release(changelog, download_url) + requests.post(url=webhook_release, json=webhook) + + +def get_webhook_release(changelog: str, download_url: str): + (title, content) = changelog + return {"content": f"__**{title}**__\n{content}\n{download_url}"} + + +def get_webhook_testing( + version_name: str, + build_type: str, + changelog: str, + download_url: str, + build_date: str, +): + return { + "embeds": [ + { + "title": f"Nowa wersja {build_type} aplikacji Szkolny.eu", + "description": f"Dostępna jest nowa wersja testowa **{build_type}**.", + "color": 2201331, + "fields": [ + { + "name": f"Wersja `{version_name}`", + "value": f"[Pobierz .APK]({download_url})" + if download_url + else "*Pobieranie niedostępne*", + "inline": False, + }, + { + "name": "Data kompilacji", + "value": build_date or "-", + "inline": False, + }, + { + "name": "Ostatnie zmiany", + "value": changelog or "-", + "inline": False, + }, + ], + } + ] + } + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("usage: webhook_discord.py ") + exit(-1) + + project_dir = get_project_dir() + + load_dotenv() + APK_FILE = os.getenv("APK_FILE") + APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE") + APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY") + WEBHOOK_RELEASE = os.getenv("WEBHOOK_RELEASE") + WEBHOOK_TESTING = os.getenv("WEBHOOK_TESTING") + + post_webhook( + project_dir, + APK_FILE, + APK_SERVER_RELEASE, + APK_SERVER_NIGHTLY, + WEBHOOK_RELEASE, + WEBHOOK_TESTING, + ) From 8db81478f3dd2b4899f08db0d845fbc3a6c3c51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 4 Apr 2021 21:41:55 +0200 Subject: [PATCH 009/240] [Actions] Add release build workflow. --- .github/workflows/build-release-apk.yml | 151 ++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 .github/workflows/build-release-apk.yml diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml new file mode 100644 index 00000000..169704c1 --- /dev/null +++ b/.github/workflows/build-release-apk.yml @@ -0,0 +1,151 @@ +name: Release build - official + +on: + push: + tags: + - "*" + +jobs: + prepare: + name: Prepare build environment + runs-on: self-hosted + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + clean: false + - name: Set executable permissions to gradlew + run: chmod +x ./gradlew + - name: Setup Python + uses: actions/setup-python@v2 + - name: Install packages + uses: BSFishy/pip-action@v1 + with: + packages: | + python-dotenv + pycryptodome + mysql-connector-python + requests + - name: Write signing passwords + env: + DB_HOST: ${{ secrets.DB_HOST }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASS: ${{ secrets.DB_PASS }} + DB_NAME: ${{ secrets.DB_NAME }} + run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit + build: + name: Build APK + runs-on: self-hosted + needs: + - prepare + outputs: + androidHome: ${{ env.ANDROID_HOME }} + androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} + steps: + - name: Setup JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Clean build artifacts + run: | + rm -rf app/release/* + rm -rf app/build/outputs/apk/* + rm -rf app/build/outputs/bundle/* + - name: Assemble official release with Gradle + run: ./gradlew assembleOfficialRelease + sign: + name: Sign APK + runs-on: self-hosted + needs: + - build + outputs: + signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }} + signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }} + steps: + - name: Sign build artifacts + id: sign_app + uses: r0adkll/sign-android-release@v1 + with: + releaseDirectory: app/release + signingKeyBase64: ${{ secrets.KEY_STORE }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }} + env: + ANDROID_HOME: ${{ needs.build.outputs.androidHome }} + ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }} + BUILD_TOOLS_VERSION: "30.0.2" + - name: Rename signed artifacts + id: artifacts + run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE + publish: + name: Publish APK + runs-on: self-hosted + needs: + - sign + steps: + - name: Setup Python + uses: actions/setup-python@v2 + + - name: Extract changelogs + id: changelog + run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE + + - name: Upload APK to SFTP + uses: easingthemes/ssh-deploy@v2.1.6 + env: + REMOTE_HOST: ${{ secrets.SSH_IP }} + REMOTE_USER: ${{ secrets.SSH_USERNAME }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }} + SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }} + TARGET: ${{ secrets.SSH_PATH_RELEASE }} + - name: Save version metadata + env: + DB_HOST: ${{ secrets.DB_HOST }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASS: ${{ secrets.DB_PASS }} + DB_NAME: ${{ secrets.DB_NAME }} + APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }} + APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }} + run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE + + - name: Distribute to App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.FIREBASE_APP_ID }} + token: ${{ secrets.FIREBASE_TOKEN }} + groups: ${{ secrets.FIREBASE_GROUPS_RELEASE }} + file: ${{ needs.sign.outputs.signedReleaseFile }} + releaseNotesFile: ${{ steps.changelog.outputs.changelogPlainTitledFile }} + - name: Release on GitHub + uses: softprops/action-gh-release@v1 + with: + name: ${{ steps.changelog.outputs.changelogTitle }} + body_path: ${{ steps.changelog.outputs.changelogMarkdownFile }} + files: ${{ needs.sign.outputs.signedReleaseFile }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Post Discord webhook + env: + APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }} + APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }} + APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }} + WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }} + WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }} + run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE + + - name: Upload workflow artifact + uses: actions/upload-artifact@v2 + if: true + with: + name: ${{ steps.changelog.outputs.appVersionName }} + path: | + app/release/whatsnew*/ + app/release/*.apk + app/release/*.aab + app/release/*.json + app/release/*.txt From 3e98fb967bcbf00dfef16ea23c6dfb5779b9c08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 4 Apr 2021 21:42:08 +0200 Subject: [PATCH 010/240] [Actions] Add Google Play build workflow. --- .github/workflows/build-release-aab-play.yml | 129 +++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .github/workflows/build-release-aab-play.yml diff --git a/.github/workflows/build-release-aab-play.yml b/.github/workflows/build-release-aab-play.yml new file mode 100644 index 00000000..c5bfb2ea --- /dev/null +++ b/.github/workflows/build-release-aab-play.yml @@ -0,0 +1,129 @@ +name: Release build - Google Play [AAB] + +on: + push: + branches: + - "master" + +jobs: + prepare: + name: Prepare build environment + runs-on: self-hosted + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + clean: false + - name: Set executable permissions to gradlew + run: chmod +x ./gradlew + - name: Setup Python + uses: actions/setup-python@v2 + - name: Install packages + uses: BSFishy/pip-action@v1 + with: + packages: | + python-dotenv + pycryptodome + mysql-connector-python + requests + - name: Write signing passwords + env: + DB_HOST: ${{ secrets.DB_HOST }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASS: ${{ secrets.DB_PASS }} + DB_NAME: ${{ secrets.DB_NAME }} + run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit + build: + name: Build App Bundle + runs-on: self-hosted + needs: + - prepare + outputs: + androidHome: ${{ env.ANDROID_HOME }} + androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} + steps: + - name: Setup JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Clean build artifacts + run: | + rm -rf app/release/* + rm -rf app/build/outputs/apk/* + rm -rf app/build/outputs/bundle/* + - name: Bundle play release with Gradle + run: ./gradlew bundlePlayRelease + sign: + name: Sign App Bundle + runs-on: self-hosted + needs: + - build + outputs: + signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }} + signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }} + steps: + - name: Sign build artifacts + id: sign_app + uses: r0adkll/sign-android-release@v1 + with: + releaseDirectory: app/release + signingKeyBase64: ${{ secrets.KEY_STORE }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }} + env: + ANDROID_HOME: ${{ needs.build.outputs.androidHome }} + ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }} + BUILD_TOOLS_VERSION: "30.0.2" + - name: Rename signed artifacts + id: artifacts + run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE + publish: + name: Publish App Bundle + runs-on: self-hosted + needs: + - sign + steps: + - name: Setup Python + uses: actions/setup-python@v2 + + - name: Extract changelogs + id: changelog + run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE + + - name: Save version metadata + env: + DB_HOST: ${{ secrets.DB_HOST }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASS: ${{ secrets.DB_PASS }} + DB_NAME: ${{ secrets.DB_NAME }} + APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }} + APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }} + run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE + + - name: Publish AAB to Google Play + uses: r0adkll/upload-google-play@v1 + if: ${{ endsWith(needs.sign.outputs.signedReleaseFile, '.aab') }} + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + packageName: pl.szczodrzynski.edziennik + releaseFile: ${{ needs.sign.outputs.signedReleaseFile }} + releaseName: ${{ steps.changelog.outputs.appVersionName }} + track: ${{ secrets.PLAY_RELEASE_TRACK }} + userFraction: 1.0 + whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }} + + - name: Upload workflow artifact + uses: actions/upload-artifact@v2 + if: true + with: + name: ${{ steps.changelog.outputs.appVersionName }} + path: | + app/release/whatsnew*/ + app/release/*.apk + app/release/*.aab + app/release/*.json + app/release/*.txt From 9c620de1e7c02d946e47ecf199bdc034c04db89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 4 Apr 2021 21:43:00 +0200 Subject: [PATCH 011/240] [Actions] Add nightly build workflow. --- .github/workflows/build-nightly-apk.yml | 150 ++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 .github/workflows/build-nightly-apk.yml diff --git a/.github/workflows/build-nightly-apk.yml b/.github/workflows/build-nightly-apk.yml new file mode 100644 index 00000000..e2ed28bf --- /dev/null +++ b/.github/workflows/build-nightly-apk.yml @@ -0,0 +1,150 @@ +name: Nightly build + +on: + schedule: + - cron: "30 1 * * *" + workflow_dispatch: + +jobs: + prepare: + name: Prepare build environment + runs-on: self-hosted + outputs: + hasNewChanges: ${{ steps.nightly.outputs.hasNewChanges }} + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + clean: false + - name: Set executable permissions to gradlew + run: chmod +x ./gradlew + - name: Setup Python + uses: actions/setup-python@v2 + - name: Install packages + uses: BSFishy/pip-action@v1 + with: + packages: | + python-dotenv + pycryptodome + mysql-connector-python + requests + - name: Bump nightly version + id: nightly + run: python $GITHUB_WORKSPACE/.github/utils/bump_nightly.py $GITHUB_WORKSPACE + - name: Write signing passwords + if: steps.nightly.outputs.hasNewChanges + env: + DB_HOST: ${{ secrets.DB_HOST }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASS: ${{ secrets.DB_PASS }} + DB_NAME: ${{ secrets.DB_NAME }} + run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit + build: + name: Build APK + runs-on: self-hosted + needs: + - prepare + if: needs.prepare.outputs.hasNewChanges + outputs: + androidHome: ${{ env.ANDROID_HOME }} + androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} + steps: + - name: Setup JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Clean build artifacts + run: | + rm -rf app/release/* + rm -rf app/build/outputs/apk/* + rm -rf app/build/outputs/bundle/* + - name: Assemble official release with Gradle + run: ./gradlew assembleOfficialRelease + sign: + name: Sign APK + runs-on: self-hosted + needs: + - build + outputs: + signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }} + signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }} + steps: + - name: Sign build artifacts + id: sign_app + uses: r0adkll/sign-android-release@v1 + with: + releaseDirectory: app/release + signingKeyBase64: ${{ secrets.KEY_STORE }} + alias: ${{ secrets.KEY_ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }} + env: + ANDROID_HOME: ${{ needs.build.outputs.androidHome }} + ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }} + BUILD_TOOLS_VERSION: "30.0.2" + - name: Rename signed artifacts + id: artifacts + run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE + publish: + name: Publish APK + runs-on: self-hosted + needs: + - sign + steps: + - name: Setup Python + uses: actions/setup-python@v2 + + - name: Extract changelogs + id: changelog + run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE + + - name: Upload APK to SFTP + uses: easingthemes/ssh-deploy@v2.1.6 + env: + REMOTE_HOST: ${{ secrets.SSH_IP }} + REMOTE_USER: ${{ secrets.SSH_USERNAME }} + SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }} + SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }} + TARGET: ${{ secrets.SSH_PATH_NIGHTLY }} + - name: Save version metadata + env: + DB_HOST: ${{ secrets.DB_HOST }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASS: ${{ secrets.DB_PASS }} + DB_NAME: ${{ secrets.DB_NAME }} + APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }} + APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }} + run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE + + - name: Distribute to App Distribution + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{ secrets.FIREBASE_APP_ID }} + token: ${{ secrets.FIREBASE_TOKEN }} + groups: ${{ secrets.FIREBASE_GROUPS_NIGHTLY }} + file: ${{ needs.sign.outputs.signedReleaseFile }} + releaseNotesFile: ${{ steps.changelog.outputs.commitLogPlainFile }} + + - name: Post Discord webhook + env: + APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }} + APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }} + APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }} + WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }} + WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }} + run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE + + - name: Upload workflow artifact + uses: actions/upload-artifact@v2 + if: true + with: + name: ${{ steps.changelog.outputs.appVersionName }} + path: | + app/release/whatsnew*/ + app/release/*.apk + app/release/*.aab + app/release/*.json + app/release/*.txt From 8b0f3490e39edabf7abdcb70214737cacb7a3a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 4 Apr 2021 22:41:12 +0200 Subject: [PATCH 012/240] [UI/Settings] Fix hiding hiding sticks from old without devMode. --- .../settings/cards/SettingsRegisterCard.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt index 14afd0cb..36239083 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.ui.modules.settings.cards import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import eu.szkolny.font.SzkolnyFont +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.after import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS @@ -141,12 +142,15 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { else null, - util.createPropertyItem( - text = R.string.settings_register_hide_sticks_from_old, - icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline, - value = configProfile.grades.hideSticksFromOld - ) { _, it -> - configProfile.grades.hideSticksFromOld = it - } + if (App.devMode) + util.createPropertyItem( + text = R.string.settings_register_hide_sticks_from_old, + icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline, + value = configProfile.grades.hideSticksFromOld + ) { _, it -> + configProfile.grades.hideSticksFromOld = it + } + else + null ) } From e9a2dae1e4bf5775b544d85acb2869fa97bf91f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 10:56:13 +0200 Subject: [PATCH 013/240] [Gradle] Fix moving artifacts to release folder, again. --- app/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2cd3922a..f49a6dad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,7 +98,10 @@ tasks.whenTaskAdded { task -> if (flavor != "") { tasks.create(renameTaskName, Copy) { - from file("${projectDir}/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/"), file("${buildDir}/outputs/apk/${flavor}Release/") + from file("${projectDir}/${flavor}/release/"), + file("${buildDir}/outputs/mapping/${flavor}Release/"), + file("${buildDir}/outputs/apk/${flavor}/release/"), + file("${buildDir}/outputs/bundle/${flavor}Release/") include "*.aab", "*.apk", "mapping.txt", "output-metadata.json" destinationDir file("${projectDir}/release/") rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1' From e124c429d1ea6245c04183419cad9d56e9f3ed29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 11:06:09 +0200 Subject: [PATCH 014/240] [Actions] Update nightly build schedule. --- .github/workflows/build-nightly-apk.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-nightly-apk.yml b/.github/workflows/build-nightly-apk.yml index e2ed28bf..ada926f0 100644 --- a/.github/workflows/build-nightly-apk.yml +++ b/.github/workflows/build-nightly-apk.yml @@ -2,7 +2,8 @@ name: Nightly build on: schedule: - - cron: "30 1 * * *" + # 23:30 UTC, 0:30 or 1:30 CET/CEST + - cron: "30 23 * * *" workflow_dispatch: jobs: From a4ca44e1ce690d2f32b390845190a54225241ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 12:58:54 +0200 Subject: [PATCH 015/240] [Git] Add README banner image. --- .github/readme-banner.png | Bin 0 -> 61749 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/readme-banner.png diff --git a/.github/readme-banner.png b/.github/readme-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..03e787426d2233fcd82974496b93bcb07963491f GIT binary patch literal 61749 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 literal 0 HcmV?d00001 From fd6553871fafcd5dbc9e284d8d5421b8712d4459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 13:00:30 +0200 Subject: [PATCH 016/240] [Git] Add banner image to README. --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6b0ca3bb..025c7a0b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -# Szkolny.eu - -Nieoficjalna aplikacja do obsługi najpopularniejszych dzienników elektronicznych w Polsce. -
    +![Readme Banner](.github/readme-banner.png) + [![Discord](https://img.shields.io/discord/619178050562686988?color=%237289DA&logo=discord&logoColor=white&style=for-the-badge)](https://szkolny.eu/discord) [![Oficjalna strona](https://img.shields.io/badge/-website-orange?style=for-the-badge&logo=internet-explorer&logoColor=white)](https://szkolny.eu/) [![Facebook Fanpage](https://img.shields.io/badge/-facebook-blue?style=for-the-badge&logo=facebook&logoColor=white)](https://szkolny.eu/facebook) From b4459e1fd4bf0648e2cbf330b6a331d1ec02422b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 13:08:21 +0200 Subject: [PATCH 017/240] [Git] Add workflow badges to README. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 025c7a0b..1646ab6b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ [![Najnowsza wersja](https://img.shields.io/github/v/release/szkolny-eu/szkolny-android?color=%2344CC11&include_prereleases&logo=github&logoColor=white&style=for-the-badge)](https://github.com/szkolny-eu/szkolny-android/releases/latest) ![Licencja](https://img.shields.io/github/license/szkolny-eu/szkolny-android?color=blue&logo=github&logoColor=white&style=for-the-badge) +[![Release build](https://img.shields.io/github/workflow/status/szkolny-eu/szkolny-android/Release%20build%20-%20official?label=Release&logo=github-actions&logoColor=white&style=for-the-badge)](https://github.com/szkolny-eu/szkolny-android/actions/workflows/build-release-apk.yml) +[![Play build](https://img.shields.io/github/workflow/status/szkolny-eu/szkolny-android/Release%20build%20-%20Google%20Play%20%5BAAB%5D?label=Play&logo=google-play&logoColor=white&style=for-the-badge)](https://github.com/szkolny-eu/szkolny-android/actions/workflows/build-release-aab-play.yml) +[![Nightly build](https://img.shields.io/github/workflow/status/szkolny-eu/szkolny-android/Nightly%20build?label=Nightly&logo=github-actions&logoColor=white&style=for-the-badge)](https://github.com/szkolny-eu/szkolny-android/actions/workflows/build-nightly-apk.yml) +
    ## Ważna informacja From 8661ecdafb10d519ee28972fc2eb9ab9e72f1629 Mon Sep 17 00:00:00 2001 From: Marcin Kowalicki Date: Mon, 5 Apr 2021 19:07:25 +0200 Subject: [PATCH 018/240] Finish English translation (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feature] Espionage 0.5 speed * [Languages] Start translating app to English * [Languages] More English translations * [Languages] Finish English translation * Fix bugs in typing * Revert "[Feature] Espionage 0.5 speed" This reverts commit b925d3137a724cd744970eb313c4799832c50a4f. * Remove Espionage [*] * Another typo * Fix things that were described in review * Fix typo * Hour -> Time * Update line breaks in strings file. * Remove unneeded whitespace. * Update English translations. Co-authored-by: Kuba Szczodrzyński --- app/src/main/res/values-en/strings.xml | 132 ++++++++++++++++++++++++- app/src/main/res/values/strings.xml | 4 - 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 2f5cb1fe..21644aac 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1235,5 +1235,135 @@ In order to download the file, you have to grant file storage permission for the application.\n\nClick OK to grant the permission. You denied the required permissions for the application.\n\nIn order to grant the permission, open the Permissions screen for Szkolny.eu in phone settings.\n\nClick OK to open app settings now. Required permissions - Your mother won\'t see your F grades + Hide sticks from old + Official build + Google Play + Testing version + {cmd-information-outline} Recommended + {cmd-android-studio} Developer version + {cmd-alert-circle-outline} Testing version + \??? + See also + Source code + Git branch + .APK + Unofficial (.APK) + Commit hash + Unsaved changes + Last tag + Commits since last tag + Remote repository + Build details + Check code + Profile name + Logout + Synchronize this profile + Update + Not now + Update + EduDziennik + Log in - %s + Date + Teacher + By days + List + lesson %d + By type + Add new student + Lesson subject + Read more + App version + Version details + Build date + No API access + Time + Help with app development on GitHub + Distribution + Build verification in progress... + By months + Summary + Type + See more + Update available + Build details + Update app to the latest version - %s. + App update available + Close archive + Archived profile + Profile is archived + Holiday ;) + The end of the school year + Logout from other devices + Student profile not found. + Go to application website + Get help or support authors + Information about application version + No current profile + Loading e-registers list... + Log in using token + Provide mobile app token. + Attendance configuration + Show symbols and colors from e-register config + There are no absences here. + There are no grades in this semester + Attendance settings + What is your e-register at school? + Log in using login and password + Use a login in form of \"9874123u\" + Use token, symbol and PIN code + Register device on journal VULCAN® page + Use e-mail/username and password + Logging in to PPE... + Podlaska Platforma Edukacyjna + Log in using e-mail and password + Logging in to VULCAN® register... + Login via VULCAN® platform + Log in using e-mail + Attendance ID + Log in with the server name, login and password + Log in with the data that you provide on VULCAN® e-register website + Provide data, that you use on e-register website + Base type ID + Counted to the stats? + Choose which e-register your school uses. If you have several accounts in different e-registere, you will be able to add them later. + You must have a LIBRUS® Rodzina account + Only Oświata w Radomiu and Innowacyjny Tarnobrzeg + How do you log into the e-register? + Group consecutive days on the list + Visible when the list is expanded + Display attendance in months view + %.2f%% + Attendance during this period: %.2%% + Log in child/parent account in app + Enter the e-mail address and password that you use to log in to the browser on the EduDziennik website. + Probably the school year for this student has not yet started (will start %s). Please try to sync later. + The school year ended on %s. Student data from the previous year will be moved to the archive for later review. + Trademarks featured in this application belong to their rightful owners and are used for informational purposes only. + You are viewing a student\'s data from the school year %d/%d. + Use data, that you enter on the e-register website + To be able to scan the QR code, you need to grant access to the camera.\n\nClick OK to grant permissions. + It\'s a pity, the opinions of others help me develop the application. + The selected login method is still being tested and may not work properly. If you have problems with the app, please choose the recommended login method. + You have an outdated version of the Szkolny.eu application. You need to update the app to continue to sync data. + You are using an old version of the Szkolny.eu application (%s). To use the app and ensure the best performance, please upgrade to %s. Change log: %s + You are viewing a student\'s data from the previous school year (%d /%d). Syncing and downloading of messages and some homework have been disabled. To open a student\'s profile for the current year, select Close Archive on the home page. + Log in to LIBRUS® Synergia on your computer, select the Mobile Applications tab, then enter the received Token and PIN below. + Enter the login received from the school with which you log into LIBRUS® Synergia (purple form).\n\nIt is recommended to log in with the LIBRUS® Family account (using e-mail) in the previous step. + Log in with your LIBRUS® Rodzina account, which works in the official LIBRUS® application and on the website portal.librus.pl, in the blue form. \n\nIf you do not have a LIBRUS® Rodzina account, you can create one at https://portal.librus.pl/rodzina/register. + If it takes too long, please check your internet connection and restart the application. + Select which image corresponds to the one you see when logging in to your log website. If your school doesn\'t use any of these city platforms, choose the first option. + You have an application build with unpublished changes. The build is in the repository:\n%1$s (%2$s)\nwhich is private or does not contain the latest changes.\n\nFor security reasons and compliance with the license, the use of the application has been blocked. + Log in to the VULCAN® log on your computer, select the Mobile Access tab, click the Register mobile device button. Enter the received Token, Symbol and PIN in the fields below. + Enter the data that you log in to the VULCAN® log website or the city platform. + You cannot modify this type of compilation of the Szkolny.eu application.\n\nTo make your own changes, please use the source code available on GitHub and see the README and license information.\n\nhttps://szkolny.eu/github/android + This build contains changes not committed to any revision. Save and publish all changes before \"release\".\n\nFor security reasons and for compliance with the license, the use of the application has been blocked. + You are using a \"debug\" build. This information will only be displayed once for the current device. + You are using an unofficial compilation of the Szkolny.eu application. We recommend that you use only the official versions of the application.\n\nLast changes in this version were made by:\n%3$s\nin the repository:\n%1$s (%2$s).\n\nThis window will not reappear. + The hash of the current commit was not found. Check Gradle configuration. + Child %s does not have a profile on this account in the current school year. Probably this profile has been deleted or the student no longer attends this class.\n\nTo go to the current profile, select a student from the list or log in to their account with the Add student button. + A reference to a remote repository was not found. Make sure you are using the official repository fork and verify your Gradle configuration. + "Enter the data you use to log in to the MobiDziennik website. As the server address, you can enter the address of the website where you have MobiDziennik. " + In order to be able to save the generated timetable, you must grant access rights to the device\'s memory.\n\nClick OK to grant permissions. + (Child) + (Parent) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13528d64..52eef2ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1346,10 +1346,6 @@ Podaj dane, którymi logujesz się na stronie internetowej dziennika VULCAN® lub na miejskiej platformie. Podaj dane, których używasz do logowania na stronie MobiDziennika. Jako adres serwera możesz wpisać adres strony internetowej, na której masz MobiDziennik. Logowanie do dziennika VULCAN®... - iDziennik Progman / iUczniowie - Zaloguj używając nazwy użytkownika i hasła - Podaj dane, których używasz na stronie internetowej e-dziennika - Użyj danych, które wpisujesz w formularz na stronie iDziennika. Jeśli nie pamiętasz hasła, wejdź na http://iuczniowie.progman.pl/ i kliknij przycisk \"Zapomniałem hasła\". EduDziennik Zaloguj używając e-maila i hasła Użyj danych, które podajesz na stronie internetowej e-dziennika From b8ff649c96b9df186d7bddb2fec94b7c60d9a0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 19:45:57 +0200 Subject: [PATCH 019/240] [UI] Move privacy policy dialog to login chooser. --- .../ui/modules/login/LoginChooserFragment.kt | 20 +++++++++++++++++++ .../ui/modules/login/LoginSummaryFragment.kt | 14 ------------- app/src/main/res/values/strings.xml | 1 + 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt index 8d1a2a19..ab99efb8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt @@ -10,12 +10,14 @@ import android.app.Activity import android.content.Intent import android.graphics.Color import android.os.Bundle +import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.Animation import android.view.animation.RotateAnimation +import android.widget.TextView import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -29,6 +31,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailability import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity +import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -203,6 +206,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope { return } + if (!app.config.privacyPolicyAccepted) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.privacy_policy) + .setMessage(Html.fromHtml(activity.getString(R.string.privacy_policy_dialog_html))) + .setPositiveButton(R.string.i_agree) { _, _ -> + app.config.privacyPolicyAccepted = true + onLoginModeClicked(loginType, loginMode) + } + .setNegativeButton(R.string.i_disagree, null) + .show() + .also { dialog -> + dialog.findViewById(android.R.id.message)?.movementMethod = + BetterLinkMovementMethod.getInstance() + } + return + } + launch { if (!checkAvailability(loginType.loginType)) return@launch diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt index 63525c45..ceca8a00 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt @@ -5,7 +5,6 @@ package pl.szczodrzynski.edziennik.ui.modules.login import android.os.Bundle -import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -75,19 +74,6 @@ class LoginSummaryFragment : Fragment(), CoroutineScope { } b.finishButton.onClick { - if (!app.config.privacyPolicyAccepted) { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.privacy_policy) - .setMessage(Html.fromHtml("Korzystając z aplikacji potwierdzasz
    przeczytanie Polityki prywatności i akceptujesz jej postanowienia.")) - .setPositiveButton(R.string.i_agree) { _, _ -> - app.config.privacyPolicyAccepted = true - b.finishButton.performClick() - } - .setNegativeButton(R.string.i_disagree, null) - .show() - return@onClick - } - val args = Bundle( "registrationAllowed" to b.registerMeSwitch.isChecked ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52eef2ca..e491bf20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1423,4 +1423,5 @@ Brak dostępu do API Data kompilacji Aby móc zapisać wygenerowany plan lekcji musisz przyznać uprawnienia dostępu do pamięci urządzenia.\n\nKliknij OK, aby przyznać uprawnienia. + przeczytanie Polityki prywatności i akceptujesz jej postanowienia.

    Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]>
    From ea2974bfaea7e7ed7607b269fca46a467d0b07da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 19:46:22 +0200 Subject: [PATCH 020/240] [App] Improve unofficial build type info on debug. --- .../pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt index 3deaabfd..8d50a948 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt @@ -88,9 +88,10 @@ class BuildManager(val app: App) : CoroutineScope { else -> no.asColoredSpannable(mtrlYellow) } ) + isDebug -> no else -> TextUtils.concat( no.asColoredSpannable(mtrlRed), - if (gitAuthor != null) " ($gitAuthor)" else "" + if (gitAuthor.isNotNullNorBlank()) " ($gitAuthor)" else "" ) }, R.string.build_platform to when { From 582e2059d8887af985c7236f18eda892d0e302f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 5 Apr 2021 20:00:38 +0200 Subject: [PATCH 021/240] [App] Move buildTimestamp to manifest to improve build performance. --- app/build.gradle | 4 +++- app/src/main/AndroidManifest.xml | 2 ++ .../edziennik/utils/managers/BuildManager.kt | 9 ++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f49a6dad..72c03e94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,10 @@ android { versionName release.versionName buildConfigField "java.util.Map", "GIT_INFO", gitInfoMap - buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) buildConfigField "String", "VERSION_BASE", "\"${release.versionName}\"" + manifestPlaceholders = [ + buildTimestamp: String.valueOf(System.currentTimeMillis()) + ] multiDexEnabled = true diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f295533..af4b653a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,8 @@ android:usesCleartextTraffic="true" tools:ignore="UnusedAttribute"> + + + + + + + + + + + + diff --git a/app/src/main/res/layout/row_lesson_change_item.xml b/app/src/main/res/layout/agenda_lesson_changes_item.xml similarity index 72% rename from app/src/main/res/layout/row_lesson_change_item.xml rename to app/src/main/res/layout/agenda_lesson_changes_item.xml index 33515e89..8700a575 100644 --- a/app/src/main/res/layout/row_lesson_change_item.xml +++ b/app/src/main/res/layout/agenda_lesson_changes_item.xml @@ -1,11 +1,10 @@ - + app:cardCornerRadius="5dp"> + android:textColor="@color/md_white_1000" /> - - diff --git a/app/src/main/res/layout/row_teacher_absence_item.xml b/app/src/main/res/layout/agenda_teacher_absence_item.xml similarity index 72% rename from app/src/main/res/layout/row_teacher_absence_item.xml rename to app/src/main/res/layout/agenda_teacher_absence_item.xml index da78c625..4b749e8c 100644 --- a/app/src/main/res/layout/row_teacher_absence_item.xml +++ b/app/src/main/res/layout/agenda_teacher_absence_item.xml @@ -1,11 +1,10 @@ - + app:cardCornerRadius="5dp"> + android:textColor="@color/md_white_1000" /> - - diff --git a/app/src/main/res/layout/agenda_event_lesson_change.xml b/app/src/main/res/layout/agenda_wrapped_event.xml similarity index 77% rename from app/src/main/res/layout/agenda_event_lesson_change.xml rename to app/src/main/res/layout/agenda_wrapped_event.xml index c51f9b35..2ca38528 100644 --- a/app/src/main/res/layout/agenda_event_lesson_change.xml +++ b/app/src/main/res/layout/agenda_wrapped_event.xml @@ -1,15 +1,17 @@ + + - diff --git a/app/src/main/res/layout/agenda_event_teacher_absence.xml b/app/src/main/res/layout/agenda_wrapped_lesson_changes.xml similarity index 86% rename from app/src/main/res/layout/agenda_event_teacher_absence.xml rename to app/src/main/res/layout/agenda_wrapped_lesson_changes.xml index 785256d0..834d2ddd 100644 --- a/app/src/main/res/layout/agenda_event_teacher_absence.xml +++ b/app/src/main/res/layout/agenda_wrapped_lesson_changes.xml @@ -5,9 +5,9 @@ android:gravity="center_vertical" android:orientation="horizontal"> - diff --git a/app/src/main/res/layout/agenda_wrapped_teacher_absence.xml b/app/src/main/res/layout/agenda_wrapped_teacher_absence.xml new file mode 100644 index 00000000..4793b0d8 --- /dev/null +++ b/app/src/main/res/layout/agenda_wrapped_teacher_absence.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_day.xml b/app/src/main/res/layout/dialog_day.xml index 59fd7f8c..f1544f59 100644 --- a/app/src/main/res/layout/dialog_day.xml +++ b/app/src/main/res/layout/dialog_day.xml @@ -49,7 +49,7 @@ Date: Fri, 9 Apr 2021 21:52:04 +0200 Subject: [PATCH 035/240] [Agenda] Implement updating event list when changed. --- .../edziennik/data/db/dao/TimetableDao.kt | 2 + .../ui/dialogs/event/EventManualDialog.kt | 5 -- .../ui/modules/agenda/AgendaFragment.kt | 8 ++- .../modules/agenda/AgendaFragmentDefault.kt | 62 ++++++++++++++----- 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt index c7cbee12..8ce9ed9b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt @@ -84,6 +84,8 @@ abstract class TimetableDao : BaseDao { "LIMIT 1") fun getBetweenDates(dateFrom: Date, dateTo: Date) = getRaw("$QUERY WHERE (type != 3 AND date >= '${dateFrom.stringY_m_d}' AND date <= '${dateTo.stringY_m_d}') OR ((type = 3 OR type = 1) AND oldDate >= '${dateFrom.stringY_m_d}' AND oldDate <= '${dateTo.stringY_m_d}') $ORDER_BY") + fun getChanges(profileId: Int) = + getRaw("$QUERY WHERE timetable.profileId = $profileId AND $IS_CHANGED $ORDER_BY") // GET ALL - NOW fun getAllNow(profileId: Int) = diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index 97e34009..9beac76b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -20,7 +20,6 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent @@ -599,8 +598,6 @@ class EventManualDialog( dialog.dismiss() Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA) - activity.reloadTarget() } private fun finishRemoving() { editingEvent ?: return @@ -613,7 +610,5 @@ class EventManualDialog( removeEventDialog?.dismiss() dialog.dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA) - activity.reloadTarget() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt index dd3fbbbc..0d5fd425 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt @@ -75,7 +75,7 @@ class AgendaFragment : Fragment(), CoroutineScope { EventManualDialog( activity, app.profileId, - defaultDate = agendaDefault?.selectedDate + defaultDate = AgendaFragmentDefault.selectedDate ) }, BottomSheetPrimaryItem(true) @@ -112,7 +112,11 @@ class AgendaFragment : Fragment(), CoroutineScope { activity.navView.bottomBar.fabExtendedText = getString(R.string.add) activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon3.cmd_plus activity.navView.setFabOnClickListener { - EventManualDialog(activity, app.profileId, defaultDate = agendaDefault?.selectedDate) + EventManualDialog( + activity, + app.profileId, + defaultDate = AgendaFragmentDefault.selectedDate + ) } activity.gainAttention() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index 9e4e31ab..dff2722c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -7,7 +7,10 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda import android.util.SparseIntArray import androidx.core.util.forEach import androidx.core.util.set +import com.github.tibolte.agendacalendarview.CalendarManager import com.github.tibolte.agendacalendarview.CalendarPickerController +import com.github.tibolte.agendacalendarview.agenda.AgendaAdapter +import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent import com.github.tibolte.agendacalendarview.models.CalendarEvent import com.github.tibolte.agendacalendarview.models.IDayItem import kotlinx.coroutines.Dispatchers @@ -15,6 +18,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog @@ -33,30 +37,39 @@ class AgendaFragmentDefault( private val app: App, private val b: FragmentAgendaDefaultBinding ) { + companion object { + var selectedDate: Date = Date.getToday() + } + private val unreadDates = mutableSetOf() - var selectedDate: Date = Date.getToday() + private val events = mutableListOf() + private var isInitialized = false suspend fun initView(fragment: AgendaFragment) { - val dateStart = app.profile.dateSemester1Start.asCalendar - val dateEnd = app.profile.dateYearEnd.asCalendar - - val events = withContext(Dispatchers.Default) { - val events = mutableListOf() + isInitialized = false + withContext(Dispatchers.Default) { addLessonChanges(events) val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true) if (showTeacherAbsences) { addTeacherAbsence(events) } - - addEvents(events) - - return@withContext events } - if (!fragment.isAdded) - return + app.db.eventDao().getAll(app.profileId).observe(fragment) { + addEvents(events, it) + if (isInitialized) + updateView() + else + initViewPriv() + } + } + + private fun initViewPriv() { + val dateStart = app.profile.dateSemester1Start.asCalendar + val dateEnd = app.profile.dateYearEnd.asCalendar + b.agendaDefaultView.init( events, dateStart, @@ -71,7 +84,11 @@ class AgendaFragmentDefault( when (event) { is AgendaEvent -> DayDialog(activity, app.profileId, date) is LessonChangesEvent -> LessonChangeDialog(activity, app.profileId, date) - is TeacherAbsenceEvent -> TeacherAbsenceDialog(activity, app.profileId, date) + is TeacherAbsenceEvent -> TeacherAbsenceDialog( + activity, + app.profileId, + date + ) } } @@ -91,10 +108,25 @@ class AgendaFragmentDefault( LessonChangesEventRenderer(), TeacherAbsenceEventRenderer() ) + + isInitialized = true } - private fun addEvents(events: MutableList) { - val eventList = app.db.eventDao().getAllNow(app.profileId) + private fun updateView() { + val manager = CalendarManager.getInstance() + manager.events.clear() + manager.loadEvents(events, BaseCalendarEvent()) + + val adapter = b.agendaDefaultView.agendaView.agendaListView.adapter as? AgendaAdapter + adapter?.updateEvents(manager.events) + b.agendaDefaultView.agendaView.agendaListView.scrollToCurrentDate(selectedDate.asCalendar) + } + + private fun addEvents( + events: MutableList, + eventList: List + ) { + events.removeAll { it is AgendaEvent } events += eventList.map { if (!it.seen) From b14ef5cd78e93fdc2b715855b855862290211f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 10 Apr 2021 18:49:24 +0200 Subject: [PATCH 036/240] [Agenda] Limit event text to 3 lines max. --- app/src/main/res/layout/agenda_event_item.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/agenda_event_item.xml b/app/src/main/res/layout/agenda_event_item.xml index b07106f5..5f4ff89a 100644 --- a/app/src/main/res/layout/agenda_event_item.xml +++ b/app/src/main/res/layout/agenda_event_item.xml @@ -22,6 +22,8 @@ android:id="@+id/eventTitle" android:layout_width="match_parent" android:layout_height="match_parent" + android:ellipsize="end" + android:maxLines="3" android:textSize="16sp" tools:text="sprawdzian - Język polski" /> From 3eae8fb58b1bbedecedf2d629752ea1264d82199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 10 Apr 2021 20:51:29 +0200 Subject: [PATCH 037/240] [Agenda] Add config dialog. Add compact mode. --- .../edziennik/config/ProfileConfigUI.kt | 30 ++++ .../ui/dialogs/agenda/AgendaConfigDialog.kt | 93 +++++++++++ .../ui/modules/agenda/AgendaFragment.kt | 10 +- .../modules/agenda/AgendaFragmentDefault.kt | 19 ++- .../agenda/event/AgendaEventRenderer.kt | 6 +- .../settings/cards/SettingsRegisterCard.kt | 8 + app/src/main/res/layout/agenda_event_item.xml | 67 ++++---- .../main/res/layout/dialog_config_agenda.xml | 148 ++++++++++++++++++ app/src/main/res/values/strings.xml | 14 ++ 9 files changed, 359 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt create mode 100644 app/src/main/res/layout/dialog_config_agenda.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt index 5ce237a0..ca22cb76 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt @@ -15,6 +15,36 @@ class ProfileConfigUI(private val config: ProfileConfig) { get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT } set(value) { config.set("agendaViewType", value); mAgendaViewType = value } + private var mAgendaCompactMode: Boolean? = null + var agendaCompactMode: Boolean + get() { mAgendaCompactMode = mAgendaCompactMode ?: config.values.get("agendaCompactMode", false); return mAgendaCompactMode ?: false } + set(value) { config.set("agendaCompactMode", value); mAgendaCompactMode = value } + + private var mAgendaGroupByType: Boolean? = null + var agendaGroupByType: Boolean + get() { mAgendaGroupByType = mAgendaGroupByType ?: config.values.get("agendaGroupByType", false); return mAgendaGroupByType ?: false } + set(value) { config.set("agendaGroupByType", value); mAgendaGroupByType = value } + + private var mAgendaLessonChanges: Boolean? = null + var agendaLessonChanges: Boolean + get() { mAgendaLessonChanges = mAgendaLessonChanges ?: config.values.get("agendaLessonChanges", true); return mAgendaLessonChanges ?: true } + set(value) { config.set("agendaLessonChanges", value); mAgendaLessonChanges = value } + + private var mAgendaTeacherAbsence: Boolean? = null + var agendaTeacherAbsence: Boolean + get() { mAgendaTeacherAbsence = mAgendaTeacherAbsence ?: config.values.get("agendaTeacherAbsence", true); return mAgendaTeacherAbsence ?: true } + set(value) { config.set("agendaTeacherAbsence", value); mAgendaTeacherAbsence = value } + + private var mAgendaElearningMark: Boolean? = null + var agendaElearningMark: Boolean + get() { mAgendaElearningMark = mAgendaElearningMark ?: config.values.get("agendaElearningMark", false); return mAgendaElearningMark ?: false } + set(value) { config.set("agendaElearningMark", value); mAgendaElearningMark = value } + + private var mAgendaElearningGroup: Boolean? = null + var agendaElearningGroup: Boolean + get() { mAgendaElearningGroup = mAgendaElearningGroup ?: config.values.get("agendaElearningGroup", true); return mAgendaElearningGroup ?: true } + set(value) { config.set("agendaElearningGroup", value); mAgendaElearningGroup = value } + private var mHomeCards: List? = null var homeCards: List get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt new file mode 100644 index 00000000..150d7d65 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/agenda/AgendaConfigDialog.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.agenda + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED +import pl.szczodrzynski.edziennik.databinding.DialogConfigAgendaBinding +import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog +import java.util.* + +class AgendaConfigDialog( + private val activity: AppCompatActivity, + private val reloadOnDismiss: Boolean = true, + private val onShowListener: ((tag: String) -> Unit)? = null, + private val onDismissListener: ((tag: String) -> Unit)? = null +) { + companion object { + const val TAG = "AgendaConfigDialog" + } + + private val app by lazy { activity.application as App } + private val config by lazy { app.config.ui } + private val profileConfig by lazy { app.config.forProfile().ui } + + private lateinit var b: DialogConfigAgendaBinding + private lateinit var dialog: AlertDialog + + init { run { + if (activity.isFinishing) + return@run + b = DialogConfigAgendaBinding.inflate(activity.layoutInflater) + onShowListener?.invoke(TAG) + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.menu_agenda_config) + .setView(b.root) + .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + .setOnDismissListener { + saveConfig() + onDismissListener?.invoke(TAG) + if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() + } + .create() + loadConfig() + dialog.show() + }} + + private fun loadConfig() { + b.config = profileConfig + b.isAgendaMode = profileConfig.agendaViewType == Profile.AGENDA_DEFAULT + + b.eventSharingEnabled.isChecked = app.profile.enableSharedEvents + && app.profile.registration == REGISTRATION_ENABLED + b.eventSharingEnabled.onChange { _, isChecked -> + if (isChecked && app.profile.registration != REGISTRATION_ENABLED) { + b.eventSharingEnabled.isChecked = false + val dialog = RegistrationConfigDialog(activity, app.profile, onChangeListener = { enabled -> + b.eventSharingEnabled.isChecked = enabled + setEventSharingEnabled(enabled) + }, onShowListener, onDismissListener) + dialog.showEnableDialog() + return@onChange + } + setEventSharingEnabled(isChecked) + } + } + + private fun setEventSharingEnabled(enabled: Boolean) { + if (enabled == app.profile.enableSharedEvents) + return + app.profile.enableSharedEvents = enabled + app.profileSave() + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.event_sharing) + .setMessage( + if (enabled) + R.string.settings_register_shared_events_dialog_enabled_text + else + R.string.settings_register_shared_events_dialog_disabled_text + ) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun saveConfig() { + + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt index 0d5fd425..8d228dcc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt @@ -25,6 +25,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding +import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.Themes @@ -78,6 +79,13 @@ class AgendaFragment : Fragment(), CoroutineScope { defaultDate = AgendaFragmentDefault.selectedDate ) }, + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_agenda_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + AgendaConfigDialog(activity, true, null, null) + }, BottomSheetPrimaryItem(true) .withTitle(R.string.menu_agenda_change_view) .withIcon(if (type == Profile.AGENDA_DEFAULT) CommunityMaterial.Icon.cmd_calendar_outline else CommunityMaterial.Icon2.cmd_format_list_bulleted_square) @@ -135,8 +143,6 @@ class AgendaFragment : Fragment(), CoroutineScope { agendaDefault = AgendaFragmentDefault(activity, app, b) agendaDefault?.initView(this@AgendaFragment) - - b.progressBar.visibility = View.GONE }}} private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index dff2722c..680eeb18 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda import android.util.SparseIntArray import androidx.core.util.forEach import androidx.core.util.set +import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.CalendarManager import com.github.tibolte.agendacalendarview.CalendarPickerController import com.github.tibolte.agendacalendarview.agenda.AgendaAdapter @@ -21,6 +22,7 @@ import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog +import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEvent @@ -44,17 +46,17 @@ class AgendaFragmentDefault( private val unreadDates = mutableSetOf() private val events = mutableListOf() private var isInitialized = false + private val profileConfig by lazy { app.config.forProfile().ui } suspend fun initView(fragment: AgendaFragment) { isInitialized = false withContext(Dispatchers.Default) { - addLessonChanges(events) + if (profileConfig.agendaLessonChanges) + addLessonChanges(events) - val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true) - if (showTeacherAbsences) { + if (profileConfig.agendaTeacherAbsence) addTeacherAbsence(events) - } } app.db.eventDao().getAll(app.profileId).observe(fragment) { @@ -70,6 +72,8 @@ class AgendaFragmentDefault( val dateStart = app.profile.dateSemester1Start.asCalendar val dateEnd = app.profile.dateYearEnd.asCalendar + val isCompactMode = profileConfig.agendaCompactMode + b.agendaDefaultView.init( events, dateStart, @@ -82,13 +86,15 @@ class AgendaFragmentDefault( val date = Date.fromCalendar(event.instanceDay) when (event) { - is AgendaEvent -> DayDialog(activity, app.profileId, date) + is AgendaEvent -> EventDetailsDialog(activity, event.event) is LessonChangesEvent -> LessonChangeDialog(activity, app.profileId, date) is TeacherAbsenceEvent -> TeacherAbsenceDialog( activity, app.profileId, date ) + is BaseCalendarEvent -> if (event.isPlaceHolder) + DayDialog(activity, app.profileId, date) } } @@ -104,12 +110,13 @@ class AgendaFragmentDefault( } } }, - AgendaEventRenderer(), + AgendaEventRenderer(isCompactMode), LessonChangesEventRenderer(), TeacherAbsenceEventRenderer() ) isInitialized = true + b.progressBar.isVisible = false } private fun updateView() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt index da28ffd5..6e78776a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt @@ -11,13 +11,17 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding import pl.szczodrzynski.edziennik.utils.Colors -class AgendaEventRenderer : EventRenderer() { +class AgendaEventRenderer( + private val isCompact: Boolean +) : EventRenderer() { @SuppressLint("SetTextI18n") override fun render(view: View, aEvent: AgendaEvent) { val b = AgendaWrappedEventBinding.bind(view).item val event = aEvent.event + b.isCompact = isCompact + b.card.setCardBackgroundColor(event.eventColor) b.eventTitle.setTextColor(Colors.legibleTextColor(event.eventColor)) b.eventSubtitle.setTextColor(Colors.legibleTextColor(event.eventColor)) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt index 36239083..3dacd82d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.after import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED +import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog @@ -58,6 +59,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { } override fun getItems() = listOfNotNull( + util.createActionItem( + text = R.string.menu_agenda_config, + icon = CommunityMaterial.Icon.cmd_calendar_outline + ) { + AgendaConfigDialog(activity, reloadOnDismiss = false) + }, + util.createActionItem( text = R.string.menu_grades_config, icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline diff --git a/app/src/main/res/layout/agenda_event_item.xml b/app/src/main/res/layout/agenda_event_item.xml index 5f4ff89a..c70c4a95 100644 --- a/app/src/main/res/layout/agenda_event_item.xml +++ b/app/src/main/res/layout/agenda_event_item.xml @@ -3,37 +3,50 @@ ~ Copyright (c) Kuba Szczodrzyński 2021-4-8. --> - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + + + + app:cardCornerRadius="5dp" + tools:cardBackgroundColor="@color/blue_selected"> - - - - - + android:orientation="vertical" + android:padding="10dp"> + + + + + + + diff --git a/app/src/main/res/layout/dialog_config_agenda.xml b/app/src/main/res/layout/dialog_config_agenda.xml new file mode 100644 index 00000000..475c9d84 --- /dev/null +++ b/app/src/main/res/layout/dialog_config_agenda.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5f21e05..6f3021e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1425,4 +1425,18 @@ Aby móc zapisać wygenerowany plan lekcji musisz przyznać uprawnienia dostępu do pamięci urządzenia.\n\nKliknij OK, aby przyznać uprawnienia. przeczytanie Polityki prywatności i akceptujesz jej postanowienia.

    Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]>
    Szkolny.eu v%s\n%s + Ustawienia terminarza + Wygląd + Pokazuj zmiany planu lekcji + Pokazuj nieobecności nauczycieli + Tryb kompaktowy + Mniejszy rozmiar wydarzeń na liście + Grupuj wydarzenia tego samego typu + Niedostępne w trybie kalendarza + Udostępnianie wydarzeń + Włącz Udostępnianie wydarzeń + Nauczanie zdalne + Ustaw wydarzenia jako lekcje on-line + Wybierz rodzaj wydarzeń + Grupuj lekcje on-line na liście From 777ae945e0e8ce7d477449d83f10bbd3242f711c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sat, 10 Apr 2021 22:26:43 +0200 Subject: [PATCH 038/240] [Agenda] Implement grouping events by type. --- .../modules/agenda/AgendaFragmentDefault.kt | 34 ++++++++++-- .../modules/agenda/event/AgendaEventGroup.kt | 23 ++++++++ .../agenda/event/AgendaEventGroupRenderer.kt | 31 +++++++++++ app/src/main/res/layout/agenda_group_item.xml | 55 +++++++++++++++++++ .../main/res/layout/agenda_wrapped_group.xml | 18 ++++++ 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt create mode 100644 app/src/main/res/layout/agenda_group_item.xml create mode 100644 app/src/main/res/layout/agenda_wrapped_group.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index 680eeb18..9aaf3d5a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -26,6 +26,8 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventGroup +import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventGroupRenderer import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventRenderer import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer @@ -111,6 +113,7 @@ class AgendaFragmentDefault( } }, AgendaEventRenderer(isCompactMode), + AgendaEventGroupRenderer(), LessonChangesEventRenderer(), TeacherAbsenceEventRenderer() ) @@ -135,10 +138,33 @@ class AgendaFragmentDefault( ) { events.removeAll { it is AgendaEvent } - events += eventList.map { - if (!it.seen) - unreadDates.add(it.date.value) - AgendaEvent(it) + if (!profileConfig.agendaGroupByType) { + events += eventList.map { + if (!it.seen) + unreadDates.add(it.date.value) + AgendaEvent(it) + } + return + } + + eventList.groupBy { + it.date.value to it.type + }.forEach { (_, list) -> + val event = list.first() + if (list.size == 1) { + if (!event.seen) + unreadDates.add(event.date.value) + events += AgendaEvent(event) + } + else { + events.add(0, AgendaEventGroup( + profileId = event.profileId, + date = event.date, + typeName = event.typeName ?: "-", + typeColor = event.typeColor ?: event.eventColor, + eventCount = list.size + )) + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt new file mode 100644 index 00000000..60e5f769 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.event + +import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent +import pl.szczodrzynski.edziennik.utils.models.Date + +class AgendaEventGroup( + val profileId: Int, + val date: Date, + val typeName: String, + val typeColor: Int, + val eventCount: Int +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = typeColor, + showBadge = false +) { + override fun copy() = AgendaEventGroup(profileId, date, typeName, typeColor, eventCount) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt new file mode 100644 index 00000000..191f526c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.modules.agenda.event + +import android.view.View +import com.github.tibolte.agendacalendarview.render.EventRenderer +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedGroupBinding +import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors + +class AgendaEventGroupRenderer : EventRenderer() { + + override fun render(view: View, event: AgendaEventGroup) { + val b = AgendaWrappedGroupBinding.bind(view).item + + b.foreground.foreground.setTintColor(event.typeColor) + b.background.background.setTintColor(event.typeColor) + b.name.background.setTintColor(event.typeColor) + b.name.text = event.typeName + b.name.setTextColor(Colors.legibleTextColor(event.typeColor)) + b.count.text = event.eventCount.toString() + b.count.background.setTintColor(android.R.attr.colorBackground.resolveAttr(view.context)) + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_group +} + diff --git a/app/src/main/res/layout/agenda_group_item.xml b/app/src/main/res/layout/agenda_group_item.xml new file mode 100644 index 00000000..23d3486d --- /dev/null +++ b/app/src/main/res/layout/agenda_group_item.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_wrapped_group.xml b/app/src/main/res/layout/agenda_wrapped_group.xml new file mode 100644 index 00000000..6b081d8e --- /dev/null +++ b/app/src/main/res/layout/agenda_wrapped_group.xml @@ -0,0 +1,18 @@ + + + + + + + From f5ceaa9afe612d8104d6a5fa04147cbf81a5df94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 11 Apr 2021 22:08:33 +0200 Subject: [PATCH 039/240] [Agenda] Add unread badges to events and groups. --- .../ui/dialogs/event/EventDetailsDialog.kt | 4 + .../modules/agenda/AgendaFragmentDefault.kt | 117 ++++++++++++++++-- .../edziennik/ui/modules/agenda/BaseEvent.kt | 11 +- .../ui/modules/agenda/event/AgendaEvent.kt | 7 +- .../modules/agenda/event/AgendaEventGroup.kt | 7 +- .../agenda/event/AgendaEventGroupRenderer.kt | 12 +- .../agenda/event/AgendaEventRenderer.kt | 68 +++++++--- .../lessonchanges/LessonChangesEvent.kt | 10 +- .../LessonChangesEventRenderer.kt | 23 +++- .../teacherabsence/TeacherAbsenceEvent.kt | 4 +- .../TeacherAbsenceEventRenderer.kt | 19 ++- .../main/res/layout/agenda_counter_item.xml | 61 +++++++++ .../res/layout/agenda_event_compact_item.xml | 57 +++++++++ app/src/main/res/layout/agenda_event_item.xml | 66 ++++++---- app/src/main/res/layout/agenda_group_item.xml | 23 ++-- ...changes.xml => agenda_wrapped_counter.xml} | 6 +- ...e.xml => agenda_wrapped_event_compact.xml} | 6 +- 17 files changed, 407 insertions(+), 94 deletions(-) create mode 100644 app/src/main/res/layout/agenda_counter_item.xml create mode 100644 app/src/main/res/layout/agenda_event_compact_item.xml rename app/src/main/res/layout/{agenda_wrapped_lesson_changes.xml => agenda_wrapped_counter.xml} (83%) rename app/src/main/res/layout/{agenda_wrapped_teacher_absence.xml => agenda_wrapped_event_compact.xml} (83%) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index cdd2e50a..0958188e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -92,6 +92,10 @@ class EventDetailsDialog( b.eventShared = eventShared b.eventOwn = eventOwn + if (!event.seen) { + app.eventManager.markAsSeen(event) + } + val bullet = " • " val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index 9aaf3d5a..1aee0ab7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -5,6 +5,8 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda import android.util.SparseIntArray +import android.widget.AbsListView +import android.widget.AbsListView.OnScrollListener import androidx.core.util.forEach import androidx.core.util.set import androidx.core.view.isVisible @@ -14,9 +16,7 @@ import com.github.tibolte.agendacalendarview.agenda.AgendaAdapter import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent import com.github.tibolte.agendacalendarview.models.CalendarEvent import com.github.tibolte.agendacalendarview.models.IDayItem -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.db.full.EventFull @@ -40,16 +40,67 @@ class AgendaFragmentDefault( private val activity: MainActivity, private val app: App, private val b: FragmentAgendaDefaultBinding -) { +) : OnScrollListener, CoroutineScope { companion object { var selectedDate: Date = Date.getToday() } + override val coroutineContext = Job() + Dispatchers.Main + private val unreadDates = mutableSetOf() private val events = mutableListOf() private var isInitialized = false private val profileConfig by lazy { app.config.forProfile().ui } + private val listView + get() = b.agendaDefaultView.agendaView.agendaListView + private val adapter + get() = listView.adapter as? AgendaAdapter + private val manager + get() = CalendarManager.getInstance() + + // TODO: 2021-04-11 find a way to attach the OnScrollListener automatically + // then set this to IDLE by default + // the FAB also needs the original listener, though + private var scrollState = OnScrollListener.SCROLL_STATE_TOUCH_SCROLL + private var updatePending = false + private var notifyPending = false + override fun onScrollStateChanged(view: AbsListView?, newScrollState: Int) { + scrollState = newScrollState + if (updatePending) updateData() + if (notifyPending) notifyDataSetChanged() + } + + /** + * Mark the data as needing update, either after 1 second (when + * not scrolling) or 1 second after scrolling stops. + */ + private fun updateData() = launch { + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + updatePending = false + delay(1000) + notifyDataSetChanged() + } else updatePending = true + } + + /** + * Notify the adapter about changes, either instantly or after + * scrolling stops. + */ + private fun notifyDataSetChanged() { + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + notifyPending = false + adapter?.notifyDataSetChanged() + } else notifyPending = true + } + + override fun onScroll( + view: AbsListView?, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) = Unit + suspend fun initView(fragment: AgendaFragment) { isInitialized = false @@ -95,9 +146,22 @@ class AgendaFragmentDefault( app.profileId, date ) + is AgendaEventGroup -> DayDialog(activity, app.profileId, date) is BaseCalendarEvent -> if (event.isPlaceHolder) DayDialog(activity, app.profileId, date) } + + if (event is BaseEvent && event.showItemBadge) { + val unreadCount = manager.events.count { + it.instanceDay.equals(event.instanceDay) && it.showBadge + } + // only clicked event is unread, remove the day badge + if (unreadCount == 1 && event.showBadge) { + event.dayReference.showBadge = false + unreadDates.remove(date.value) + } + setAsRead(event) + } } override fun onScrollToDate(calendar: Calendar) { @@ -105,6 +169,7 @@ class AgendaFragmentDefault( // Mark as read scrolled date if (selectedDate.value in unreadDates) { + setAsRead(calendar) activity.launch(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, selectedDate, true) } @@ -123,20 +188,45 @@ class AgendaFragmentDefault( } private fun updateView() { - val manager = CalendarManager.getInstance() manager.events.clear() manager.loadEvents(events, BaseCalendarEvent()) - val adapter = b.agendaDefaultView.agendaView.agendaListView.adapter as? AgendaAdapter adapter?.updateEvents(manager.events) - b.agendaDefaultView.agendaView.agendaListView.scrollToCurrentDate(selectedDate.asCalendar) + listView.scrollToCurrentDate(selectedDate.asCalendar) + } + + private fun setAsRead(date: Calendar) { + // get all events matching the date + val events = manager.events.filter { + if (it.instanceDay.equals(date) && it.showBadge && it is AgendaEvent) { + // hide the day badge for the date + it.dayReference.showBadge = false + return@filter true + } + false + } + // set this date's events as read + setAsRead(*events.toTypedArray()) + } + + private fun setAsRead(vararg event: CalendarEvent) { + // hide per-event badges + for (e in event) { + events.firstOrNull { + it == e + }?.showBadge = false + e.showBadge = false + } + + listView.setOnScrollListener(this) + updateData() } private fun addEvents( events: MutableList, eventList: List ) { - events.removeAll { it is AgendaEvent } + events.removeAll { it is AgendaEvent || it is AgendaEventGroup } if (!profileConfig.agendaGroupByType) { events += eventList.map { @@ -155,14 +245,14 @@ class AgendaFragmentDefault( if (!event.seen) unreadDates.add(event.date.value) events += AgendaEvent(event) - } - else { + } else { events.add(0, AgendaEventGroup( profileId = event.profileId, date = event.date, typeName = event.typeName ?: "-", typeColor = event.typeColor ?: event.eventColor, - eventCount = list.size + count = list.size, + showBadge = list.any { !it.seen } )) } } @@ -179,7 +269,8 @@ class AgendaFragmentDefault( LessonChangesEvent( app.profileId, date = date ?: return@mapNotNull null, - changeCount = changes.size + count = changes.size, + showBadge = changes.any { !it.seen } ) } } @@ -200,7 +291,7 @@ class AgendaFragmentDefault( events += TeacherAbsenceEvent( app.profileId, date = Date.fromValue(dateInt), - absenceCount = count + count = count ) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt index f183485b..62569504 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/BaseEvent.kt @@ -13,7 +13,8 @@ open class BaseEvent( private val id: Long, private val time: Calendar, private val color: Int, - private val showBadge: Boolean + private var showBadge: Boolean, + var showItemBadge: Boolean = showBadge ) : CalendarEvent { override fun copy() = BaseEvent(id, time, color, showBadge) @@ -36,6 +37,12 @@ open class BaseEvent( weekReference = value } + override fun getShowBadge() = showBadge + override fun setShowBadge(value: Boolean) { + showBadge = value + showItemBadge = value + } + override fun getId() = id override fun getStartTime() = time override fun getEndTime() = time @@ -44,7 +51,6 @@ open class BaseEvent( override fun getLocation() = "" override fun getColor() = color override fun getTextColor() = 0 - override fun getShowBadge() = showBadge override fun isPlaceholder() = false override fun isAllDay() = false @@ -55,7 +61,6 @@ open class BaseEvent( override fun setDescription(value: String) = Unit override fun setLocation(value: String) = Unit override fun setTextColor(value: Int) = Unit - override fun setShowBadge(value: Boolean) = Unit override fun setPlaceholder(value: Boolean) = Unit override fun setAllDay(value: Boolean) = Unit } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt index 3cd05727..e6ba6e23 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEvent.kt @@ -8,12 +8,13 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent class AgendaEvent( - val event: EventFull + val event: EventFull, + showBadge: Boolean = !event.seen ) : BaseEvent( id = event.id, time = event.startTimeCalendar, color = event.eventColor, - showBadge = !event.seen + showBadge = showBadge ) { - override fun copy() = AgendaEvent(event) + override fun copy() = AgendaEvent(event, showBadge) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt index 60e5f769..e50be2c3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt @@ -12,12 +12,13 @@ class AgendaEventGroup( val date: Date, val typeName: String, val typeColor: Int, - val eventCount: Int + val count: Int, + showBadge: Boolean ) : BaseEvent( id = date.value.toLong(), time = date.asCalendar, color = typeColor, - showBadge = false + showBadge = showBadge ) { - override fun copy() = AgendaEventGroup(profileId, date, typeName, typeColor, eventCount) + override fun copy() = AgendaEventGroup(profileId, date, typeName, typeColor, count, showBadge) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt index 191f526c..2a1f9af8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroupRenderer.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda.event import android.view.View +import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AgendaWrappedGroupBinding @@ -17,13 +18,14 @@ class AgendaEventGroupRenderer : EventRenderer() { override fun render(view: View, event: AgendaEventGroup) { val b = AgendaWrappedGroupBinding.bind(view).item - b.foreground.foreground.setTintColor(event.typeColor) - b.background.background.setTintColor(event.typeColor) - b.name.background.setTintColor(event.typeColor) + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) b.name.text = event.typeName - b.name.setTextColor(Colors.legibleTextColor(event.typeColor)) - b.count.text = event.eventCount.toString() + b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.count.text = event.count.toString() b.count.background.setTintColor(android.R.attr.colorBackground.resolveAttr(view.context)) + + b.badge.isVisible = event.showItemBadge } override fun getEventLayout(): Int = R.layout.agenda_wrapped_group diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt index 6e78776a..dafdb853 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt @@ -6,36 +6,72 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda.event import android.annotation.SuppressLint import android.view.View +import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding +import pl.szczodrzynski.edziennik.join +import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.setTintColor import pl.szczodrzynski.edziennik.utils.Colors class AgendaEventRenderer( - private val isCompact: Boolean + val isCompact: Boolean ) : EventRenderer() { @SuppressLint("SetTextI18n") override fun render(view: View, aEvent: AgendaEvent) { - val b = AgendaWrappedEventBinding.bind(view).item val event = aEvent.event - b.isCompact = isCompact + val timeText = if (event.time == null) + view.context.getString(R.string.agenda_event_all_day) + else + event.time!!.stringHM - b.card.setCardBackgroundColor(event.eventColor) - b.eventTitle.setTextColor(Colors.legibleTextColor(event.eventColor)) - b.eventSubtitle.setTextColor(Colors.legibleTextColor(event.eventColor)) + val eventTitle = "${event.typeName ?: "wydarzenie"} - ${event.topic}" - b.eventTitle.text = "${event.typeName ?: "wydarzenie"} - ${event.topic}" - b.eventSubtitle.text = - (if (event.time == null) - view.context.getString(R.string.agenda_event_all_day) - else - event.time!!.stringHM) + - (event.subjectLongName?.let { ", $it" } ?: "") + - (event.teacherName?.let { ", $it" } ?: "") + - (event.teamName?.let { ", $it" } ?: "") + val eventSubtitle = listOfNotNull( + timeText, + event.subjectLongName, + event.teacherName, + event.teamName + ).join(", ") + + if (isCompact) { + val b = AgendaWrappedEventCompactBinding.bind(view).item + + b.card.foreground.setTintColor(event.eventColor) + b.card.background.setTintColor(event.eventColor) + b.title.text = eventTitle + b.title.setTextColor(Colors.legibleTextColor(event.eventColor)) + + b.badgeBackground.isVisible = aEvent.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(view.context) + ) + b.badge.isVisible = aEvent.showItemBadge + } + else { + val b = AgendaWrappedEventBinding.bind(view).item + + b.card.foreground.setTintColor(event.eventColor) + b.card.background.setTintColor(event.eventColor) + b.title.text = eventTitle + b.title.setTextColor(Colors.legibleTextColor(event.eventColor)) + b.subtitle.text = eventSubtitle + b.subtitle.setTextColor(Colors.legibleTextColor(event.eventColor)) + + b.badgeBackground.isVisible = aEvent.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(view.context) + ) + b.badge.isVisible = aEvent.showItemBadge + } } - override fun getEventLayout(): Int = R.layout.agenda_wrapped_event + override fun getEventLayout() = if (isCompact) + R.layout.agenda_wrapped_event_compact + else + R.layout.agenda_wrapped_event } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt index e65c82bd..b38d8829 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEvent.kt @@ -10,12 +10,16 @@ import pl.szczodrzynski.edziennik.utils.models.Date class LessonChangesEvent( val profileId: Int, val date: Date, - val changeCount: Int + val count: Int, + showBadge: Boolean ) : BaseEvent( id = date.value.toLong(), time = date.asCalendar, color = 0xff78909c.toInt(), - showBadge = false + showBadge = false, + showItemBadge = showBadge ) { - override fun copy() = LessonChangesEvent(profileId, date, changeCount) + override fun copy() = LessonChangesEvent(profileId, date, count, showItemBadge) + + override fun getShowBadge() = false } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt index 5ffc788c..8b8ff81b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt @@ -5,17 +5,32 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges import android.view.View +import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.databinding.AgendaWrappedLessonChangesBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding +import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors class LessonChangesEventRenderer : EventRenderer() { override fun render(view: View, event: LessonChangesEvent) { - val b = AgendaWrappedLessonChangesBinding.bind(view).item + val b = AgendaWrappedCounterBinding.bind(view).item - b.lessonChangeCount.text = event.changeCount.toString() + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_lesson_changes) + b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.count.text = event.count.toString() + b.count.setTextColor(b.name.currentTextColor) + + b.badgeBackground.isVisible = event.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(view.context) + ) + b.badge.isVisible = event.showItemBadge } - override fun getEventLayout(): Int = R.layout.agenda_wrapped_lesson_changes + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt index 4208814b..b0b01f50 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt @@ -10,12 +10,12 @@ import pl.szczodrzynski.edziennik.utils.models.Date class TeacherAbsenceEvent( val profileId: Int, val date: Date, - val absenceCount: Int + val count: Int ) : BaseEvent( id = date.value.toLong(), time = date.asCalendar, color = 0xffff1744.toInt(), showBadge = false ) { - override fun copy() = TeacherAbsenceEvent(profileId, date, absenceCount) + override fun copy() = TeacherAbsenceEvent(profileId, date, count) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt index 756c60c9..ec0a3915 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt @@ -5,17 +5,28 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence import android.view.View +import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.databinding.AgendaWrappedTeacherAbsenceBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding +import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors class TeacherAbsenceEventRenderer : EventRenderer() { override fun render(view: View, event: TeacherAbsenceEvent) { - val b = AgendaWrappedTeacherAbsenceBinding.bind(view).item + val b = AgendaWrappedCounterBinding.bind(view).item - b.teacherAbsenceCount.text = event.absenceCount.toString() + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_teacher_absence) + b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.count.text = event.count.toString() + b.count.setTextColor(b.name.currentTextColor) + + b.badgeBackground.isVisible = false + b.badge.isVisible = false } - override fun getEventLayout(): Int = R.layout.agenda_wrapped_teacher_absence + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter } diff --git a/app/src/main/res/layout/agenda_counter_item.xml b/app/src/main/res/layout/agenda_counter_item.xml new file mode 100644 index 00000000..334c419c --- /dev/null +++ b/app/src/main/res/layout/agenda_counter_item.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_event_compact_item.xml b/app/src/main/res/layout/agenda_event_compact_item.xml new file mode 100644 index 00000000..df48e70c --- /dev/null +++ b/app/src/main/res/layout/agenda_event_compact_item.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/agenda_event_item.xml b/app/src/main/res/layout/agenda_event_item.xml index c70c4a95..3ede7682 100644 --- a/app/src/main/res/layout/agenda_event_item.xml +++ b/app/src/main/res/layout/agenda_event_item.xml @@ -3,50 +3,64 @@ ~ Copyright (c) Kuba Szczodrzyński 2021-4-8. --> - + - - - - - - - - + android:orientation="horizontal"> + tools:text="sprawdzian - Język polski" + tools:textColor="@color/md_white_1000" /> + tools:text="9:05, biologia, Jan Kowalski, 7a" + tools:textColor="@color/md_white_1000" /> - - + + + + + + diff --git a/app/src/main/res/layout/agenda_group_item.xml b/app/src/main/res/layout/agenda_group_item.xml index 23d3486d..4787a8cf 100644 --- a/app/src/main/res/layout/agenda_group_item.xml +++ b/app/src/main/res/layout/agenda_group_item.xml @@ -5,33 +5,29 @@ + android:orientation="horizontal"> @@ -43,7 +39,6 @@ android:layout_marginRight="-1dp" android:background="@drawable/bg_rounded_8dp" android:gravity="center" - android:paddingVertical="10dp" android:paddingStart="16dp" android:paddingLeft="16dp" android:paddingEnd="18dp" @@ -52,4 +47,12 @@ tools:backgroundTint="?android:colorBackground" tools:text="3" /> + + diff --git a/app/src/main/res/layout/agenda_wrapped_lesson_changes.xml b/app/src/main/res/layout/agenda_wrapped_counter.xml similarity index 83% rename from app/src/main/res/layout/agenda_wrapped_lesson_changes.xml rename to app/src/main/res/layout/agenda_wrapped_counter.xml index 834d2ddd..31864636 100644 --- a/app/src/main/res/layout/agenda_wrapped_lesson_changes.xml +++ b/app/src/main/res/layout/agenda_wrapped_counter.xml @@ -1,4 +1,8 @@ + + diff --git a/app/src/main/res/layout/agenda_wrapped_teacher_absence.xml b/app/src/main/res/layout/agenda_wrapped_event_compact.xml similarity index 83% rename from app/src/main/res/layout/agenda_wrapped_teacher_absence.xml rename to app/src/main/res/layout/agenda_wrapped_event_compact.xml index 4793b0d8..997c24db 100644 --- a/app/src/main/res/layout/agenda_wrapped_teacher_absence.xml +++ b/app/src/main/res/layout/agenda_wrapped_event_compact.xml @@ -1,4 +1,8 @@ + + From 12619f6bde218e251745438b0a474223f0bda656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 12 Apr 2021 11:50:54 +0200 Subject: [PATCH 040/240] [Agenda] Update scroll listeners code. --- app/build.gradle | 2 +- .../modules/agenda/AgendaFragmentDefault.kt | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 72c03e94..26ac63c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,7 +152,7 @@ dependencies { implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2" // Szkolny.eu libraries/forks - implementation "eu.szkolny:agendacalendarview:1799f8ef47" + implementation "eu.szkolny:agendacalendarview:5431f03098" implementation "eu.szkolny:cafebar:5bf0c618de" implementation "eu.szkolny.fslogin:lib:2.0.0" implementation "eu.szkolny:material-about-library:1d5ebaf47c" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index 1aee0ab7..eb6f74fb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -59,18 +59,28 @@ class AgendaFragmentDefault( private val manager get() = CalendarManager.getInstance() - // TODO: 2021-04-11 find a way to attach the OnScrollListener automatically - // then set this to IDLE by default - // the FAB also needs the original listener, though - private var scrollState = OnScrollListener.SCROLL_STATE_TOUCH_SCROLL + private var scrollState = OnScrollListener.SCROLL_STATE_IDLE private var updatePending = false private var notifyPending = false override fun onScrollStateChanged(view: AbsListView?, newScrollState: Int) { + b.agendaDefaultView.agendaScrollListener.onScrollStateChanged(view, scrollState) scrollState = newScrollState if (updatePending) updateData() if (notifyPending) notifyDataSetChanged() } + override fun onScroll( + view: AbsListView?, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) = b.agendaDefaultView.agendaScrollListener.onScroll( + view, + firstVisibleItem, + visibleItemCount, + totalItemCount + ) + /** * Mark the data as needing update, either after 1 second (when * not scrolling) or 1 second after scrolling stops. @@ -94,13 +104,6 @@ class AgendaFragmentDefault( } else notifyPending = true } - override fun onScroll( - view: AbsListView?, - firstVisibleItem: Int, - visibleItemCount: Int, - totalItemCount: Int - ) = Unit - suspend fun initView(fragment: AgendaFragment) { isInitialized = false @@ -183,6 +186,8 @@ class AgendaFragmentDefault( TeacherAbsenceEventRenderer() ) + listView.setOnScrollListener(this) + isInitialized = true b.progressBar.isVisible = false } From 3eb09033bf6236e0e447e4ec7e5d515a51fdce17 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 12 Apr 2021 12:26:52 +0200 Subject: [PATCH 041/240] [Strings] Update copyright date (#26) * Update copyright date * Update copyright in other languages --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-en/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9949f2e2..3dd3dcfc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -856,7 +856,7 @@ Open-Source-Lizenzen Datenschutzrichtlinie E-Klassenbuch - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Februar 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - April 2021 Klicken Sie hier, um nach Aktualisierungen zu suchen Aktualisierung Version diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 64b56cdb..86911c58 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -858,7 +858,7 @@ Open-source licenses Privacy policy E-register - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - February 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - April 2021 Click to check for updates Update Version diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5f21e05..3b0ee64a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -921,7 +921,7 @@ Licencje open-source Polityka prywatności E-dziennik - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - luty 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - kwiecień 2021 Kliknij, aby sprawdzić aktualizacje Aktualizacja Wersja From 8b1529f2404d65027b02b711528fc6806d3d02c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 12 Apr 2021 13:13:10 +0200 Subject: [PATCH 042/240] [App] Make registerAvailability flavor-aware. --- .../edziennik/config/ConfigSync.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt index 068400a1..387e5cf7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.config import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.config.utils.get import pl.szczodrzynski.edziennik.config.utils.getIntList import pl.szczodrzynski.edziennik.config.utils.set @@ -123,6 +124,19 @@ class ConfigSync(private val config: Config) { private var mRegisterAvailability: Map? = null var registerAvailability: Map - get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson>(it, object: TypeToken>(){}.type) }; return mRegisterAvailability ?: mapOf() } - set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value } + get() { + val flavor = config.values.get("registerAvailabilityFlavor", null as String?) + if (BuildConfig.FLAVOR != flavor) + return mapOf() + + mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> + gson.fromJson(it, object: TypeToken>(){}.type) + } + return mRegisterAvailability ?: mapOf() + } + set(value) { + config.setMap("registerAvailability", value) + config.set("registerAvailabilityFlavor", BuildConfig.FLAVOR) + mRegisterAvailability = value + } } From 4647da780390b4b421b1a7c2530e98b51ae2c5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 12 Apr 2021 14:27:51 +0200 Subject: [PATCH 043/240] [IDE] Fix XML tags reordering when formatting. --- .idea/codeStyles/Project.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 29204aff..7643783a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -15,6 +15,7 @@ xmlns:android + ^$ @@ -25,6 +26,7 @@ xmlns:.* + ^$ @@ -36,6 +38,7 @@ .*:id + http://schemas.android.com/apk/res/android @@ -46,6 +49,7 @@ .*:name + http://schemas.android.com/apk/res/android @@ -56,6 +60,7 @@ name + ^$ @@ -66,6 +71,7 @@ style + ^$ @@ -76,6 +82,7 @@ .* + ^$ @@ -87,6 +94,7 @@ .* + http://schemas.android.com/apk/res/android @@ -98,6 +106,7 @@ .* + .* From ccf0bdaf0554daa8afcc9b9e89a654e743e8eb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 12 Apr 2021 14:34:51 +0200 Subject: [PATCH 044/240] [4.7.1] Update build.gradle, signing and changelog. --- app/src/main/assets/pl-changelog.html | 12 +++--------- app/src/main/cpp/szkolny-signing.cpp | 2 +- .../data/api/szkolny/interceptor/Signing.kt | 2 +- build.gradle | 4 ++-- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 2219ad8c..db18101b 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,13 +1,7 @@ -

    Wersja 4.7, 2021-04-07

    +

    Wersja 4.7.1, 2021-04-12

      -
    • Szkolny.eu jest teraz open source! Zapraszamy na stronę https://szkolny.eu/ po więcej ważnych informacji.
    • -
    • Poprawiono wybieranie obrazków (tła nagłówka, tła aplikacji oraz profilu) z dowolnego źródła.
    • -
    • Ukończono tłumaczenie na język angielski. @MarcinK50
    • -
    • Dodano ekran informacji o kompilacji w Ustawieniach.
    • -
    • Zaktualizowano ekran licencji open source.
    • -
    • Naprawiono zatrzymanie aplikacji na Androidzie 4.4 i starszych.
    • -
    • Naprawiono problemy z połączeniem internetowym na Androidzie 4.4 i starszych.
    • -
    • Zoptymalizowano wielkość aplikacji.
    • +
    • Poprawiono sprawdzanie dostępności e-dziennika.
    • +
    • Zmieniono datę w informacjach o aplikacji. @Luncenok


    diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index f2b1cf8a..e2311acc 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0xda, 0x9f, 0xd4, 0x2b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0xcc, 0x64, 0xdb, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index 819194d8..d07afeb7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDLPrcQX7M===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDXHhAtZBW===.$param2".sha256() } } diff --git a/build.gradle b/build.gradle index cc8c8471..2e05ee75 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.4.31' release = [ - versionName: "4.7", - versionCode: 4070099 + versionName: "4.7.1", + versionCode: 4070199 ] setup = [ From 634ef16bc5cd2c1c1ac362ffdd598f565d46876d Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 12 Apr 2021 17:17:52 +0200 Subject: [PATCH 045/240] [UI] Add notification icons. (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add notification icons * Change color of icons * [Notifications] Update icon colors. * [Notifications] Update lucky number icon. * Add icons to Notifications List Fragment * Update notifications_list_item.xml * Move the gravity to the LinearLayout * add paddingLeft * Change IconicsImageView to View. * Rearrange XML attributes. Co-authored-by: Kuba Szczodrzyński --- .../data/api/task/PostNotifications.kt | 14 ++++++++++ .../edziennik/data/db/entity/Notification.kt | 17 ++++++++++++ .../notifications/NotificationsAdapter.kt | 6 +++++ .../res/layout/notifications_list_item.xml | 26 +++++++++++++++---- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt index 843f2174..28e62d59 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt @@ -8,6 +8,9 @@ import android.util.SparseIntArray import androidx.core.app.NotificationCompat import androidx.core.util.forEach import androidx.core.util.set +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.* import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SERVER_MESSAGE import pl.szczodrzynski.edziennik.utils.models.Time @@ -107,6 +110,10 @@ class PostNotifications(val app: App, nList: List) { .setContentText(buildSummaryText(summaryCounts)) .setTicker(newNotificationsText) .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app).apply { + icon = CommunityMaterial.Icon.cmd_bell_ring_outline + colorRes = R.color.colorPrimary + }.toBitmap()) .setStyle(NotificationCompat.InboxStyle() .also { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -137,6 +144,9 @@ class PostNotifications(val app: App, nList: List) { .setSubText(if (it.type == TYPE_SERVER_MESSAGE) null else it.title) .setTicker("${it.profileName}: ${it.title}") .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app, it.getLargeIcon()).apply { + colorRes = R.color.colorPrimary + }.toBitmap()) .setStyle(NotificationCompat.BigTextStyle() .bigText(it.text)) .setWhen(it.addedDate) @@ -160,6 +170,10 @@ class PostNotifications(val app: App, nList: List) { .setContentText(buildSummaryText(summaryCounts)) .setTicker(newNotificationsText) .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app).apply { + icon = CommunityMaterial.Icon.cmd_bell_ring_outline + colorRes = R.color.colorPrimary + }.toBitmap()) .addDefaults() .setGroupSummary(true) .setContentIntent(summaryIntent) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt index 1e8b8e0e..6a2a787b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt @@ -10,6 +10,8 @@ import android.content.Intent import androidx.room.Entity import androidx.room.PrimaryKey import com.google.gson.JsonObject +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.MainActivity @Entity(tableName = "notifications") @@ -96,4 +98,19 @@ data class Notification( fillIntent(intent) return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT) } + + fun getLargeIcon(): IIcon = when (type) { + TYPE_TIMETABLE_LESSON_CHANGE -> CommunityMaterial.Icon3.cmd_timetable + TYPE_NEW_GRADE -> CommunityMaterial.Icon3.cmd_numeric_5_box_outline + TYPE_NEW_EVENT -> CommunityMaterial.Icon.cmd_calendar_outline + TYPE_NEW_HOMEWORK -> CommunityMaterial.Icon3.cmd_notebook_outline + TYPE_NEW_SHARED_EVENT -> CommunityMaterial.Icon.cmd_calendar_outline + TYPE_NEW_SHARED_HOMEWORK -> CommunityMaterial.Icon3.cmd_notebook_outline + TYPE_NEW_MESSAGE -> CommunityMaterial.Icon.cmd_email_outline + TYPE_NEW_NOTICE -> CommunityMaterial.Icon.cmd_emoticon_outline + TYPE_NEW_ATTENDANCE -> CommunityMaterial.Icon.cmd_calendar_remove_outline + TYPE_LUCKY_NUMBER -> CommunityMaterial.Icon.cmd_emoticon_excited_outline + TYPE_NEW_ANNOUNCEMENT -> CommunityMaterial.Icon.cmd_bullhorn_outline + else -> CommunityMaterial.Icon.cmd_bell_ring_outline + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt index 8df33a8c..6c0fe067 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt @@ -4,6 +4,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.colorRes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -43,6 +45,10 @@ class NotificationsAdapter( val date = Date.fromMillis(item.addedDate).formattedString val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) + b.notificationIcon.background = IconicsDrawable(app, item.getLargeIcon()).apply { + colorRes = R.color.colorPrimary + } + b.title.text = item.text b.profileDate.text = listOf( item.profileName ?: "", diff --git a/app/src/main/res/layout/notifications_list_item.xml b/app/src/main/res/layout/notifications_list_item.xml index 498f7e21..13df88d1 100644 --- a/app/src/main/res/layout/notifications_list_item.xml +++ b/app/src/main/res/layout/notifications_list_item.xml @@ -9,12 +9,28 @@ android:orientation="vertical" android:padding="8dp"> - + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + Date: Mon, 12 Apr 2021 17:18:53 +0200 Subject: [PATCH 046/240] [UI] Add eggfall. (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add eggfall * Add egg.png * Add more eggs * [Gradle] Update android-snowfall to a fork. * [Eggfall] Add randomizing egg images. * [UI] Restore snowfall icon. Add separate eggfall setting. * [Eggfall] Limit eggfall to near-easter date only. Co-authored-by: GitHub Co-authored-by: Kuba Szczodrzyński --- app/build.gradle | 2 +- .../szczodrzynski/edziennik/MainActivity.kt | 20 +++-- .../edziennik/config/ConfigUI.kt | 5 ++ .../settings/cards/SettingsThemeCard.kt | 13 ++++ .../edziennik/utils/BigNightUtil.kt | 69 ++++++++++++++++++ app/src/main/res/drawable/egg1.webp | Bin 0 -> 6320 bytes app/src/main/res/drawable/egg2.webp | Bin 0 -> 5664 bytes app/src/main/res/drawable/egg3.webp | Bin 0 -> 5982 bytes app/src/main/res/drawable/egg4.webp | Bin 0 -> 7238 bytes app/src/main/res/drawable/egg5.webp | Bin 0 -> 7806 bytes app/src/main/res/drawable/egg6.webp | Bin 0 -> 2582 bytes app/src/main/res/layout/eggfall.xml | 14 ++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-en/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 15 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt create mode 100644 app/src/main/res/drawable/egg1.webp create mode 100644 app/src/main/res/drawable/egg2.webp create mode 100644 app/src/main/res/drawable/egg3.webp create mode 100644 app/src/main/res/drawable/egg4.webp create mode 100644 app/src/main/res/drawable/egg5.webp create mode 100644 app/src/main/res/drawable/egg6.webp create mode 100644 app/src/main/res/layout/eggfall.xml diff --git a/app/build.gradle b/app/build.gradle index 72c03e94..838083cc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,6 +152,7 @@ dependencies { implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2" // Szkolny.eu libraries/forks + implementation "eu.szkolny:android-snowfall:1ca9ea2da3" implementation "eu.szkolny:agendacalendarview:1799f8ef47" implementation "eu.szkolny:cafebar:5bf0c618de" implementation "eu.szkolny.fslogin:lib:2.0.0" @@ -180,7 +181,6 @@ dependencies { implementation "com.github.bassaer:chatmessageview:2.0.1" implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" implementation "com.github.ChuckerTeam.Chucker:library:3.0.1" - implementation "com.github.jetradarmobile:android-snowfall:1.2.0" implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31" implementation("com.heinrichreimersoftware:material-intro") { version { strictly "1.5.8" } } implementation "com.hypertrack:hyperlog:0.0.10" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 8dd83bf2..2cd4f7b7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.Observer import androidx.navigation.NavOptions import com.danimahardhika.cafebar.CafeBar import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.jetradarmobile.snowfall.SnowfallView import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.colorInt @@ -81,12 +82,9 @@ import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment -import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.* import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.dpToPx -import pl.szczodrzynski.edziennik.utils.appManagerIntentList import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.NavTarget import pl.szczodrzynski.navlib.* @@ -470,9 +468,21 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // IT'S WINTER MY DUDES val today = Date.getToday() - if ((today.month == 12 || today.month == 1) && app.config.ui.snowfall) { + if ((today.month % 11 == 1) && app.config.ui.snowfall) { b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) } + else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { + val eggfall = layoutInflater.inflate(R.layout.eggfall, b.rootFrame, false) as SnowfallView + eggfall.setSnowflakeBitmaps(listOf( + BitmapFactory.decodeResource(resources, R.drawable.egg1), + BitmapFactory.decodeResource(resources, R.drawable.egg2), + BitmapFactory.decodeResource(resources, R.drawable.egg3), + BitmapFactory.decodeResource(resources, R.drawable.egg4), + BitmapFactory.decodeResource(resources, R.drawable.egg5), + BitmapFactory.decodeResource(resources, R.drawable.egg6) + )) + b.rootFrame.addView(eggfall) + } // WHAT'S NEW DIALOG if (app.config.appVersion < BuildConfig.VERSION_CODE) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt index 7b39383e..f36652a1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt @@ -49,6 +49,11 @@ class ConfigUI(private val config: Config) { get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false } set(value) { config.set("snowfall", value); mSnowfall = value } + private var mEggfall: Boolean? = null + var eggfall: Boolean + get() { mEggfall = mEggfall ?: config.values.get("eggfall", false); return mEggfall ?: false } + set(value) { config.set("eggfall", value); mEggfall = value } + private var mBottomSheetOpened: Boolean? = null var bottomSheetOpened: Boolean get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt index 17b108e5..a009279d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.settings.MiniMenuConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.ThemeChooserDialog import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil +import pl.szczodrzynski.edziennik.utils.BigNightUtil import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.models.Date @@ -36,6 +37,18 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { } else null, + if (BigNightUtil().isDataWielkanocyNearDzisiaj()) // cool klasa for utility to dzień wielkanocy + util.createPropertyItem( + text = R.string.settings_theme_eggfall_text, + subText = R.string.settings_theme_eggfall_subtext, + icon = CommunityMaterial.Icon.cmd_egg_easter, + value = configGlobal.ui.eggfall + ) { _, it -> + configGlobal.ui.eggfall = it + activity.recreate() + } + else null, + util.createActionItem( text = R.string.settings_theme_theme_text, subText = Themes.getThemeNameRes(), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt new file mode 100644 index 00000000..c7f57ccc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.utils + +import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.math.absoluteValue + +// Obliczanie daty wielkanocy - algorytm Gaussa +// www.algorytm.org +// (c) 2008 by Tomasz Lubinski +// http://www.algorytm.org/przetwarzanie-dat/wyznaczanie-daty-wielkanocy-algortym-gaussa/dwg-j.html + +class BigNightUtil { + + /* Pobierz wartosc A z tabeli lat */ + private fun getA(rok: Int) = when { + rok <= 1582 -> 15 + rok <= 1699 -> 22 + rok <= 1899 -> 23 + rok <= 2199 -> 24 + rok <= 2299 -> 25 + rok <= 2399 -> 26 + rok <= 2499 -> 25 + else -> 0 + } + + /* Pobierz wartosc B z tabeli lat */ + private fun getB(rok: Int) = when { + rok <= 1582 -> 6 + rok <= 1699 -> 2 + rok <= 1799 -> 3 + rok <= 1899 -> 4 + rok <= 2099 -> 5 + rok <= 2199 -> 6 + rok <= 2299 -> 0 + rok <= 2499 -> 1 + else -> 0 + } + + /* oblicz ile dni po 22 marca przypada wielkanoc */ + private fun Oblicz_Date_wielkanocy(rok: Int): Int { + val a = rok % 19 + val b = rok % 4 + val c = rok % 7 + var d = (a * 19 + getA(rok)) % 30 + val e = (2 * b + 4 * c + 6 * d + getB(rok)) % 7 + if (d == 29 && e == 6 || d == 28 && e == 6) { + d -= 7 + } + return d + e + } + + private fun get_dataOf_bigNight(): Date { + val date = Date.getToday() + date.month = 4 + date.day = 22 + Oblicz_Date_wielkanocy(date.year) + if (date.day > 31) + date.day = date.day % 31 + else + date.month = 3 + + return date + } + + fun isDataWielkanocyNearDzisiaj() = + Date.diffDays(Date.getToday(), get_dataOf_bigNight()).absoluteValue < 7 +} diff --git a/app/src/main/res/drawable/egg1.webp b/app/src/main/res/drawable/egg1.webp new file mode 100644 index 0000000000000000000000000000000000000000..94440758e1a18ebcfeb98c0631e04a70e5517aec GIT binary patch literal 6320 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 L;7|ANYz77ZRJU3m literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/egg2.webp b/app/src/main/res/drawable/egg2.webp new file mode 100644 index 0000000000000000000000000000000000000000..bfc8644a22e321241746c546e053c732924741d8 GIT binary patch literal 5664 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 E0D)X4Z~y=R literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/egg3.webp b/app/src/main/res/drawable/egg3.webp new file mode 100644 index 0000000000000000000000000000000000000000..886b5ecedd6453b23615f0ac5dfba90f0adb7389 GIT binary patch literal 5982 zcmWIYbaM+6XJ80-bqWXzu!!JdU|!n;>1k-*;Z?ccWAHS`WREIql(I?kSX)vghq<&z`6GMz6MOx7@IrHnG37 zv!Fn4@%HAv{lznv6h2>`(2n?_3gqkYvFx|Z(Q~HwJZ5l?3p9_Mx`2)?N40# zR#|oJ@P$(+&pf!2Rkmv4nMVa@d+MjmtT}S#=9y=jVcX7qPK=gVX_x9|FD+j?^QxWM zx0JNr&Z;TWt1EX*zq$9YlmD~rcaJ($oSzWo_&y@-r^U{yN$RUt#-8?xSKF4;rycF~ zIxcPZ^ed|>_vMH#-97W;?Ure03e&t_#GG9duNdt2DNS{1^30!$UGBKQi|RXe?M$6` zZ`{9k6Fn~f@s)1<8+o8sWS+-|84kAUjaPqsOBek9Sb9n#N35al;grU;+T1%uZ?kHi zORUyA%AHae#-&}5doC&ONNG&p;~l@Ayl{Pb|KvxpP($_OYmRTx;@%~7na^`uvXxe6 z;x4t5qCU)%B$lO5vN`(2Pw23TQfH#n^c0!POH|rUY*o$NeZS`OBnc(i=_xXGpSP-H zCRXX5)b`<5sqA>UOr`B*k4n11+o>t~%f3#MSZ1Q%uIgi@lHHU1-TQF zW%cYekG33N>Kw$w^oco0Vw>w{qZ>*&zkJ%n8~$zJY5y2%EOh3|kC^o=<&6*2IL(4C zi2vEkGRgYKZCB^}h3O6@=H=EacTC?SIpOY&XKQjxzKEr7Z!EAqHC=bXlHEUVu`Y?# z58vxxv$f(^#+8z{ChnKB8>%|0Z~xiyeYu0yjLHj_|IA_jsww7D<0H4PdhYDmA7bYx z9T4|23TF=SNczl4*IX(8$v6`i__kT^jS0P@q=Ns=%DXEx0Ew{RtUPyR5Yvsbg z@}g;z<`!MZxZ1&gcE!3HzvoA#GAw?mdF9@bm09}nYeW07VBw9HS3ms`vD+QMbjZV} z^yE{|nBeJROsCw0_^$qXVjyQ7R(W9NtOCZ)Rj*&oJ!Vk4{WRmX2eU44^{zS=Tkbx4 zLy6XM3+27{x|N7zT@oOJCUafX7 zKllFr9)SzjEP6XM=Pa z(mbB+++?H?z^5r?FQ&^-E56p`eCyO{i13v#m$?O`2XRrzW=%Z@t$9t z=Rfy%tKZ1~4F9_RtNa6drGK&YNB_+G&--rvtMfnZ&z8SycX0oNdZYi+f6M>$|Be4p z|NsA+zYl)P|GoB2@UQOgkG>7po;>4Hd0e^hS=zUKF&Vc74OVzL|2Zn=yZQ8O?RB4o z58Ps?yYVN4@xL~!$Xr`)&95=v>OX0AtvbjW(&Sw8_2^MiBp+1Ghq;@l{v z+Q0YJ-9>?S%#~*R{piwbd-5d1(x>buKX{c5Me0&_zIh?eEijG$XH=>5xqDNb@2_54 zqdZIaR-b#^H_hj*JP8YYI+Pi1oas?@c)9d!_-1~VP5cg@cLnc>)4s|pxm(9^Nr`&- z{eG?x)E1zrM{IP%ePW#{IEQQ;NRMjY;!f*lwV6-2T+0^OrcUol)t2UM%SIqW9lL^$0)dR=-zI ze-wOw+?dQ@H~-Qf)@!R$MI_&}yKeESh_!#ORh=xxF7WTr>F||LcE9xe{qXZi>p!Jj zYL;pL-RFc{->;=+#lZ0IpL=VcZ6{e!Yq1ady)0?NB9329$c5ocKU!y{A3Afq%aH|IAtIwMW{ia$3m=hq9HvE=G{Zhg@^>ttlE7o7I-}Kd8PdOt|ljX4&o|x#pjo1ghNM zG8Ri$$$q<@SUX{v;N%zI!xR6ORVoyRdPK}+d)QXFNa*N||Eu;+I&tXyly7^(|HZWb z-@n-Lv2L&2B(7zrqe{Q+@hDo}9rGbwh3TQHm;GUnWzTjxyfhNA-gKyG{kx=>S#SPy zO|w0^;8oX(=Q&MFpz!Hww^BlS9c~|dHMy&Qx&QLx*%_!`Bb#j6E>AOsO*`xkU^#AUAL}u^3i(2nHjxLD2x@u*x+J+rJy!$O!ljr=t z)F`7H^1a_rO)~7#W%(OklV4wXESJ9Wm-n4rDleiReQ+p{<#{H+^o(2ka*%C#(T}&) zs?&N8JhI-Ov~H8w5Akagx9Us0Ocm4?;j!O&%fafnfEa_D+}ps zi9bFO6COLv=Iz@TQr@?fnJMyW=$+Qb1|L=^F|xKatNnFuSHJ)Atj@HaTkHGxd^WvN z?ey5CQF*ann-&Xap!dhJ{Z}IDrq4O&w=`8$Nc?5WwBu(YEP0dsMIIdc+uEU8AaZwg znaR_oGR9LD^JQ$a7wuZ2@cOiI%h#-3rtI(YBpxMfai1}_^f=2&^Va1|FUr$}N?IyI z4<4C3KV0p9ae3mpqr2SrE!W0|2D)7^5o zjC;Jko6XHM?(da^EAMdrD{Ueth26{w=1Z3b9F4(8po(mmv#Qv z@wVfl=AD7>-d!*(k9A$kuJqy6$?D48w>$P8y4&rdeDCq`FDDYM_9>inZ8vhO%n#(y zC~$wx&gqffJi}ps>g2i8GnB3cAAWLv4)fksQ3f9#@N!gq_t)XOWip{zEBOA|Id@;( zxj6Iq;m^`tzSH*^IK;A;9kgG6tmbig`q#?W8+3IUi!^<{9+y;M@VWeP&u#Ixt`{4M zlhW$NmbDv7YMy_YrVyhgS#|8lvErXT%Ky#^Ze4QVd;0Q6mr~5`Sp8;fTw-(U!v8{k z;a{_2pLFfiRICq-TEb+PS{(Qzk#~9dh?#~?+KIaE)@!YdWw=He4s^{7wagCD9 zFA37!?F&2aFm!LID!G$Vu2}Xs@tT>-ZCB>4jv6*EvzkI@ZxP;Kz2kd+bl0=8IvLrBXlPv+~#M5`O{;Ix9}X@ zE~aF7xasu87_o?N*7u%n+?p}NZf~BX>-=+NE0%b)y^Y>?Ty_hmvdq@KYTy3lPMr2_ zPiLZTwU@Sdg7*xoYefo2or~qxZ`&oic2QUR^s76pb(b!g6rjWKf6e`WhBbQC&56&d zA8vOQ6}0r)@t$A9`B#hKhTzZr#WUX+ex16{G=E8Cz3qpFYMH6QzgfjjzrUPseZ{9x zGxci5Id#*Hz55MK8x=)acrW@$?3$SGBA)9~@j+?ofyZHs>TYO>N^>*6v)e86xhnmD zVyU7`G50S)*1zi{{)YZ-y8lR}Qa? z{1e@>Z_%#p*1O!(RrjvgYkYhAe0zD}W=_@r7w)pmon|ZlXRTAr>G0GOLN8WZip9&w z7p^%ME4GI5P2Y_v1_2x;K+B+I=PvmE%E4){( z-^_c&ZGW2I*Y|CKxsmVY-dnxQeWuSVqYiHk#*QTtTP8B4Ok5jMZOS3y_qpu+=U1YO zT@scYofj|YnyU58>aBqZfRE zer~#}65sN!(<^z$ywt5GyzIiW-P~21gcdzgSJ-)K3fHA8)>a9EJ&(l_qzoc-x5Pe+ z-?-JY@$r7~)}o~=ygu1_WSOX`={2k{C5-&d}cX$#k)50^>h^~9d~yuep>Q+ zTViLNNm!VBKd0=En2Q`~`(t0v?6|D)X~(02x=RO)T~{Sf+ov|I=g)DKo7y`!Desqk ze0&N|?%ZFzoeocBs?*()5)}2;&9z&+OEuEgblrMIo>}j+&%U`D<+J$C`(u1!EB*Kk z|HX#8KIlvQBHq_EUv_aqbK;g`%P$9)&*GmboTrIWc!+88{=#$+9%=Jadh=om}%UN0f!IYmOD*YvK-f<2)Qkr9*GjVTQ z(RM;Q;$Ef!s}1vu+w$!j)o(|?4JwLXl~-@T`LpkXK($AT0^joccON)#;4Gs9XUWmlV#mAn(yo^euGpxz^;FqU z!`>U^_Dq3kUu>4TY9@s=pH+41yI__oaA}Twk^ct8S#11Pf0kYJ^xwbs=Om-@Rt+h= zo%24OzMB-FDzxbu^UH(<)kfPiIKIp{6Kr_pRd_m6;g*Z4>vE-zD*j~NQh2@MY-R47 zDc5d)T@@_1t`9pA1>>j=~HpT)3y52|JO;rww!0}miw%d z{2FE}=5HMkUF&mlA?x>=fb;tV!XLRUXz_?YZ|mQA;#Z|vLVii&=lTcDwUc%^J`+Bx zT9vIT#%Xir+AY?Cvwl&_dLB(~e&C^YTk-9KmCgBI4~Wf~eb6Yfzh12Ry^fLiktZo1 zXTRd>Zqhk>pl3rDTf0tZ*gx*2|Kfhl+_3m(@zcNOTuuewVK*sXA3gC3U-ZA5^MBvF z|B%u3xW15Iaw~iNL0`9nXEGM7m8?_@ul;Hkem$mQ{^$Kat{zbN@?LnawZ%@M&}B)# zFMHIBZQHu1^Pjrh&8zYHeyq=~e3}=inq|=<^QEr9tn2jSn!1_mB3(mQcjYkm-VM~1 zRhFMy(0R1q%JT8rpI_EKiSdhlU|2BI=xWo6e-Gj(h>Ks5k4$bllJjYy(;=h2-RWl& z9{gS2QQ0rozgpeKd|&9g+xy!Zc0B$grBv+}wb0bDZO@*+HOZUb%WvWeWGDz}J$2e< zJLl~Zbt%rwjn#tQuXOi$t9LD0`-m-D#(ut9>L1TX;lIT*I(KY*lh+{ot=cB`x<969`^<&*S(h)k+XoWNgwbKCSs54LXq)^fJg;m(Sx%Smc0ALPz2?)|vw`sPUm zw!z7Xmpz_OEY&Dp`TP9s?p{WoRE?Y~_sySY39{^(`8>bI&TI0er7oI|Ud%r0=BNHm ztM)fN?4!Hxt-MS@UA)D8z3Wq>*KT>g@9t&!M`sq@tWxse)V(h6!`inqW#Wp|NU@el zKb4d{9-H2ubW&#TGoy-Kl@VurqQ7Xrf+A-=R3_*xKfc7Is|FHG`$c=OfUm*=ZKc&>Mj?9FV~(zUYjKh|D61zes6~{}qe~>cZN_xDhd+VH8$@4trrmRx3z0SMoyWoOn9@XoY z{=Y1;gI#l9%0rwx+YB|E`%|&)bOlE~(GSmOpZ+_CT@g^KX{2j-p%-yB>s`YUJi? z(kR$?wBW}D=||U&E@0`3EMVEI6D<6&;d5sFy#FUQU$#+;@#s73_1$3l$IVGzy{>|7 zZ#LckBssTe%IA~96SNpUHM1+sF;Z$f>R7M9#2<94|DE5V0KU14TJ2t&Ob)Nu+v^k( zdGSufgB-pS{aG$kJ&V*c-+VaudXqe3__rNJVGn#fFFr3l@&8xm^CR)*ZR}#-+Rkcm zcgQ$}Us8MQdH9Z&e$i8fO!vIY_dK605@(j=T)+OI_TIFeg1sM4N6Gdm^55Cyte9)x z#HPY<^a|e&e%|;LrOsyd1q#}mx6ZscD=u}B5{F={$Lxs39G`z2f4#IP>Q;-d1hWW7 R%Kl%132f%`I2#xk7yzWIuy6nX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/egg4.webp b/app/src/main/res/drawable/egg4.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b958948e7733a93b6d09a8a386a133d793dcc90 GIT binary patch literal 7238 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 CdO3*z literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/egg5.webp b/app/src/main/res/drawable/egg5.webp new file mode 100644 index 0000000000000000000000000000000000000000..2cc48842a490c11475611d340cc6beb93c509eb3 GIT binary patch literal 7806 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 JzZNhs000aaJ}>|P literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/egg6.webp b/app/src/main/res/drawable/egg6.webp new file mode 100644 index 0000000000000000000000000000000000000000..9dd98d89636e0cb691e206e5e98f2f6210100da1 GIT binary patch literal 2582 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 XipV$X=D%xnF^@g2{4p;xfB^~s35*Yh literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/eggfall.xml b/app/src/main/res/layout/eggfall.xml new file mode 100644 index 00000000..fbc6a0fc --- /dev/null +++ b/app/src/main/res/layout/eggfall.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3dd3dcfc..3af14e8f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -967,6 +967,8 @@ Öffnen Sie das Menü mit der Zurück-Taste Schlittenfahrt im Schnee Jingle Bells, Jingle Bells + Brrrr + Bajo jajo, bajo jajo Rosa System Thema diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 86911c58..4c0c5b89 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -969,6 +969,8 @@ Back button opens drawer Jingle all the way Jingle bells, Jingle bells + Brrrr + Bajo jajo, bajo jajo Pink System Theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b0ee64a..a008fc94 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1034,6 +1034,8 @@ Otwieraj menu przyciskiem wstecz Dzwonią dzwonki sań Pada śnieg, pada śnieg + Brrrr + Bajo jajo, bajo jajo Różowy Systemowy Motyw From 5eaa754401277f956b52a9cc3b555c48b943ee03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Tue, 13 Apr 2021 20:50:51 +0200 Subject: [PATCH 047/240] [UI] Add icons for done and manual events. --- .../agenda/event/AgendaEventRenderer.kt | 81 ++++++++++++------- .../LessonChangesEventRenderer.kt | 5 +- .../TeacherAbsenceEventRenderer.kt | 5 +- .../res/layout/agenda_event_compact_item.xml | 2 +- app/src/main/res/layout/agenda_event_item.xml | 2 +- 5 files changed, 60 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt index dafdb853..ac65fcef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt @@ -6,8 +6,15 @@ package pl.szczodrzynski.edziennik.ui.modules.agenda.event import android.annotation.SuppressLint import android.view.View +import android.widget.FrameLayout +import android.widget.TextView import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.view.IconicsTextView import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding @@ -22,14 +29,33 @@ class AgendaEventRenderer( @SuppressLint("SetTextI18n") override fun render(view: View, aEvent: AgendaEvent) { + if (isCompact) { + val b = AgendaWrappedEventCompactBinding.bind(view).item + bindView(aEvent, b.card, b.title, null, b.badgeBackground, b.badge) + } else { + val b = AgendaWrappedEventBinding.bind(view).item + bindView(aEvent, b.card, b.title, b.subtitle, b.badgeBackground, b.badge) + } + } + + private fun bindView( + aEvent: AgendaEvent, + card: FrameLayout, + title: IconicsTextView, + subtitle: TextView?, + badgeBackground: View, + badge: View + ) { val event = aEvent.event + val textColor = Colors.legibleTextColor(event.eventColor) + val timeText = if (event.time == null) - view.context.getString(R.string.agenda_event_all_day) + card.context.getString(R.string.agenda_event_all_day) else event.time!!.stringHM - val eventTitle = "${event.typeName ?: "wydarzenie"} - ${event.topic}" + var eventTitle = "${event.typeName ?: "wydarzenie"} - ${event.topic}" val eventSubtitle = listOfNotNull( timeText, @@ -38,36 +64,33 @@ class AgendaEventRenderer( event.teamName ).join(", ") - if (isCompact) { - val b = AgendaWrappedEventCompactBinding.bind(view).item - - b.card.foreground.setTintColor(event.eventColor) - b.card.background.setTintColor(event.eventColor) - b.title.text = eventTitle - b.title.setTextColor(Colors.legibleTextColor(event.eventColor)) - - b.badgeBackground.isVisible = aEvent.showItemBadge - b.badgeBackground.background.setTintColor( - android.R.attr.colorBackground.resolveAttr(view.context) - ) - b.badge.isVisible = aEvent.showItemBadge + if (event.addedManually) { + eventTitle = "{cmd-clipboard-edit-outline} $eventTitle" } - else { - val b = AgendaWrappedEventBinding.bind(view).item - b.card.foreground.setTintColor(event.eventColor) - b.card.background.setTintColor(event.eventColor) - b.title.text = eventTitle - b.title.setTextColor(Colors.legibleTextColor(event.eventColor)) - b.subtitle.text = eventSubtitle - b.subtitle.setTextColor(Colors.legibleTextColor(event.eventColor)) + card.foreground.setTintColor(event.eventColor) + card.background.setTintColor(event.eventColor) + title.text = eventTitle + title.setTextColor(textColor) + subtitle?.text = eventSubtitle + subtitle?.setTextColor(textColor) - b.badgeBackground.isVisible = aEvent.showItemBadge - b.badgeBackground.background.setTintColor( - android.R.attr.colorBackground.resolveAttr(view.context) - ) - b.badge.isVisible = aEvent.showItemBadge - } + title.setCompoundDrawables( + null, + null, + if (event.isDone) IconicsDrawable(card.context).apply { + icon = CommunityMaterial.Icon.cmd_check + colorInt = textColor + sizeDp = 24 + } else null, + null + ) + + badgeBackground.isVisible = aEvent.showItemBadge + badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(card.context) + ) + badge.isVisible = aEvent.showItemBadge } override fun getEventLayout() = if (isCompact) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt index 8b8ff81b..2a904016 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt @@ -17,13 +17,14 @@ class LessonChangesEventRenderer : EventRenderer() { override fun render(view: View, event: LessonChangesEvent) { val b = AgendaWrappedCounterBinding.bind(view).item + val textColor = Colors.legibleTextColor(event.color) b.card.foreground.setTintColor(event.color) b.card.background.setTintColor(event.color) b.name.setText(R.string.agenda_lesson_changes) - b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.name.setTextColor(textColor) b.count.text = event.count.toString() - b.count.setTextColor(b.name.currentTextColor) + b.count.setTextColor(textColor) b.badgeBackground.isVisible = event.showItemBadge b.badgeBackground.background.setTintColor( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt index ec0a3915..12dd1947 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt @@ -16,13 +16,14 @@ class TeacherAbsenceEventRenderer : EventRenderer() { override fun render(view: View, event: TeacherAbsenceEvent) { val b = AgendaWrappedCounterBinding.bind(view).item + val textColor = Colors.legibleTextColor(event.color) b.card.foreground.setTintColor(event.color) b.card.background.setTintColor(event.color) b.name.setText(R.string.agenda_teacher_absence) - b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.name.setTextColor(textColor) b.count.text = event.count.toString() - b.count.setTextColor(b.name.currentTextColor) + b.count.setTextColor(textColor) b.badgeBackground.isVisible = false b.badge.isVisible = false diff --git a/app/src/main/res/layout/agenda_event_compact_item.xml b/app/src/main/res/layout/agenda_event_compact_item.xml index df48e70c..b97ddfa3 100644 --- a/app/src/main/res/layout/agenda_event_compact_item.xml +++ b/app/src/main/res/layout/agenda_event_compact_item.xml @@ -25,7 +25,7 @@ android:orientation="vertical" android:padding="10dp"> - - Date: Wed, 14 Apr 2021 10:16:22 +0200 Subject: [PATCH 048/240] [UI] Fix updating event dialog when editing or removing. --- .../ui/dialogs/event/EventDetailsDialog.kt | 14 +++++++++++--- .../ui/dialogs/event/EventManualDialog.kt | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index 0958188e..664cc078 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -32,7 +32,7 @@ import kotlin.coroutines.CoroutineContext class EventDetailsDialog( val activity: AppCompatActivity, - val event: EventFull, + var event: EventFull, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -139,6 +139,7 @@ class EventDetailsDialog( launch(Dispatchers.Default) { app.db.eventDao().replace(event) } + update() b.checkDoneButton.isChecked = true } .setNegativeButton(R.string.cancel, null) @@ -149,6 +150,7 @@ class EventDetailsDialog( launch(Dispatchers.Default) { app.db.eventDao().replace(event) } + update() } } b.checkDoneButton.attachToastHint(R.string.hint_mark_as_done) @@ -160,6 +162,14 @@ class EventDetailsDialog( activity, event.profileId, editingEvent = event, + onSaveListener = { + if (it == null) { + dialog.dismiss() + return@EventManualDialog + } + event = it + update() + }, onShowListener = onShowListener, onDismissListener = onDismissListener ) @@ -327,8 +337,6 @@ class EventDetailsDialog( removeEventDialog?.dismiss() dialog.dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == MainActivity.DRAWER_ITEM_AGENDA) - activity.reloadTarget() } private fun openInCalendar() { launch { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index 9beac76b..a40c5772 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -48,6 +48,7 @@ class EventManualDialog( val defaultTime: Time? = null, val defaultType: Long? = null, val editingEvent: EventFull? = null, + val onSaveListener: ((event: EventFull?) -> Unit)? = null, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -596,6 +597,7 @@ class EventManualDialog( } } + onSaveListener?.invoke(eventObject.withMetadata(metadataObject)) dialog.dismiss() Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show() } @@ -608,6 +610,7 @@ class EventManualDialog( } removeEventDialog?.dismiss() + onSaveListener?.invoke(null) dialog.dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() } From db598af28aa73cba9f6cf02ddd0c9b1bfcab9cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 10:20:00 +0200 Subject: [PATCH 049/240] [UI] Add legend in event details dialog. --- .../ui/dialogs/event/EventDetailsDialog.kt | 6 ++++++ app/src/main/res/layout/dialog_event_details.xml | 14 ++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 22 insertions(+) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index 664cc078..4feb1d53 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -104,6 +104,12 @@ class EventDetailsDialog( } catch (_: Exception) {} + b.legend.text = listOfNotNull( + if (event.addedManually) R.string.legend_event_added_manually else null, + if (event.isDone) R.string.legend_event_is_done else null + ).map { activity.getString(it) }.join("\n") + b.legend.isVisible = b.legend.text.isNotBlank() + b.typeColor.background?.setTintColor(event.eventColor) b.details = mutableListOf( diff --git a/app/src/main/res/layout/dialog_event_details.xml b/app/src/main/res/layout/dialog_event_details.xml index 7a77a813..a2fe00f5 100644 --- a/app/src/main/res/layout/dialog_event_details.xml +++ b/app/src/main/res/layout/dialog_event_details.xml @@ -105,9 +105,23 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6f3021e4..303100f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1439,4 +1439,6 @@ Ustaw wydarzenia jako lekcje on-line Wybierz rodzaj wydarzeń Grupuj lekcje on-line na liście + {cmd-clipboard-edit-outline} wydarzenie dodane ręcznie + {cmd-check} oznaczono jako wykonane From 297867cbf3ed09ad73f30f1f1d28d910de1f49b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 11:21:45 +0200 Subject: [PATCH 050/240] [UI] Add event type colors to type dropdown. --- .../ui/dialogs/event/EventManualDialog.kt | 69 +++++------ .../ui/modules/views/EventTypeDropdown.kt | 111 ++++++++++++++++++ .../ui/modules/views/TeacherDropdown.kt | 6 + .../edziennik/utils/TextInputDropDown.kt | 42 ++++++- .../res/layout/dialog_event_manual_v2.xml | 2 +- 5 files changed, 181 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index a40c5772..0cd2affe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -4,8 +4,6 @@ package pl.szczodrzynski.edziennik.ui.dialogs.event -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -26,7 +24,6 @@ import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.full.EventFull @@ -35,7 +32,6 @@ import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS import pl.szczodrzynski.edziennik.utils.Anim -import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.coroutines.CoroutineContext @@ -323,57 +319,41 @@ class EventManualDialog( selectDefault(defaultLesson?.displayTeacherId) } + with (b.typeDropdown) { + db = app.db + profileId = this@EventManualDialog.profileId + loadItems() + selectDefault(editingEvent?.type) + selectDefault(defaultType) - val deferred = async(Dispatchers.Default) { - // get the event type list - var eventTypes = app.db.eventTypeDao().getAllNow(profileId) - - if (eventTypes.none { it.id in -1L..10L }) { - eventTypes = app.db.eventTypeDao().addDefaultTypes(activity, profileId) + onTypeSelected = { + b.typeColor.background.setTintColor(it.color) + customColor = null } - - b.typeDropdown.clear() - b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) } - } - deferred.await() - - b.typeDropdown.isEnabled = true - - defaultType?.let { - b.typeDropdown.select(it) } - b.typeDropdown.selected?.let { item -> - customColor = (item.tag as EventType).color - } - - // copy IDs from event being edited + // copy data from event being edited editingEvent?.let { b.topic.setText(it.topic) - b.typeDropdown.select(it.type)?.let { item -> - customColor = (item.tag as EventType).color - } - if (it.color != null && it.color != -1) + if (it.color != -1) customColor = it.color } + b.typeColor.background.setTintColor( + customColor + ?: b.typeDropdown.getSelected()?.color + ?: Event.COLOR_DEFAULT + ) + // copy IDs from the LessonFull defaultLesson?.let { b.teamDropdown.select(it.displayTeamId) } - b.typeDropdown.setOnChangeListener { - b.typeColor.background.colorFilter = PorterDuffColorFilter((it.tag as EventType).color, PorterDuff.Mode.SRC_ATOP) - customColor = null - return@setOnChangeListener true - } - - (customColor ?: Event.COLOR_DEFAULT).let { - b.typeColor.background.colorFilter = PorterDuffColorFilter(it, PorterDuff.Mode.SRC_ATOP) - } - b.typeColor.onClick { - val currentColor = (b.typeDropdown.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT + val currentColor = customColor + ?: b.typeDropdown.getSelected()?.color + ?: Event.COLOR_DEFAULT val colorPickerDialog = ColorPickerDialog.newBuilder() .setColor(currentColor) .create() @@ -381,7 +361,7 @@ class EventManualDialog( object : ColorPickerDialogListener { override fun onDialogDismissed(dialogId: Int) {} override fun onColorSelected(dialogId: Int, color: Int) { - b.typeColor.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + b.typeColor.background.setTintColor(color) customColor = color } }) @@ -597,7 +577,12 @@ class EventManualDialog( } } - onSaveListener?.invoke(eventObject.withMetadata(metadataObject)) + onSaveListener?.invoke(eventObject.withMetadata(metadataObject).also { + it.subjectLongName = b.subjectDropdown.selected?.text?.toString() + it.teacherName = b.teacherDropdown.selected?.text?.toString() + it.teamName = b.teamDropdown.selected?.text?.toString() + it.typeName = b.typeDropdown.selected?.text?.toString() + }) dialog.dismiss() Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt new file mode 100644 index 00000000..5527c50e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.views + +import android.content.Context +import android.content.ContextWrapper +import android.util.AttributeSet +import androidx.appcompat.app.AppCompatActivity +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.utils.TextInputDropDown + +class EventTypeDropdown : TextInputDropDown { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + private val activity: AppCompatActivity? + get() { + var context: Context? = context ?: return null + if (context is AppCompatActivity) return context + while (context is ContextWrapper) { + if (context is AppCompatActivity) + return context + context = context.baseContext + } + return null + } + + lateinit var db: AppDb + var profileId: Int = 0 + var onTypeSelected: ((eventType: EventType) -> Unit)? = null + + override fun create(context: Context) { + super.create(context) + isEnabled = false + } + + suspend fun loadItems() { + val types = withContext(Dispatchers.Default) { + val list = mutableListOf() + + var types = db.eventTypeDao().getAllNow(profileId) + + if (types.none { it.id in -1L..10L }) { + types = db.eventTypeDao().addDefaultTypes(context, profileId) + } + + list += types.map { + Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply { + icon = CommunityMaterial.Icon.cmd_circle + sizeDp = 24 + colorInt = it.color + }) + } + + list + } + + clear().append(types) + isEnabled = true + + setOnChangeListener { + when (it.tag) { + is EventType -> { + // selected an event type + onTypeSelected?.invoke(it.tag) + true + } + else -> false + } + } + } + + /** + * Select an event type by the [typeId]. + */ + fun selectType(typeId: Long) { + select(typeId) + } + + /** + * Select an event type by the [typeId] **if it's not selected yet**. + */ + fun selectDefault(typeId: Long?) { + if (typeId == null || selected != null) + return + selectType(typeId) + } + + /** + * Get the currently selected event type. + * ### Returns: + * - null if no valid type is selected + * - [EventType] - the selected event type + */ + fun getSelected(): EventType? { + return when (selected?.tag) { + is EventType -> selected?.tag as EventType + else -> null + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt index 6411426b..684412f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt @@ -84,6 +84,9 @@ class TeacherDropdown : TextInputDropDown { } } + /** + * Select a teacher by the [teacherId]. + */ fun selectTeacher(teacherId: Long) { if (select(teacherId) == null) select(Item( @@ -93,6 +96,9 @@ class TeacherDropdown : TextInputDropDown { )) } + /** + * Select a teacher by the [teacherId] **if it's not selected yet**. + */ fun selectDefault(teacherId: Long?) { if (teacherId == null || selected != null) return diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt index f8836374..19aa27d6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt @@ -1,7 +1,11 @@ package pl.szczodrzynski.edziennik.utils +import android.annotation.SuppressLint import android.content.Context +import android.graphics.drawable.Drawable import android.util.AttributeSet +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuPopupHelper import androidx.appcompat.widget.PopupMenu import com.google.android.material.textfield.TextInputEditText import com.mikepenz.iconics.IconicsDrawable @@ -33,6 +37,7 @@ open class TextInputDropDown : TextInputEditText { setText(selected?.displayText ?: selected?.text) } + @SuppressLint("RestrictedApi") open fun create(context: Context) { val drawable = IconicsDrawable(context, CommunityMaterial.Icon.cmd_chevron_down).apply { colorInt = Themes.getPrimaryTextColor(context) @@ -58,7 +63,9 @@ open class TextInputDropDown : TextInputEditText { val popup = PopupMenu(context, this) items.forEachIndexed { index, item -> - popup.menu.add(0, item.id.toInt(), index, item.text) + popup.menu.add(0, item.id.toInt(), index, item.text).also { + it.icon = item.icon + } } popup.setOnMenuItemClickListener { menuItem -> @@ -70,29 +77,46 @@ open class TextInputDropDown : TextInputEditText { true } - popup.setOnDismissListener { + val helper = MenuPopupHelper(context, popup.menu as MenuBuilder, this) + helper.setForceShowIcon(true) + helper.setOnDismissListener { clearFocus() } - - popup.show() + helper.show() } } - fun select(item: Item): Item? { + /** + * Select an arbitrary [item]. Allows to select an item not present + * in the original list. + */ + fun select(item: Item): Item { selected = item updateText() error = null return item } + /** + * Select an item by its ID. Returns the selected item + * if found. + */ fun select(id: Long?): Item? { return items.singleOrNull { it.id == id }?.let { select(it) } } + /** + * Select an item by its tag. Returns the selected item + * if found. + */ fun select(tag: Any?): Item? { return items.singleOrNull { it.tag == tag }?.let { select(it) } } + /** + * Select an item by its index. Returns the selected item + * if the index exists. + */ fun select(index: Int): Item? { return items.getOrNull(index)?.let { select(it) } } @@ -143,5 +167,11 @@ open class TextInputDropDown : TextInputEditText { } } - class Item(val id: Long, val text: CharSequence, val displayText: CharSequence? = null, val tag: Any? = null) + class Item( + val id: Long, + val text: CharSequence, + val displayText: CharSequence? = null, + val tag: Any? = null, + val icon: Drawable? = null + ) } diff --git a/app/src/main/res/layout/dialog_event_manual_v2.xml b/app/src/main/res/layout/dialog_event_manual_v2.xml index 8d2fc267..228fcc74 100644 --- a/app/src/main/res/layout/dialog_event_manual_v2.xml +++ b/app/src/main/res/layout/dialog_event_manual_v2.xml @@ -94,7 +94,7 @@ android:layout_weight="1" android:hint="@string/dialog_event_manual_type"> - Date: Wed, 14 Apr 2021 11:59:58 +0200 Subject: [PATCH 051/240] [UI] Refactor dropdown inputs code. --- .../ui/dialogs/event/EventManualDialog.kt | 32 +++++------ .../ui/modules/views/DateDropdown.kt | 2 +- .../ui/modules/views/EventTypeDropdown.kt | 18 +----- .../ui/modules/views/SubjectDropdown.kt | 41 +++++++------ .../ui/modules/views/TeacherDropdown.kt | 49 ++++++---------- .../ui/modules/views/TeamDropdown.kt | 57 +++++++++---------- .../ui/modules/views/TimeDropdown.kt | 2 +- .../edziennik/utils/TextInputDropDown.kt | 2 +- 8 files changed, 87 insertions(+), 116 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt index 0cd2affe..da684520 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt @@ -23,9 +23,7 @@ import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding @@ -396,11 +394,11 @@ class EventManualDialog( private fun saveEvent() { val date = b.dateDropdown.getSelected() as? Date val timeSelected = b.timeDropdown.getSelected() - val teamId = b.teamDropdown.getSelected() as? Long - val type = b.typeDropdown.selected?.id + val team = b.teamDropdown.getSelected() + val type = b.typeDropdown.getSelected() val topic = b.topic.text?.toString() - val subjectId = b.subjectDropdown.getSelected() as? Long - val teacherId = b.teacherDropdown.getSelected() + val subject = b.subjectDropdown.getSelected() as? Subject + val teacher = b.teacherDropdown.getSelected() val share = b.shareSwitch.isChecked @@ -431,7 +429,7 @@ class EventManualDialog( isError = true } - if (share && teamId == null) { + if (share && team == null) { b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose) if (!isError) b.teamDropdown.parent.requestChildFocus(b.teamDropdown, b.teamDropdown) isError = true @@ -467,10 +465,10 @@ class EventManualDialog( time = startTime, topic = topic, color = customColor, - type = type ?: Event.TYPE_DEFAULT, - teacherId = teacherId ?: -1, - subjectId = subjectId ?: -1, - teamId = teamId ?: -1, + type = type?.id ?: Event.TYPE_DEFAULT, + teacherId = teacher?.id ?: -1, + subjectId = subject?.id ?: -1, + teamId = team?.id ?: -1, addedDate = editingEvent?.addedDate ?: System.currentTimeMillis() ).also { it.addedManually = true @@ -478,7 +476,7 @@ class EventManualDialog( val metadataObject = Metadata( profileId, - when (type) { + when (type?.id) { Event.TYPE_HOMEWORK -> Metadata.TYPE_HOMEWORK else -> Metadata.TYPE_EVENT }, @@ -578,10 +576,10 @@ class EventManualDialog( } onSaveListener?.invoke(eventObject.withMetadata(metadataObject).also { - it.subjectLongName = b.subjectDropdown.selected?.text?.toString() - it.teacherName = b.teacherDropdown.selected?.text?.toString() - it.teamName = b.teamDropdown.selected?.text?.toString() - it.typeName = b.typeDropdown.selected?.text?.toString() + it.subjectLongName = (b.subjectDropdown.getSelected() as? Subject)?.longName + it.teacherName = b.teacherDropdown.getSelected()?.fullName + it.teamName = b.teamDropdown.getSelected()?.name + it.typeName = b.typeDropdown.getSelected()?.name }) dialog.dismiss() Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt index 92bf0515..6c96c352 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt @@ -175,7 +175,7 @@ class DateDropdown : TextInputDropDown { } } - fun pickerDialog() { + private fun pickerDialog() { val date = getSelected() as? Date ?: Date.getToday() MaterialDatePicker.Builder.datePicker() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt index 5527c50e..878264c6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/EventTypeDropdown.kt @@ -5,9 +5,7 @@ package pl.szczodrzynski.edziennik.ui.modules.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.colorInt @@ -23,18 +21,6 @@ class EventTypeDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var onTypeSelected: ((eventType: EventType) -> Unit)? = null @@ -83,9 +69,7 @@ class EventTypeDropdown : TextInputDropDown { /** * Select an event type by the [typeId]. */ - fun selectType(typeId: Long) { - select(typeId) - } + fun selectType(typeId: Long) = select(typeId) /** * Select an event type by the [typeId] **if it's not selected yet**. diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt index 06e01225..4285ec7d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.crc16 import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.ui.dialogs.input import pl.szczodrzynski.edziennik.utils.TextInputDropDown @@ -40,7 +41,7 @@ class SubjectDropdown : TextInputDropDown { var showNoSubject = true var showCustomSubject = false var customSubjectName = "" - var onSubjectSelected: ((subjectId: Long?) -> Unit)? = null + var onSubjectSelected: ((subject: Subject?) -> Unit)? = null var onCustomSubjectSelected: ((subjectName: String) -> Unit)? = null override fun create(context: Context) { @@ -73,7 +74,7 @@ class SubjectDropdown : TextInputDropDown { list += subjects.map { Item( it.id, it.longName, - tag = it.id + tag = it ) } list @@ -91,10 +92,11 @@ class SubjectDropdown : TextInputDropDown { } -1L -> { // no subject + deselect() onSubjectSelected?.invoke(null) - true + false } - is Long -> { + is Subject -> { // selected a subject onSubjectSelected?.invoke(it.tag) true @@ -104,7 +106,7 @@ class SubjectDropdown : TextInputDropDown { } } - fun customNameDialog() { + private fun customNameDialog() { activity ?: return MaterialAlertDialogBuilder(activity!!) .setTitle("Własny przedmiot") @@ -127,32 +129,37 @@ class SubjectDropdown : TextInputDropDown { .show() } - fun selectSubject(subjectId: Long) { - if (select(subjectId) == null) - select(Item( - subjectId, - "nieznany przedmiot ($subjectId)", - tag = subjectId - )) + /** + * Select a subject by the [subjectId]. + */ + fun selectSubject(subjectId: Long): Item? { + if (subjectId == -1L) { + deselect() + return null + } + return select(subjectId) } - fun selectDefault(subjectId: Long?) { + /** + * Select a subject by the [subjectId] **if it's not selected yet**. + */ + fun selectDefault(subjectId: Long?): Item? { if (subjectId == null || selected != null) - return - selectSubject(subjectId) + return null + return selectSubject(subjectId) } /** * Get the currently selected subject. * ### Returns: * - null if no valid subject is selected - * - [Long] - the selected subject's ID + * - [Subject] - the selected subject * - [String] - a custom subject name entered, if [showCustomSubject] == true */ fun getSelected(): Any? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Subject -> selected?.tag as Subject is String -> selected?.tag as String else -> null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt index 684412f1..23053359 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt @@ -5,13 +5,12 @@ package pl.szczodrzynski.edziennik.ui.modules.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.utils.TextInputDropDown class TeacherDropdown : TextInputDropDown { @@ -19,22 +18,10 @@ class TeacherDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var showNoTeacher = true - var onTeacherSelected: ((teacherId: Long?) -> Unit)? = null + var onTeacherSelected: ((teacher: Teacher?) -> Unit)? = null override fun create(context: Context) { super.create(context) @@ -58,7 +45,7 @@ class TeacherDropdown : TextInputDropDown { list += teachers.map { Item( it.id, it.fullName, - tag = it.id + tag = it ) } list @@ -71,10 +58,11 @@ class TeacherDropdown : TextInputDropDown { when (it.tag) { -1L -> { // no teacher + deselect() onTeacherSelected?.invoke(null) - true + false } - is Long -> { + is Teacher -> { // selected a teacher onTeacherSelected?.invoke(it.tag) true @@ -87,34 +75,33 @@ class TeacherDropdown : TextInputDropDown { /** * Select a teacher by the [teacherId]. */ - fun selectTeacher(teacherId: Long) { - if (select(teacherId) == null) - select(Item( - teacherId, - "nieznany nauczyciel ($teacherId)", - tag = teacherId - )) + fun selectTeacher(teacherId: Long): Item? { + if (teacherId == -1L) { + deselect() + return null + } + return select(teacherId) } /** * Select a teacher by the [teacherId] **if it's not selected yet**. */ - fun selectDefault(teacherId: Long?) { + fun selectDefault(teacherId: Long?): Item? { if (teacherId == null || selected != null) - return - selectTeacher(teacherId) + return null + return selectTeacher(teacherId) } /** * Get the currently selected teacher. * ### Returns: * - null if no valid teacher is selected - * - [Long] - the selected teacher's ID + * - [Teacher] - the selected teacher */ - fun getSelected(): Long? { + fun getSelected(): Teacher? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Teacher -> selected?.tag as Teacher else -> null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt index b31d2ca8..b5eee0ac 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt @@ -5,9 +5,7 @@ package pl.szczodrzynski.edziennik.ui.modules.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R @@ -20,22 +18,10 @@ class TeamDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var showNoTeam = true - var onTeamSelected: ((teamId: Long?) -> Unit)? = null + var onTeamSelected: ((team: Team?) -> Unit)? = null override fun create(context: Context) { super.create(context) @@ -59,7 +45,7 @@ class TeamDropdown : TextInputDropDown { list += teams.map { Item( it.id, it.name, - tag = it.id + tag = it ) } list @@ -72,10 +58,11 @@ class TeamDropdown : TextInputDropDown { when (it.tag) { -1L -> { // no team + deselect() onTeamSelected?.invoke(null) - true + false } - is Long -> { + is Team -> { // selected a team onTeamSelected?.invoke(it.tag) true @@ -85,21 +72,29 @@ class TeamDropdown : TextInputDropDown { } } - fun selectTeam(teamId: Long) { - if (select(teamId) == null) - select(Item( - teamId, - "nieznana grupa ($teamId)", - tag = teamId - )) + /** + * Select a teacher by the [teamId]. + */ + fun selectTeam(teamId: Long): Item? { + if (teamId == -1L) { + deselect() + return null + } + return select(teamId) } - fun selectDefault(teamId: Long?) { + /** + * Select a team by the [teamId] **if it's not selected yet**. + */ + fun selectDefault(teamId: Long?): Item? { if (teamId == null || selected != null) - return - selectTeam(teamId) + return null + return selectTeam(teamId) } + /** + * Select a team of the [Team.TYPE_CLASS] type. + */ fun selectTeamClass() { select(items.singleOrNull { it.tag is Team && it.tag.type == Team.TYPE_CLASS @@ -110,12 +105,12 @@ class TeamDropdown : TextInputDropDown { * Get the currently selected team. * ### Returns: * - null if no valid team is selected - * - [Long] - the team's ID + * - [Team] - the selected team */ - fun getSelected(): Any? { + fun getSelected(): Team? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Team -> selected?.tag as Team else -> null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt index 989184af..afecaf7e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt @@ -175,7 +175,7 @@ class TimeDropdown : TextInputDropDown { return !noTimetable } - fun pickerDialog() { + private fun pickerDialog() { val time = (getSelected() as? Pair<*, *>)?.first as? Time ?: Time.getNow() MaterialTimePicker.Builder() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt index 19aa27d6..44db5d9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt @@ -33,7 +33,7 @@ open class TextInputDropDown : TextInputEditText { val selectedId get() = selected?.id - fun updateText() { + private fun updateText() { setText(selected?.displayText ?: selected?.text) } From 755b846b508bd51a5c7638f41cb95ccbb25ec06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 16:34:31 +0200 Subject: [PATCH 052/240] [UI/Agenda] Move common code to EventManager. --- .../ui/dialogs/event/EventDetailsDialog.kt | 10 ++-- .../ui/dialogs/event/EventListAdapter.kt | 7 ++- .../dialogs/timetable/LessonDetailsDialog.kt | 3 +- .../modules/agenda/AgendaFragmentDefault.kt | 12 +++-- .../agenda/event/AgendaEventRenderer.kt | 25 ++-------- .../attendance/AttendanceListFragment.kt | 3 +- .../attendance/AttendanceSummaryFragment.kt | 3 +- .../ui/modules/grades/GradesAdapter.kt | 3 +- .../ui/modules/grades/GradesListFragment.kt | 9 ++-- .../modules/timetable/TimetableDayFragment.kt | 3 +- .../edziennik/utils/managers/EventManager.kt | 46 ++++++++++++++++++- app/src/main/res/layout/event_list_item.xml | 23 ++-------- 12 files changed, 84 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt index 4feb1d53..c80f0c63 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt @@ -46,6 +46,8 @@ class EventDetailsDialog( private var removeEventDialog: AlertDialog? = null private val eventShared = event.sharedBy != null private val eventOwn = event.sharedBy == "self" + private val manager + get() = app.eventManager private val job = Job() override val coroutineContext: CoroutineContext @@ -93,7 +95,7 @@ class EventDetailsDialog( b.eventOwn = eventOwn if (!event.seen) { - app.eventManager.markAsSeen(event) + manager.markAsSeen(event) } val bullet = " • " @@ -104,11 +106,7 @@ class EventDetailsDialog( } catch (_: Exception) {} - b.legend.text = listOfNotNull( - if (event.addedManually) R.string.legend_event_added_manually else null, - if (event.isDone) R.string.legend_event_is_done else null - ).map { activity.getString(it) }.join("\n") - b.legend.isVisible = b.legend.text.isNotBlank() + manager.setLegendText(b.legend, event) b.typeColor.background?.setTintColor(event.eventColor) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt index 34cc6a2d..9bb199ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt @@ -33,7 +33,8 @@ class EventListAdapter( ) : RecyclerView.Adapter(), CoroutineScope { private val app = context.applicationContext as App - private val manager = app.eventManager + private val manager + get() = app.eventManager private val job = Job() override val coroutineContext: CoroutineContext @@ -67,7 +68,7 @@ class EventListAdapter( b.simpleMode = simpleMode - b.topic.text = event.topic + manager.setEventTopic(b.topic, event, showType = false) b.topic.maxLines = if (simpleMode) 2 else 3 b.details.text = mutableListOf( @@ -102,8 +103,6 @@ class EventListAdapter( } b.editButton.attachToastHint(R.string.hint_edit_event) - b.isDone.isVisible = event.isDone - if (event.showAsUnseen == null) event.showAsUnseen = !event.seen diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt index f444f38e..9b43f9be 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt @@ -50,7 +50,8 @@ class LessonDetailsDialog( get() = job + Dispatchers.Main private lateinit var adapter: EventListAdapter - private val manager by lazy { app.timetableManager } + private val manager + get() = app.timetableManager init { run { if (activity.isFinishing) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index eb6f74fb..cc25c42f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -136,7 +136,13 @@ class AgendaFragmentDefault( dateEnd, Locale.getDefault(), object : CalendarPickerController { - override fun onDaySelected(dayItem: IDayItem) {} + override fun onDaySelected(dayItem: IDayItem) { + val c = Calendar.getInstance() + c.time = dayItem.date + if (c.timeInMillis == selectedDate.inMillis) { + DayDialog(activity, app.profileId, selectedDate) + } + } override fun onEventSelected(event: CalendarEvent) { val date = Date.fromCalendar(event.instanceDay) @@ -180,7 +186,7 @@ class AgendaFragmentDefault( } } }, - AgendaEventRenderer(isCompactMode), + AgendaEventRenderer(app.eventManager, isCompactMode), AgendaEventGroupRenderer(), LessonChangesEventRenderer(), TeacherAbsenceEventRenderer() @@ -197,7 +203,7 @@ class AgendaFragmentDefault( manager.loadEvents(events, BaseCalendarEvent()) adapter?.updateEvents(manager.events) - listView.scrollToCurrentDate(selectedDate.asCalendar) + //listView.scrollToCurrentDate(selectedDate.asCalendar) } private fun setAsRead(date: Calendar) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt index ac65fcef..2e857cd3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventRenderer.kt @@ -10,10 +10,6 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer -import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import com.mikepenz.iconics.utils.colorInt -import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.view.IconicsTextView import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding @@ -22,8 +18,10 @@ import pl.szczodrzynski.edziennik.join import pl.szczodrzynski.edziennik.resolveAttr import pl.szczodrzynski.edziennik.setTintColor import pl.szczodrzynski.edziennik.utils.Colors +import pl.szczodrzynski.edziennik.utils.managers.EventManager class AgendaEventRenderer( + val manager: EventManager, val isCompact: Boolean ) : EventRenderer() { @@ -55,8 +53,6 @@ class AgendaEventRenderer( else event.time!!.stringHM - var eventTitle = "${event.typeName ?: "wydarzenie"} - ${event.topic}" - val eventSubtitle = listOfNotNull( timeText, event.subjectLongName, @@ -64,28 +60,13 @@ class AgendaEventRenderer( event.teamName ).join(", ") - if (event.addedManually) { - eventTitle = "{cmd-clipboard-edit-outline} $eventTitle" - } - card.foreground.setTintColor(event.eventColor) card.background.setTintColor(event.eventColor) - title.text = eventTitle + manager.setEventTopic(title, event, doneIconColor = textColor) title.setTextColor(textColor) subtitle?.text = eventSubtitle subtitle?.setTextColor(textColor) - title.setCompoundDrawables( - null, - null, - if (event.isDone) IconicsDrawable(card.context).apply { - icon = CommunityMaterial.Icon.cmd_check - colorInt = textColor - sizeDp = 24 - } else null, - null - ) - badgeBackground.isVisible = aEvent.showItemBadge badgeBackground.background.setTintColor( android.R.attr.colorBackground.resolveAttr(card.context) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt index 8e68cdbb..523dd09c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt @@ -41,7 +41,8 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.attendanceManager } + private val manager + get() = app.attendanceManager private var viewType = AttendanceFragment.VIEW_DAYS private var expandSubjectId = 0L diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt index 5f61020b..39282325 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt @@ -47,7 +47,8 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.attendanceManager } + private val manager + get() = app.attendanceManager private var expandSubjectId = 0L private var attendance = listOf() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt index 46c75d8d..086f073d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt @@ -40,7 +40,8 @@ class GradesAdapter( } private val app = activity.applicationContext as App - private val manager = app.gradesManager + private val manager + get() = app.gradesManager private val job = Job() override val coroutineContext: CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt index 63521e97..63c935ff 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt @@ -48,9 +48,12 @@ class GradesListFragment : Fragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.gradesManager } - private val dontCountEnabled by lazy { manager.dontCountEnabled } - private val dontCountGrades by lazy { manager.dontCountGrades } + private val manager + get() = app.gradesManager + private val dontCountEnabled + get() = manager.dontCountEnabled + private val dontCountGrades + get() = manager.dontCountGrades private var expandSubjectId = 0L override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt index e6464d7d..e92ca55c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt @@ -54,7 +54,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { private var endHour = DEFAULT_END_HOUR private var firstEventMinute = 24 * 60 - private val manager by lazy { app.timetableManager } + private val manager + get() = app.timetableManager // find SwipeRefreshLayout in the hierarchy private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt index ca444ca5..f5b7faa9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt @@ -4,12 +4,17 @@ package pl.szczodrzynski.edziennik.utils.managers +import androidx.core.view.isVisible +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.iconics.view.IconicsTextView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.startCoroutineTimer import kotlin.coroutines.CoroutineContext class EventManager(val app: App) : CoroutineScope { @@ -32,4 +37,41 @@ class EventManager(val app: App) : CoroutineScope { app.db.metadataDao().setSeen(event.profileId, event, true) } } + + fun setEventTopic( + title: IconicsTextView, + event: EventFull, + showType: Boolean = true, + doneIconColor: Int? = null + ) { + var eventTopic = if (showType) + "${event.typeName ?: "wydarzenie"} - ${event.topic}" + else + event.topic + + if (event.addedManually) { + eventTopic = "{cmd-clipboard-edit-outline} $eventTopic" + } + + title.text = eventTopic + + title.setCompoundDrawables( + null, + null, + if (event.isDone) IconicsDrawable(title.context).apply { + icon = CommunityMaterial.Icon.cmd_check + colorInt = doneIconColor ?: R.color.md_green_500.resolveColor(title.context) + sizeDp = 24 + } else null, + null + ) + } + + fun setLegendText(legend: IconicsTextView, event: EventFull) { + legend.text = listOfNotNull( + if (event.addedManually) R.string.legend_event_added_manually else null, + if (event.isDone) R.string.legend_event_is_done else null + ).map { legend.context.getString(it) }.join("\n") + legend.isVisible = legend.text.isNotBlank() + } } diff --git a/app/src/main/res/layout/event_list_item.xml b/app/src/main/res/layout/event_list_item.xml index 24445327..a69b74c0 100644 --- a/app/src/main/res/layout/event_list_item.xml +++ b/app/src/main/res/layout/event_list_item.xml @@ -4,8 +4,7 @@ --> + xmlns:android="http://schemas.android.com/apk/res/android"> @@ -61,12 +60,10 @@ android:gravity="center_vertical" android:orientation="horizontal"> - - - - + tools:visibility="visible" /> Date: Wed, 14 Apr 2021 16:59:12 +0200 Subject: [PATCH 053/240] [UI/Agenda] Update DayDialog for showing event group. --- .../edziennik/ui/dialogs/day/DayDialog.kt | 62 ++++-- .../modules/agenda/AgendaFragmentDefault.kt | 3 +- .../modules/agenda/event/AgendaEventGroup.kt | 3 +- .../LessonChangesEventRenderer.kt | 18 ++ .../TeacherAbsenceEventRenderer.kt | 15 ++ .../res/layout/agenda_lesson_changes_item.xml | 33 --- .../layout/agenda_teacher_absence_item.xml | 33 --- app/src/main/res/layout/dialog_day.xml | 192 +++++++++--------- 8 files changed, 170 insertions(+), 189 deletions(-) delete mode 100644 app/src/main/res/layout/agenda_lesson_changes_item.xml delete mode 100644 app/src/main/res/layout/agenda_teacher_absence_item.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt index e5b7768d..74ba0030 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt @@ -7,7 +7,7 @@ package pl.szczodrzynski.edziennik.ui.dialogs.day import android.view.View import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.* @@ -19,6 +19,10 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog +import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer +import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent +import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -29,6 +33,7 @@ class DayDialog( val activity: AppCompatActivity, val profileId: Int, val date: Date, + val eventTypeId: Long? = null, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -109,38 +114,51 @@ class DayDialog( } lessonChanges.ifNotEmpty { - b.lessonChangeContainer.root.visibility = View.VISIBLE - b.lessonChangeContainer.lessonChangeCount.text = it.size.toString() + LessonChangesEventRenderer().render( + b.lessonChanges, LessonChangesEvent( + profileId = profileId, + date = date, + count = it.size, + showBadge = false + ) + ) - b.lessonChangeLayout.onClick { + b.lessonChangesFrame.onClick { LessonChangeDialog( - activity, - profileId, - date, - onShowListener = onShowListener, - onDismissListener = onDismissListener + activity, + profileId, + date, + onShowListener = onShowListener, + onDismissListener = onDismissListener ) } } + b.lessonChangesFrame.isVisible = lessonChanges.isNotEmpty() val teacherAbsences = withContext(Dispatchers.Default) { app.db.teacherAbsenceDao().getAllByDateNow(profileId, date) } teacherAbsences.ifNotEmpty { - b.teacherAbsenceContainer.root.visibility = View.VISIBLE - b.teacherAbsenceContainer.teacherAbsenceCount.text = it.size.toString() + TeacherAbsenceEventRenderer().render( + b.teacherAbsence, TeacherAbsenceEvent( + profileId = profileId, + date = date, + count = it.size + ) + ) - b.teacherAbsenceLayout.onClick { + b.teacherAbsenceFrame.onClick { TeacherAbsenceDialog( - activity, - profileId, - date, - onShowListener = onShowListener, - onDismissListener = onDismissListener + activity, + profileId, + date, + onShowListener = onShowListener, + onDismissListener = onDismissListener ) } } + b.teacherAbsenceFrame.isVisible = teacherAbsences.isNotEmpty() adapter = EventListAdapter( activity, @@ -169,8 +187,12 @@ class DayDialog( } ) - app.db.eventDao().getAllByDate(profileId, date).observe(activity, Observer { events -> - adapter.items = events + app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events -> + adapter.items = if (eventTypeId != null) + events.filter { it.type == eventTypeId } + else + events + if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter b.eventsView.apply { @@ -189,6 +211,6 @@ class DayDialog( b.eventsView.visibility = View.GONE b.eventsNoData.visibility = View.VISIBLE } - }) + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt index cc25c42f..9167fb9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragmentDefault.kt @@ -155,7 +155,7 @@ class AgendaFragmentDefault( app.profileId, date ) - is AgendaEventGroup -> DayDialog(activity, app.profileId, date) + is AgendaEventGroup -> DayDialog(activity, app.profileId, date, eventTypeId = event.typeId) is BaseCalendarEvent -> if (event.isPlaceHolder) DayDialog(activity, app.profileId, date) } @@ -260,6 +260,7 @@ class AgendaFragmentDefault( events.add(0, AgendaEventGroup( profileId = event.profileId, date = event.date, + typeId = event.type, typeName = event.typeName ?: "-", typeColor = event.typeColor ?: event.eventColor, count = list.size, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt index e50be2c3..3ef4eb47 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/event/AgendaEventGroup.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date class AgendaEventGroup( val profileId: Int, val date: Date, + val typeId: Long, val typeName: String, val typeColor: Int, val count: Int, @@ -20,5 +21,5 @@ class AgendaEventGroup( color = typeColor, showBadge = showBadge ) { - override fun copy() = AgendaEventGroup(profileId, date, typeName, typeColor, count, showBadge) + override fun copy() = AgendaEventGroup(profileId, date, typeId, typeName, typeColor, count, showBadge) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt index 2a904016..5f66cc27 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchanges/LessonChangesEventRenderer.kt @@ -8,6 +8,7 @@ import android.view.View import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding import pl.szczodrzynski.edziennik.resolveAttr import pl.szczodrzynski.edziennik.setTintColor @@ -33,5 +34,22 @@ class LessonChangesEventRenderer : EventRenderer() { b.badge.isVisible = event.showItemBadge } + fun render(b: AgendaCounterItemBinding, event: LessonChangesEvent) { + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_lesson_changes) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = event.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(b.root.context) + ) + b.badge.isVisible = event.showItemBadge + } + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt index 12dd1947..8d70f941 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt @@ -8,6 +8,7 @@ import android.view.View import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding import pl.szczodrzynski.edziennik.setTintColor import pl.szczodrzynski.edziennik.utils.Colors @@ -29,5 +30,19 @@ class TeacherAbsenceEventRenderer : EventRenderer() { b.badge.isVisible = false } + fun render(b: AgendaCounterItemBinding, event: TeacherAbsenceEvent) { + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_teacher_absence) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = false + b.badge.isVisible = false + } + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter } diff --git a/app/src/main/res/layout/agenda_lesson_changes_item.xml b/app/src/main/res/layout/agenda_lesson_changes_item.xml deleted file mode 100644 index 8700a575..00000000 --- a/app/src/main/res/layout/agenda_lesson_changes_item.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/agenda_teacher_absence_item.xml b/app/src/main/res/layout/agenda_teacher_absence_item.xml deleted file mode 100644 index 4b749e8c..00000000 --- a/app/src/main/res/layout/agenda_teacher_absence_item.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_day.xml b/app/src/main/res/layout/dialog_day.xml index f1544f59..8bbc7b64 100644 --- a/app/src/main/res/layout/dialog_day.xml +++ b/app/src/main/res/layout/dialog_day.xml @@ -3,118 +3,108 @@ ~ Copyright (c) Kuba Szczodrzyński 2019-12-16. --> - - + android:layout_width="match_parent" + android:layout_height="match_parent"> - - - + + + + + + + + + + + + + + + + + android:paddingVertical="16dp" + android:visibility="gone" + tools:visibility="visible"> - + android:layout_gravity="center" + android:drawablePadding="16dp" + android:fontFamily="sans-serif-light" + android:gravity="center" + android:text="@string/dialog_day_no_events" + android:textSize="24sp" + app:drawableTopCompat="@drawable/ic_no_events" /> - - - - - - - - - - - - - - - - - - - - - + android:layout_gravity="center" + android:gravity="center" + android:text="@string/dialog_no_events_hint" + android:textStyle="italic" /> - - + + + + From 02eb5b7ee4499916a5eeca1cf7e3409cdc8d673c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 17:03:44 +0200 Subject: [PATCH 054/240] [UI/Agenda] Disable not available config options. --- app/src/main/res/layout/dialog_config_agenda.xml | 14 +++++++++++--- app/src/main/res/values/strings.xml | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_config_agenda.xml b/app/src/main/res/layout/dialog_config_agenda.xml index 475c9d84..f17b5989 100644 --- a/app/src/main/res/layout/dialog_config_agenda.xml +++ b/app/src/main/res/layout/dialog_config_agenda.xml @@ -1,5 +1,4 @@ - - @@ -67,7 +66,7 @@ android:layout_marginHorizontal="16dp" android:enabled="@{isAgendaMode}" android:text="@string/agenda_config_compact_mode_hint" - android:textAppearance="@style/NavView.TextView.Small" + android:textAppearance="@style/NavView.TextView.Helper" tools:enabled="false" /> + + @@ -141,6 +148,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:checked="@={config.agendaElearningGroup}" + android:enabled="false" android:minHeight="32dp" android:text="@string/agenda_config_elearning_group" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80e158d8..9196508e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1443,4 +1443,5 @@ Grupuj lekcje on-line na liście {cmd-clipboard-edit-outline} wydarzenie dodane ręcznie {cmd-check} oznaczono jako wykonane + Funkcja jeszcze nie jest dostępna. From 1e8fb6a9aecaaff1f5da8cb5fb621d2563d5d144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 20:19:04 +0200 Subject: [PATCH 055/240] [UI/Messages] Restore search string after closing a message. Add message counter. --- .../pl/szczodrzynski/edziennik/Extensions.kt | 57 ++++-- .../edziennik/data/db/full/MessageFull.kt | 2 +- .../ui/modules/messages/MessagesAdapter.kt | 187 ++---------------- .../modules/messages/MessagesListFragment.kt | 40 ++-- .../modules/messages/models/MessagesSearch.kt | 4 +- .../messages/utils/MessagesComparator.kt | 27 +++ .../modules/messages/utils/MessagesFilter.kt | 149 ++++++++++++++ .../messages/utils/SearchTextWatcher.kt | 43 ++++ .../messages/viewholder/MessageViewHolder.kt | 28 +-- .../messages/viewholder/SearchViewHolder.kt | 45 +++-- 10 files changed, 343 insertions(+), 239 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index a07634a1..8d3b59d1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.text.* +import android.text.style.CharacterStyle import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan import android.text.style.StyleSpan @@ -552,28 +553,46 @@ fun CharSequence?.asBoldSpannable(): Spannable { spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } -fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable { +fun CharSequence.asSpannable( + vararg spans: CharacterStyle, + substring: CharSequence? = null, + ignoreCase: Boolean = false, + ignoreDiacritics: Boolean = false +): Spannable { val spannable = SpannableString(this) - if (substring == null) { - spans.forEach { - spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - else if (substring.isNotEmpty()) { - val string = - if (ignoreDiacritics) - this.cleanDiacritics() - else this + substring?.let { substr -> + val string = if (ignoreDiacritics) + this.cleanDiacritics() + else + this + val search = if (ignoreDiacritics) + substr.cleanDiacritics() + else + substr.toString() - var index = string.indexOf(substring, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) - while (index >= 0) { - spans.forEach { - spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + var index = 0 + do { + index = string.indexOf( + string = search, + startIndex = index, + ignoreCase = ignoreCase + ) + + if (index >= 0) { + spans.forEach { + spannable.setSpan( + CharacterStyle.wrap(it), + index, + index + substring.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + index += substring.length.coerceAtLeast(1) } - index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) - } + } while (index >= 0) + + } ?: spans.forEach { + spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } return spannable } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt index 3fdcdd0d..96103f38 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt @@ -30,7 +30,7 @@ class MessageFull( @Ignore var filterWeight = 0 @Ignore - var searchHighlightText: String? = null + var searchHighlightText: CharSequence? = null // metadata var seen = false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt index f8ccb491..aded9f4a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt @@ -1,11 +1,8 @@ package pl.szczodrzynski.edziennik.ui.modules.messages import android.graphics.Typeface -import android.text.Editable -import android.text.TextWatcher import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.Filter import android.widget.Filterable import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView @@ -13,22 +10,19 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.cleanDiacritics -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.utils.MessagesFilter import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder -import java.util.* import kotlin.coroutines.CoroutineContext -import kotlin.math.min class MessagesAdapter( - val activity: AppCompatActivity, - val teachers: List, - val onItemClick: ((item: MessageFull) -> Unit)? = null + val activity: AppCompatActivity, + val teachers: List, + val onItemClick: ((item: MessageFull) -> Unit)? = null ) : RecyclerView.Adapter(), CoroutineScope, Filterable { companion object { private const val TAG = "MessagesAdapter" @@ -43,41 +37,10 @@ class MessagesAdapter( override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - var items = mutableListOf() - var allItems = mutableListOf() + var items = listOf() + var allItems = listOf() val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - private val comparator by lazy { Comparator { o1: Any, o2: Any -> - if (o1 !is MessageFull || o2 !is MessageFull) - return@Comparator 0 - when { - // standard sorting - o1.filterWeight > o2.filterWeight -> return@Comparator 1 - o1.filterWeight < o2.filterWeight -> return@Comparator -1 - else -> when { - // reversed sorting - o1.addedDate > o2.addedDate -> return@Comparator -1 - o1.addedDate < o2.addedDate -> return@Comparator 1 - else -> return@Comparator 0 - } - } - }} - - val textWatcher by lazy { - object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - getFilter().filter(s.toString()) - } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - /*items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.searchText = s?.toString() ?: "" - } - }*/ - } - } - } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -103,138 +66,16 @@ class MessagesAdapter( return when { - holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this) - holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) + holder is MessageViewHolder + && item is MessageFull -> holder.onBind(activity, app, item, position, this) + holder is SearchViewHolder + && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) } } + private val messagesFilter by lazy { + MessagesFilter(this) + } override fun getItemCount() = items.size - override fun getFilter() = filter - private var prevCount = -1 - private val filter by lazy { object : Filter() { - override fun performFiltering(prefix: CharSequence?): FilterResults { - val results = FilterResults() - - if (prevCount == -1) - prevCount = allItems.size - - if (prefix.isNullOrEmpty()) { - allItems.forEach { - if (it is MessageFull) - it.searchHighlightText = null - } - results.values = allItems.toList() - results.count = allItems.size - return results - } - - val items = mutableListOf() - val prefixString = prefix.toString() - - allItems.forEach { - if (it !is MessageFull) { - items.add(it) - return@forEach - } - it.filterWeight = 100 - it.searchHighlightText = null - - var weight: Int - if (it.type == Message.TYPE_SENT) { - it.recipients?.forEach { recipient -> - weight = getMatchWeight(recipient.fullName, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 31 - it.filterWeight = min(it.filterWeight, 10 + weight) - } - } - } - else { - weight = getMatchWeight(it.senderName, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 31 - it.filterWeight = min(it.filterWeight, 10 + weight) - } - } - - - weight = getMatchWeight(it.subject, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 22 - it.filterWeight = min(it.filterWeight, 20 + weight) - } - - if (it.filterWeight != 100) { - it.searchHighlightText = prefixString - items.add(it) - } - } - - Collections.sort(items, comparator) - results.values = items - results.count = items.size - return results - } - - override fun publishResults(constraint: CharSequence?, results: FilterResults) { - results.values?.let { items = it as MutableList } - // do not re-bind the search box - val count = results.count - 1 - - // this tries to update every item except the search field - when { - count > prevCount -> { - notifyItemRangeInserted(prevCount + 1, count - prevCount) - notifyItemRangeChanged(1, prevCount) - } - count < prevCount -> { - notifyItemRangeRemoved(prevCount + 1, prevCount - count) - notifyItemRangeChanged(1, count) - } - else -> { - notifyItemRangeChanged(1, count) - } - } - - /*if (prevCount != count) { - items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.count = count - notifyItemChanged(0) - } - } - }*/ - - prevCount = count - } - }} - - private fun getMatchWeight(name: CharSequence?, prefix: String): Int { - if (name == null) - return 100 - - val nameClean = name.cleanDiacritics() - - // First match against the whole, non-split value - if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { - return 1 - } else { - // check if prefix matches any of the words - val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() - for (word in words) { - if (word.startsWith(prefix, ignoreCase = true)) { - return 2 - } - } - } - // finally check if the prefix matches any part of the name - if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { - return 3 - } - - return 100 - } + override fun getFilter() = messagesFilter } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt index 2e4a30b8..32761515 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt @@ -33,6 +33,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { private lateinit var app: App private lateinit var activity: MainActivity private lateinit var b: MessagesListFragmentBinding + private var adapter: MessagesAdapter? = null private val job: Job = Job() override val coroutineContext: CoroutineContext @@ -53,21 +54,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED) var topPosition = arguments.getInt("topPosition", NO_POSITION) var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION) + val searchText = arguments.getString("searchText", "") teachers = withContext(Dispatchers.Default) { app.db.teacherDao().getAllNow(App.profileId) } - val adapter = MessagesAdapter(activity, teachers) { + adapter = MessagesAdapter(activity, teachers) { activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( "messageId" to it.id )) } - app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { items -> + app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages -> if (!isAdded) return@Observer - items.forEach { message -> + messages.forEach { message -> message.recipients?.removeAll { it.profileId != message.profileId } message.recipients?.forEach { recipient -> if (recipient.fullName == null) { @@ -77,13 +79,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { } // load & configure the adapter - adapter.items = items.toMutableList() - adapter.items.add(0, MessagesSearch().also { - it.count = items.size + val items = messages.toMutableList() + items.add(0, MessagesSearch().also { + it.searchText = searchText }) - adapter.allItems = adapter.items.toMutableList() + + adapter?.items = items + adapter?.allItems = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { - b.list.adapter = adapter + if (searchText.isNotBlank()) + adapter?.filter?.filter(searchText) { + b.list.adapter = adapter + } + else + b.list.adapter = adapter + b.list.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) @@ -92,7 +103,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { addOnScrollListener(onScrollListener) } } - adapter.notifyDataSetChanged() + setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty()) (b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager -> @@ -119,10 +130,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { override fun onDestroy() { super.onDestroy() - if (!isAdded) return + if (!isAdded) + return + val layoutManager = (b.list.layoutManager as? LinearLayoutManager) + val searchItem = adapter?.items?.firstOrNull { it is MessagesSearch } as? MessagesSearch + onPageDestroy?.invoke(position, Bundle( - "topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(), - "bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition() + "topPosition" to layoutManager?.findFirstVisibleItemPosition(), + "bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(), + "searchText" to searchItem?.searchText?.toString() )) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt index ad206c62..71ed0486 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt @@ -5,7 +5,5 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.models class MessagesSearch { - var isFocused = false - var searchText = "" - var count = 0 + var searchText: CharSequence = "" } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt new file mode 100644 index 00000000..2279b600 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesComparator.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.utils + +import pl.szczodrzynski.edziennik.data.db.full.MessageFull + +class MessagesComparator : Comparator { + + override fun compare(o1: Any?, o2: Any?): Int { + if (o1 !is MessageFull || o2 !is MessageFull) + return 0 + + return when { + // standard sorting + o1.filterWeight > o2.filterWeight -> 1 + o1.filterWeight < o2.filterWeight -> -1 + else -> when { + // reversed sorting + o1.addedDate > o2.addedDate -> -1 + o1.addedDate < o2.addedDate -> 1 + else -> 0 + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt new file mode 100644 index 00000000..202db386 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/MessagesFilter.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.utils + +import android.widget.Filter +import pl.szczodrzynski.edziennik.cleanDiacritics +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter +import java.util.* +import kotlin.math.min + +class MessagesFilter( + private val adapter: MessagesAdapter +) : Filter() { + companion object { + private const val NO_MATCH = 1000 + } + + private val comparator = MessagesComparator() + private var prevCount = -1 + + private val allItems + get() = adapter.allItems + + private fun getMatchWeight(name: CharSequence?, prefix: CharSequence): Int { + if (name == null) + return NO_MATCH + + val prefixClean = prefix.cleanDiacritics() + val nameClean = name.cleanDiacritics() + + return when { + // First match against the whole, non-split value + nameClean.startsWith(prefixClean, ignoreCase = true) -> 1 + // check if prefix matches any of the words + nameClean.split(" ").any { + it.startsWith(prefixClean, ignoreCase = true) + } -> 2 + // finally check if the prefix matches any part of the name + nameClean.contains(prefixClean, ignoreCase = true) -> 3 + + else -> NO_MATCH + } + } + + override fun performFiltering(prefix: CharSequence?): FilterResults { + val results = FilterResults() + + if (prevCount == -1) + prevCount = allItems.size + + if (prefix.isNullOrBlank()) { + allItems.forEach { + if (it is MessageFull) + it.searchHighlightText = null + } + results.values = allItems.toList() + results.count = allItems.size + return results + } + + val items = mutableListOf() + + allItems.forEach { + if (it !is MessageFull) { + items.add(it) + return@forEach + } + it.filterWeight = NO_MATCH + it.searchHighlightText = null + + var weight: Int + // weights 11..13 and 110 + if (it.type == Message.TYPE_SENT) { + it.recipients?.forEach { recipient -> + weight = getMatchWeight(recipient.fullName, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + } + } else { + weight = getMatchWeight(it.senderName, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 10 + weight) + } + } + + // weights 21..23 and 120 + weight = getMatchWeight(it.subject, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 20 + weight) + } + + // weights 31..33 and 130 + weight = getMatchWeight(it.body, prefix) + if (weight != NO_MATCH) { + if (weight == 3) + weight = 100 + it.filterWeight = min(it.filterWeight, 30 + weight) + } + + if (it.filterWeight != NO_MATCH) { + it.searchHighlightText = prefix + items.add(it) + } + } + + Collections.sort(items, comparator) + results.values = items + results.count = items.size + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + results.values?.let { + adapter.items = it as MutableList + } + // do not re-bind the search box + val count = results.count - 1 + + // this tries to update every item except the search field + with(adapter) { + when { + count > prevCount -> { + notifyItemRangeInserted(prevCount + 1, count - prevCount) + notifyItemRangeChanged(1, prevCount) + } + count < prevCount -> { + notifyItemRangeRemoved(prevCount + 1, prevCount - count) + notifyItemRangeChanged(1, count) + } + else -> { + notifyItemRangeChanged(1, count) + } + } + } + + prevCount = count + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt new file mode 100644 index 00000000..7103ff29 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/utils/SearchTextWatcher.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.modules.messages.utils + +import android.text.Editable +import android.text.TextWatcher +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding +import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch + +class SearchTextWatcher( + private val b: MessagesListItemSearchBinding, + private val filter: MessagesFilter, + private val item: MessagesSearch +) : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable?) { + item.searchText = s ?: "" + filter.filter(s) { count -> + if (s.isNullOrBlank()) + b.searchLayout.helperText = " " + else + b.searchLayout.helperText = + b.root.context.getString(R.string.messages_search_results, count - 1) + } + } + + override fun equals(other: Any?): Boolean { + return other is SearchTextWatcher + } + + override fun hashCode(): Int { + var result = b.hashCode() + result = 31 * result + filter.hashCode() + result = 31 * result + item.hashCode() + return result + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt index 62789a0c..a466d08b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt @@ -22,17 +22,21 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.utils.models.Date class MessageViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "MessageViewHolder" } - override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) { - val manager = app.gradesManager - + override fun onBind( + activity: AppCompatActivity, + app: App, + item: MessageFull, + position: Int, + adapter: MessagesAdapter + ) { b.messageSubject.text = item.subject b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort b.messageAttachmentImage.isVisible = item.hasAttachments @@ -55,15 +59,17 @@ class MessageViewHolder( b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) b.messageSender.text = messageInfo.profileName - item.searchHighlightText?.let { highlight -> + item.searchHighlightText?.toString()?.let { highlight -> val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) b.messageSubject.text = b.messageSubject.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true + ) b.messageSender.text = b.messageSender.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) + StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), + substring = highlight, ignoreCase = true, ignoreDiacritics = true + ) } adapter.onItemClick?.let { listener -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt index 5adb1984..a0a7d69c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt @@ -9,38 +9,43 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ui.modules.messages.utils.SearchTextWatcher class SearchViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false) + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate( + inflater, + parent, + false + ) ) : RecyclerView.ViewHolder(b.root), BindableViewHolder { companion object { private const val TAG = "SearchViewHolder" } - override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) { - b.searchEdit.removeTextChangedListener(adapter.textWatcher) - b.searchEdit.addTextChangedListener(adapter.textWatcher) + override fun onBind( + activity: AppCompatActivity, + app: App, + item: MessagesSearch, + position: Int, + adapter: MessagesAdapter + ) { + val watcher = SearchTextWatcher(b, adapter.filter, item) + b.searchEdit.removeTextChangedListener(watcher) - /*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener { - override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) { - item.isFocused = showing - } - })*/ + if (adapter.items.isEmpty() || adapter.items.size == adapter.allItems.size) + b.searchLayout.helperText = " " + else + b.searchLayout.helperText = + b.root.context.getString(R.string.messages_search_results, adapter.items.size - 1) + b.searchEdit.setText(item.searchText) - /*if (b.searchEdit.text.toString() != item.searchText) { - b.searchEdit.setText(item.searchText) - b.searchEdit.setSelection(item.searchText.length) - }*/ - - //b.searchLayout.helperText = app.getString(R.string.messages_search_results, item.count) - - /*if (item.isFocused && !b.searchEdit.isFocused) - b.searchEdit.requestFocus()*/ + b.searchEdit.addTextChangedListener(watcher) } } From 47ec1899a1af548c17373650390dc1ed02845742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 21:08:27 +0200 Subject: [PATCH 056/240] [UI/Messages] Add greeting text configuration dialog. --- .../edziennik/config/ProfileConfigUI.kt | 20 +++++ .../edziennik/data/db/entity/Profile.kt | 3 + .../ui/dialogs/MessagesConfigDialog.kt | 66 +++++++++++++++++ .../ui/modules/messages/MessageFragment.kt | 14 +++- .../ui/modules/messages/MessagesFragment.kt | 16 +++- .../compose/MessagesComposeFragment.kt | 44 +++++++++-- .../settings/cards/SettingsRegisterCard.kt | 8 ++ .../res/layout/messages_config_dialog.xml | 73 +++++++++++++++++++ app/src/main/res/values/strings.xml | 6 ++ 9 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt create mode 100644 app/src/main/res/layout/messages_config_dialog.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt index 5ce237a0..2b493572 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt @@ -19,4 +19,24 @@ class ProfileConfigUI(private val config: ProfileConfig) { var homeCards: List get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() } set(value) { config.set("homeCards", value); mHomeCards = value } + + private var mMessagesGreetingOnCompose: Boolean? = null + var messagesGreetingOnCompose: Boolean + get() { mMessagesGreetingOnCompose = mMessagesGreetingOnCompose ?: config.values.get("messagesGreetingOnCompose", true); return mMessagesGreetingOnCompose ?: true } + set(value) { config.set("messagesGreetingOnCompose", value); mMessagesGreetingOnCompose = value } + + private var mMessagesGreetingOnReply: Boolean? = null + var messagesGreetingOnReply: Boolean + get() { mMessagesGreetingOnReply = mMessagesGreetingOnReply ?: config.values.get("messagesGreetingOnReply", true); return mMessagesGreetingOnReply ?: true } + set(value) { config.set("messagesGreetingOnReply", value); mMessagesGreetingOnReply = value } + + private var mMessagesGreetingOnForward: Boolean? = null + var messagesGreetingOnForward: Boolean + get() { mMessagesGreetingOnForward = mMessagesGreetingOnForward ?: config.values.get("messagesGreetingOnForward", false); return mMessagesGreetingOnForward ?: false } + set(value) { config.set("messagesGreetingOnForward", value); mMessagesGreetingOnForward = value } + + private var mMessagesGreetingText: String? = null + var messagesGreetingText: String? + get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText } + set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt index 9e52dc81..b3586a79 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt @@ -129,6 +129,9 @@ open class Profile( val isParent get() = accountName != null + val accountOwnerName + get() = accountName ?: studentNameLong + val registerName get() = when (loginStoreType) { LOGIN_TYPE_LIBRUS -> "librus" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt new file mode 100644 index 00000000..e42bf61f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/MessagesConfigDialog.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.MessagesConfigDialogBinding + +class MessagesConfigDialog( + private val activity: AppCompatActivity, + private val reloadOnDismiss: Boolean = true, + private val onShowListener: ((tag: String) -> Unit)? = null, + private val onDismissListener: ((tag: String) -> Unit)? = null +) { + companion object { + const val TAG = "MessagesConfigDialog" + } + + private val app by lazy { activity.application as App } + private val config by lazy { app.config.ui } + private val profileConfig by lazy { app.config.forProfile().ui } + + private lateinit var b: MessagesConfigDialogBinding + private lateinit var dialog: AlertDialog + + init { run { + if (activity.isFinishing) + return@run + b = MessagesConfigDialogBinding.inflate(activity.layoutInflater) + onShowListener?.invoke(TAG) + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.menu_messages_config) + .setView(b.root) + .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + .setOnDismissListener { + saveConfig() + onDismissListener?.invoke(TAG) + if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() + } + .create() + loadConfig() + dialog.show() + }} + + private fun loadConfig() { + b.config = profileConfig + + b.greetingText.setText( + profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}" + ) + } + + private fun saveConfig() { + val greetingText = b.greetingText.text?.toString()?.trim() + if (greetingText.isNullOrEmpty()) + profileConfig.messagesGreetingText = null + else + profileConfig.messagesGreetingText = "\n\n$greetingText" + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt index 30f40c30..e147dea6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt @@ -30,10 +30,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.colorAttr import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -64,10 +66,20 @@ class MessageFragment : Fragment(), CoroutineScope { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (!isAdded) return + activity.bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null) + } + ) + b.closeButton.setImageDrawable( IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply { colorAttr(activity, android.R.attr.textColorSecondary) - sizeDp = 16 + sizeDp = 24 } ) b.closeButton.setOnClickListener { activity.navigateUp() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt index add36a3c..1849d2f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import kotlin.coroutines.CoroutineContext class MessagesFragment : Fragment(), CoroutineScope { @@ -100,9 +102,19 @@ class MessagesFragment : Fragment(), CoroutineScope { fabIcon = CommunityMaterial.Icon3.cmd_pencil_outline } - setFabOnClickListener(View.OnClickListener { + bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null) + } + ) + + setFabOnClickListener { activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE) - }) + } } activity.gainAttentionFAB() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt index 051d86bd..091dabef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt @@ -4,6 +4,7 @@ package pl.szczodrzynski.edziennik.ui.modules.messages.compose +import android.annotation.SuppressLint import android.content.Context import android.graphics.Typeface import android.graphics.drawable.BitmapDrawable @@ -43,6 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage import pl.szczodrzynski.edziennik.utils.Colors @@ -50,6 +52,7 @@ import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.elevateSurface import kotlin.coroutines.CoroutineContext import kotlin.text.replace @@ -67,6 +70,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main + private val profileConfig by lazy { app.config.forProfile().ui } + private val greetingText + get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}" + private var teachers = mutableListOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -131,6 +138,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } }*/ + activity.bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null) + } + ) + launch { delay(100) getRecipientList() @@ -290,7 +307,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { b.recipients.setIllegalCharacterIdentifier { c -> c.toString().matches("[\\n;:_ ]".toRegex()) } - b.recipients.setOnChipRemoveListener { _ -> + b.recipients.setOnChipRemoveListener { b.recipients.setSelection(b.recipients.text.length) } @@ -318,14 +335,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { fabExtendedText = getString(R.string.messages_compose_send) fabIcon = CommunityMaterial.Icon3.cmd_send_outline - setFabOnClickListener(View.OnClickListener { + setFabOnClickListener { sendMessage() - }) + } } activity.gainAttentionFAB() } + @SuppressLint("SetTextI18n") private fun updateRecipientList(list: List) { launch { withContext(Dispatchers.Default) { teachers = list.sortedBy { it.fullName }.toMutableList() @@ -344,11 +362,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { val adapter = MessagesComposeSuggestionAdapter(activity, teachers) b.recipients.setAdapter(adapter) + if (profileConfig.messagesGreetingOnCompose) + b.text.setText(greetingText) + handleReplyMessage() handleMailToIntent() }} - private fun handleReplyMessage() { launch { + private fun handleReplyMessage() = launch { val replyMessage = arguments?.getString("message") if (replyMessage != null) { val chipList = mutableListOf() @@ -370,8 +391,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { if (arguments?.getString("type") == "reply") { // add greeting text - span.replace(0, 0, "\n\nZ poważaniem,\n${app.profile.accountName - ?: app.profile.studentNameLong ?: ""}\n\n\n") + if (profileConfig.messagesGreetingOnReply) + span.replace(0, 0, "$greetingText\n\n\n") + else + span.replace(0, 0, "\n\n") teachers.firstOrNull { it.id == msg.senderId }?.let { teacher -> teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) @@ -379,7 +402,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { } subject = "Re: ${msg.subject}" } else { - span.replace(0, 0, "\n\n") + // add greeting text + if (profileConfig.messagesGreetingOnForward) + span.replace(0, 0, "$greetingText\n\n\n") + else + span.replace(0, 0, "\n\n") + subject = "Fwd: ${msg.subject}" } body = MessagesUtils.htmlToSpannable(activity, msg.body @@ -401,7 +429,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { else { b.recipients.requestFocus() } - }} + } private fun handleMailToIntent() { val teacherId = arguments?.getLong("messageRecipientId") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt index 36239083..98f32de5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.after import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED +import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog @@ -65,6 +66,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { GradesConfigDialog(activity, reloadOnDismiss = false) }, + util.createActionItem( + text = R.string.menu_messages_config, + icon = CommunityMaterial.Icon.cmd_calendar_outline + ) { + MessagesConfigDialog(activity, reloadOnDismiss = false) + }, + util.createActionItem( text = R.string.menu_attendance_config, icon = CommunityMaterial.Icon.cmd_calendar_remove_outline diff --git a/app/src/main/res/layout/messages_config_dialog.xml b/app/src/main/res/layout/messages_config_dialog.xml new file mode 100644 index 00000000..d0205b39 --- /dev/null +++ b/app/src/main/res/layout/messages_config_dialog.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a008fc94..621b35ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1427,4 +1427,10 @@ Aby móc zapisać wygenerowany plan lekcji musisz przyznać uprawnienia dostępu do pamięci urządzenia.\n\nKliknij OK, aby przyznać uprawnienia. przeczytanie Polityki prywatności i akceptujesz jej postanowienia.

    Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]>
    Szkolny.eu v%s\n%s + Tworzenie wiadomości + Dodaj podpis przy tworzeniu wiadomości + Dodaj podpis przy odpowiadaniu na wiadomość + Dodaj podpis przy przekazywaniu wiadomości + Treść podpisu + Ustawienia wiadomości From 8609956ae7d356e8560052fd20210f6a5d817251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 14 Apr 2021 22:41:06 +0200 Subject: [PATCH 057/240] [UI/Timetable] Add current time marker line. (#30) --- .../modules/timetable/TimetableDayFragment.kt | 143 +++++++++++------- .../drawable/timetable_marker_triangle.xml | 15 ++ .../res/layout/fragment_timetable_v2_day.xml | 32 ---- .../res/layout/timetable_day_fragment.xml | 53 +++++++ 4 files changed, 157 insertions(+), 86 deletions(-) create mode 100644 app/src/main/res/drawable/timetable_marker_triangle.xml delete mode 100644 app/src/main/res/layout/fragment_timetable_v2_day.xml create mode 100644 app/src/main/res/layout/timetable_day_fragment.xml diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt index e92ca55c..3fc9ce83 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt @@ -5,16 +5,16 @@ package pl.szczodrzynski.edziennik.ui.modules.timetable import android.os.Bundle -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ProgressBar import android.widget.TextView import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import androidx.core.view.isVisible +import androidx.core.view.marginTop import androidx.core.view.setPadding -import androidx.lifecycle.Observer +import androidx.core.view.updateLayoutParams import com.linkedin.android.tachyon.DayView import com.linkedin.android.tachyon.DayViewConfig import kotlinx.coroutines.* @@ -24,14 +24,15 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull +import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR -import pl.szczodrzynski.edziennik.utils.ListenerScrollView import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time import java.util.* import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -44,76 +45,66 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { private lateinit var app: App private lateinit var activity: MainActivity private lateinit var inflater: AsyncLayoutInflater + private lateinit var b: TimetableDayFragmentBinding private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main + private var timeIndicatorJob: Job? = null + private lateinit var date: Date private var startHour = DEFAULT_START_HOUR private var endHour = DEFAULT_END_HOUR private var firstEventMinute = 24 * 60 + private var paddingTop = 0 private val manager get() = app.timetableManager // find SwipeRefreshLayout in the hierarchy private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } - // the day ScrollView - private val dayScrollDelegate = lazy { - val dayScroll = ListenerScrollView(context!!) - dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - dayScroll.setOnRefreshLayoutEnabledListener { enabled -> - refreshLayout?.isEnabled = enabled - } - dayScroll - } - private val dayScroll by dayScrollDelegate - // the lesson DayView + private val dayView by lazy { - val dayView = DayView(context!!, DayViewConfig( - startHour = startHour, - endHour = endHour, - dividerHeight = 1.dp, - halfHourHeight = 60.dp, - hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), - halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), - hourLabelWidth = 40.dp, - hourLabelMarginEnd = 10.dp, - eventMargin = 2.dp + val dayView = DayView(activity, DayViewConfig( + startHour = startHour, + endHour = endHour, + dividerHeight = 1.dp, + halfHourHeight = 60.dp, + hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), + halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), + hourLabelWidth = 40.dp, + hourLabelMarginEnd = 10.dp, + eventMargin = 2.dp ), true) dayView.setPadding(10.dp) - dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - dayScroll.addView(dayView) - dayView + return@lazy dayView } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null context ?: return null app = activity.application as App - this.inflater = AsyncLayoutInflater(context!!) + this.inflater = AsyncLayoutInflater(requireContext()) + date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday() startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR - return FrameLayout(activity).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - addView(ProgressBar(activity).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER) - }) - } + + b = TimetableDayFragmentBinding.inflate(inflater, null, false) + return b.root } override fun onPageCreated(): Boolean { // observe lesson database - app.db.timetableDao().getAllForDate(App.profileId, date).observe(this, Observer { lessons -> + app.db.timetableDao().getAllForDate(App.profileId, date).observe(this) { lessons -> launch { val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllByDateNow(App.profileId, date) } processLessonList(lessons, events) } - }) + } return true } @@ -121,9 +112,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { private fun processLessonList(lessons: List, events: List) { // no lessons - timetable not downloaded yet if (lessons.isEmpty()) { - inflater.inflate(R.layout.timetable_no_timetable, view as FrameLayout?) { view, _, parent -> - parent?.removeAllViews() - parent?.addView(view) + inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ -> + b.root.removeAllViews() + b.root.addView(view) + val b = TimetableNoTimetableBinding.bind(view) val weekStart = date.weekStart.stringY_m_d b.noTimetableSync.onClick { @@ -144,9 +136,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { } // one lesson indicating a day without lessons if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { - inflater.inflate(R.layout.timetable_no_lessons, view as FrameLayout?) { view, _, parent -> - parent?.removeAllViews() - parent?.addView(view) + inflater.inflate(R.layout.timetable_no_lessons, b.root) { view, _, _ -> + b.root.removeAllViews() + b.root.addView(view) } return } @@ -158,12 +150,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { return } - // clear the root view and add the ScrollView - (view as FrameLayout?)?.removeAllViews() - (view as FrameLayout?)?.addView(dayScroll) + b.scrollView.isVisible = true + b.dayFrame.removeView(b.dayView) + b.dayFrame.addView(dayView, 0) // Inflate a label view for each hour the day view will display - val hourLabelViews = ArrayList() + val hourLabelViews = mutableListOf() for (i in dayView.startHour..dayView.endHour) { if (!isAdded) continue @@ -172,6 +164,11 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { hourLabelViews.add(hourLabelView) } dayView.setHourLabelViews(hourLabelViews) + // measure dayView top padding needed for the timeIndicator + hourLabelViews.getOrNull(0)?.let { + it.measure(0, 0) + paddingTop = it.measuredHeight / 2 + dayView.paddingTop + } lessons.forEach { it.showAsUnseen = !it.seen } @@ -202,8 +199,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { // Try to recycle an existing event view if there are enough left, otherwise inflate // a new one - val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, dayView, false)) - ?: continue + val eventView = + (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate( + R.layout.timetable_lesson, + dayView, + false + )) ?: continue val lb = TimetableLessonBinding.bind(eventView) eventViews += eventView @@ -291,16 +292,50 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) } + updateTimeIndicator() + dayView.setEventViews(eventViews, eventTimeRanges) val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight - dayScroll.scrollTo(0, firstEventTop.toInt()) + b.scrollView.scrollTo(0, firstEventTop.toInt()) + + b.progressBar.isVisible = false + } + + private fun updateTimeIndicator() { + val time = Time.getNow() + val isTimeInView = + date == Date.getToday() && time.hour in dayView.startHour..dayView.endHour + + b.timeIndicator.isVisible = isTimeInView + b.timeIndicatorMarker.isVisible = isTimeInView + if (isTimeInView) { + val startTime = Time(dayView.startHour, 0, 0) + val seconds = time.inSeconds - startTime.inSeconds * 1f + b.timeIndicator.updateLayoutParams { + topMargin = (seconds * dayView.minuteHeight / 60f).toInt() + paddingTop + } + b.timeIndicatorMarker.updateLayoutParams { + topMargin = b.timeIndicator.marginTop - (16.dp / 2) + (1.dp / 2) + } + } + + if (timeIndicatorJob == null) { + timeIndicatorJob = startCoroutineTimer(repeatMillis = 30000) { + updateTimeIndicator() + } + } } override fun onResume() { super.onResume() - if (dayScrollDelegate.isInitialized()) { - val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight - dayScroll.scrollTo(0, firstEventTop.toInt()) - } + val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight + b.scrollView.scrollTo(0, firstEventTop.toInt()) + updateTimeIndicator() + } + + override fun onPause() { + super.onPause() + timeIndicatorJob?.cancel() + timeIndicatorJob = null } } diff --git a/app/src/main/res/drawable/timetable_marker_triangle.xml b/app/src/main/res/drawable/timetable_marker_triangle.xml new file mode 100644 index 00000000..97d2e71b --- /dev/null +++ b/app/src/main/res/drawable/timetable_marker_triangle.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_timetable_v2_day.xml b/app/src/main/res/layout/fragment_timetable_v2_day.xml deleted file mode 100644 index a94602d3..00000000 --- a/app/src/main/res/layout/fragment_timetable_v2_day.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/timetable_day_fragment.xml b/app/src/main/res/layout/timetable_day_fragment.xml new file mode 100644 index 00000000..5464482d --- /dev/null +++ b/app/src/main/res/layout/timetable_day_fragment.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + From a31c68e87ae3918466b450112c32435662131bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Mon, 19 Apr 2021 14:18:28 +0200 Subject: [PATCH 058/240] [Agenda] Add e-learning event type and DB migration. --- .idea/dictionaries/Kuba.xml | 1 + .../szczodrzynski/edziennik/data/db/AppDb.kt | 5 +- .../edziennik/data/db/dao/EventTypeDao.kt | 48 +++------- .../edziennik/data/db/entity/Event.kt | 4 +- .../edziennik/data/db/entity/EventType.java | 35 -------- .../edziennik/data/db/entity/EventType.kt | 89 +++++++++++++++++++ .../data/db/migration/Migration92.kt | 60 +++++++++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 169 insertions(+), 74 deletions(-) delete mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt diff --git a/.idea/dictionaries/Kuba.xml b/.idea/dictionaries/Kuba.xml index 7910687c..592a5d5e 100644 --- a/.idea/dictionaries/Kuba.xml +++ b/.idea/dictionaries/Kuba.xml @@ -5,6 +5,7 @@ ciasteczko csrf edziennik + elearning gson hebe idziennik diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index e6d7e40e..66fa4348 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.* LibrusLesson::class, TimetableManual::class, Metadata::class -], version = 91) +], version = 92) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -176,7 +176,8 @@ abstract class AppDb : RoomDatabase() { Migration88(), Migration89(), Migration90(), - Migration91() + Migration91(), + Migration92() ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt index 28be6d65..d5006970 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt @@ -9,30 +9,9 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT @Dao abstract class EventTypeDao { @@ -58,19 +37,18 @@ abstract class EventTypeDao { abstract val allNow: List fun addDefaultTypes(context: Context, profileId: Int): List { - val typeList = listOf( - EventType(profileId, TYPE_HOMEWORK, context.getString(R.string.event_type_homework), COLOR_HOMEWORK), - EventType(profileId, TYPE_DEFAULT, context.getString(R.string.event_other), COLOR_DEFAULT), - EventType(profileId, TYPE_EXAM, context.getString(R.string.event_exam), COLOR_EXAM), - EventType(profileId, TYPE_SHORT_QUIZ, context.getString(R.string.event_short_quiz), COLOR_SHORT_QUIZ), - EventType(profileId, TYPE_ESSAY, context.getString(R.string.event_essay), COLOR_ESSAY), - EventType(profileId, TYPE_PROJECT, context.getString(R.string.event_project), COLOR_PROJECT), - EventType(profileId, TYPE_PT_MEETING, context.getString(R.string.event_pt_meeting), COLOR_PT_MEETING), - EventType(profileId, TYPE_EXCURSION, context.getString(R.string.event_excursion), COLOR_EXCURSION), - EventType(profileId, TYPE_READING, context.getString(R.string.event_reading), COLOR_READING), - EventType(profileId, TYPE_CLASS_EVENT, context.getString(R.string.event_class_event), COLOR_CLASS_EVENT), - EventType(profileId, TYPE_INFORMATION, context.getString(R.string.event_information), COLOR_INFORMATION) - ) + var order = 100 + val colorMap = EventType.getTypeColorMap() + val typeList = EventType.getTypeNameMap().map { (id, name) -> + EventType( + profileId = profileId, + id = id, + name = context.getString(name), + color = colorMap[id] ?: COLOR_DEFAULT, + order = order++, + source = SOURCE_DEFAULT + ) + } addAll(typeList) return typeList } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt index a3f44585..3e59cc17 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt @@ -45,6 +45,7 @@ open class Event( var addedDate: Long = System.currentTimeMillis() ) : Keepable() { companion object { + const val TYPE_ELEARNING = -5L const val TYPE_UNDEFINED = -2L const val TYPE_HOMEWORK = -1L const val TYPE_DEFAULT = 0L @@ -57,7 +58,7 @@ open class Event( const val TYPE_READING = 7L const val TYPE_CLASS_EVENT = 8L const val TYPE_INFORMATION = 9L - const val TYPE_TEACHER_ABSENCE = 10L + const val COLOR_ELEARNING = 0xfff57f17.toInt() const val COLOR_HOMEWORK = 0xff795548.toInt() const val COLOR_DEFAULT = 0xffffc107.toInt() const val COLOR_EXAM = 0xfff44336.toInt() @@ -69,7 +70,6 @@ open class Event( const val COLOR_READING = 0xFFFFEB3B.toInt() const val COLOR_CLASS_EVENT = 0xff388e3c.toInt() const val COLOR_INFORMATION = 0xff039be5.toInt() - const val COLOR_TEACHER_ABSENCE = 0xff039be5.toInt() } @ColumnInfo(name = "eventAddedManually") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java deleted file mode 100644 index bf981e84..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.entity; - -import android.graphics.Color; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; - -@Entity(tableName = "eventTypes", - primaryKeys = {"profileId", "eventType"}) -public class EventType { - public int profileId; - - @ColumnInfo(name = "eventType") - public long id; - - @ColumnInfo(name = "eventTypeName") - public String name; - @ColumnInfo(name = "eventTypeColor") - public int color; - - public EventType(int profileId, long id, String name, int color) { - this.profileId = profileId; - this.id = id; - this.name = name; - this.color = color; - } - - public EventType(int profileId, int id, String name, String color) { - this(profileId, id, name, Color.parseColor(color)); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt new file mode 100644 index 00000000..7753b38a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-19. + */ +package pl.szczodrzynski.edziennik.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ + +@Entity( + tableName = "eventTypes", + primaryKeys = ["profileId", "eventType"] +) +class EventType( + val profileId: Int, + + @ColumnInfo(name = "eventType") + val id: Long, + + @ColumnInfo(name = "eventTypeName") + val name: String, + @ColumnInfo(name = "eventTypeColor") + val color: Int, + @ColumnInfo(name = "eventTypeOrder") + var order: Int = id.toInt(), + @ColumnInfo(name = "eventTypeSource") + val source: Int = SOURCE_REGISTER +) { + companion object { + const val SOURCE_DEFAULT = 0 + const val SOURCE_REGISTER = 1 + const val SOURCE_CUSTOM = 2 + const val SOURCE_SHARED = 3 + + fun getTypeColorMap() = mapOf( + TYPE_ELEARNING to COLOR_ELEARNING, + TYPE_HOMEWORK to COLOR_HOMEWORK, + TYPE_DEFAULT to COLOR_DEFAULT, + TYPE_EXAM to COLOR_EXAM, + TYPE_SHORT_QUIZ to COLOR_SHORT_QUIZ, + TYPE_ESSAY to COLOR_ESSAY, + TYPE_PROJECT to COLOR_PROJECT, + TYPE_PT_MEETING to COLOR_PT_MEETING, + TYPE_EXCURSION to COLOR_EXCURSION, + TYPE_READING to COLOR_READING, + TYPE_CLASS_EVENT to COLOR_CLASS_EVENT, + TYPE_INFORMATION to COLOR_INFORMATION + ) + + fun getTypeNameMap() = mapOf( + TYPE_ELEARNING to R.string.event_type_elearning, + TYPE_HOMEWORK to R.string.event_type_homework, + TYPE_DEFAULT to R.string.event_other, + TYPE_EXAM to R.string.event_exam, + TYPE_SHORT_QUIZ to R.string.event_short_quiz, + TYPE_ESSAY to R.string.event_essay, + TYPE_PROJECT to R.string.event_project, + TYPE_PT_MEETING to R.string.event_pt_meeting, + TYPE_EXCURSION to R.string.event_excursion, + TYPE_READING to R.string.event_reading, + TYPE_CLASS_EVENT to R.string.event_class_event, + TYPE_INFORMATION to R.string.event_information + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt new file mode 100644 index 00000000..4b36c0e9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-15. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_REGISTER +import pl.szczodrzynski.edziennik.getInt + +class Migration92 : Migration(91, 92) { + override fun migrate(database: SupportSQLiteDatabase) { + // make eventTypeName not nullable + database.execSQL("ALTER TABLE eventTypes RENAME TO _eventTypes;") + database.execSQL("CREATE TABLE eventTypes (" + + "profileId INTEGER NOT NULL, " + + "eventType INTEGER NOT NULL, " + + "eventTypeName TEXT NOT NULL, " + + "eventTypeColor INTEGER NOT NULL, " + + "PRIMARY KEY(profileId,eventType)" + + ");") + database.execSQL("INSERT INTO eventTypes " + + "(profileId, eventType, eventTypeName, eventTypeColor) " + + "SELECT profileId, eventType, eventTypeName, eventTypeColor " + + "FROM _eventTypes;") + database.execSQL("DROP TABLE _eventTypes;") + + // add columns for order and source + database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeOrder INTEGER NOT NULL DEFAULT 0;") + database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeSource INTEGER NOT NULL DEFAULT 0;") + + // migrate existing types to show correct order and source + database.execSQL("UPDATE eventTypes SET eventTypeOrder = eventType + 102;") + database.execSQL("UPDATE eventTypes SET eventTypeSource = $SOURCE_REGISTER WHERE eventType > $TYPE_INFORMATION;") + + // add new e-learning type + val cursor = database.query("SELECT profileId FROM profiles;") + cursor.use { + while (it.moveToNext()) { + val values = ContentValues().apply { + put("profileId", it.getInt("profileId")) + put("eventType", TYPE_ELEARNING) + put("eventTypeName", "lekcja online") + put("eventTypeColor", COLOR_ELEARNING) + put("eventTypeOrder", 100) + put("eventTypeSource", SOURCE_DEFAULT) + } + + database.insert("eventTypes", SQLiteDatabase.CONFLICT_REPLACE, values) + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31caf032..566be56b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1450,4 +1450,5 @@ Dodaj podpis przy przekazywaniu wiadomości Treść podpisu Ustawienia wiadomości + lekcja online From c855f08f9cae20f467e1f0b8757b8ac4a541e31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 17:46:03 +0200 Subject: [PATCH 059/240] [API/Mobidziennik] Update messages search query to contain student name. --- .../mobidziennik/data/web/MobidziennikWebMessagesAll.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt index 4e8f900a..5581dcc5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt @@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.fixName import pl.szczodrzynski.edziennik.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date +import java.net.URLEncoder class MobidziennikWebMessagesAll(override val data: DataMobidziennik, override val lastSync: Long?, @@ -27,7 +28,8 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, } init { - webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=+") { text -> + val query = URLEncoder.encode(data.profile?.studentNameLong ?: "a", "UTF-8") + webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=$query") { text -> MobidziennikLuckyNumberExtractor(data, text) val doc = Jsoup.parse(text) From c85dac2e4de4dde6a7a6f811d4d497edde314081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 18:00:37 +0200 Subject: [PATCH 060/240] [Strings] Update copyright dates. Fix event mark as done translation. --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-en/strings.xml | 4 ++-- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3af14e8f..431fd960 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -856,7 +856,7 @@ Open-Source-Lizenzen Datenschutzrichtlinie E-Klassenbuch - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - April 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Mai 2021 Klicken Sie hier, um nach Aktualisierungen zu suchen Aktualisierung Version diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 4c0c5b89..e519d272 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -356,7 +356,7 @@ Sharing event… Removing event from the rest of the class… Removing shared event… - Are you sure you want to mark this task as completed?\n\nIt won\'t show up on the main or homework pages, but will still be available in the timetable + Are you sure you want to mark this task as completed?\n\nIt won\'t show up on the main or homework pages, but will still be available in the agenda. Mark as done other project @@ -858,7 +858,7 @@ Open-source licenses Privacy policy E-register - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - April 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - May 2021 Click to check for updates Update Version diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31caf032..8f0ae03f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -921,7 +921,7 @@ Licencje open-source Polityka prywatności E-dziennik - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - kwiecień 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - maj 2021 Kliknij, aby sprawdzić aktualizacje Aktualizacja Wersja From 85d74bec1c1deed645e5a06a2c82c2916f431ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 19:00:09 +0200 Subject: [PATCH 061/240] [UI] Add long text for notification descriptions. --- .../edziennik/data/api/task/Notifications.kt | 61 ++++++++++++++++++- .../data/api/task/PostNotifications.kt | 2 +- .../edziennik/data/db/entity/Notification.kt | 1 + .../WidgetNotificationsFactory.kt | 3 +- app/src/main/res/values/strings.xml | 6 ++ 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt index 21a5c112..28b5c9e9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.getNotificationTitle import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week class Notifications(val app: App, val notifications: MutableList, val profiles: List) { companion object { @@ -42,13 +43,22 @@ class Notifications(val app: App, val notifications: MutableList, val text = app.getString( R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), - if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, + lesson.displayDate?.formattedString ?: "", lesson.changeSubjectName ) + val textLong = app.getString( + R.string.notification_lesson_change_long_format, + lesson.getDisplayChangeType(app), + lesson.displayDate?.formattedString ?: "-", + lesson.displayDate?.weekDay?.let { Week.getFullDayName(it) } ?: "-", + lesson.changeSubjectName, + lesson.changeTeacherName + ) notifications += Notification( id = Notification.buildId(lesson.profileId, Notification.TYPE_TIMETABLE_LESSON_CHANGE, lesson.id), title = app.getNotificationTitle(Notification.TYPE_TIMETABLE_LESSON_CHANGE), text = text, + textLong = textLong, type = Notification.TYPE_TIMETABLE_LESSON_CHANGE, profileId = lesson.profileId, profileName = profiles.singleOrNull { it.id == lesson.profileId }?.name, @@ -79,11 +89,21 @@ class Notifications(val app: App, val notifications: MutableList, event.date.formattedString, event.subjectLongName ) + val textLong = app.getString( + R.string.notification_event_long_format, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topic.take(200) + ) val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT notifications += Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), text = text, + textLong = textLong, type = type, profileId = event.profileId, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, @@ -102,11 +122,22 @@ class Notifications(val app: App, val notifications: MutableList, event.date.formattedString, event.topic ) + val textLong = app.getString( + R.string.notification_shared_event_long_format, + event.sharedByName, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topic.take(200) + ) val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT notifications += Notification( id = Notification.buildId(event.profileId, type, event.id), title = app.getNotificationTitle(type), text = text, + textLong = textLong, type = type, profileId = event.profileId, profileName = profiles.singleOrNull { it.id == event.profileId }?.name, @@ -130,10 +161,20 @@ class Notifications(val app: App, val notifications: MutableList, gradeName, grade.subjectLongName ) + val textLong = app.getString( + R.string.notification_grade_long_format, + gradeName, + grade.weight.toString(), + grade.subjectLongName ?: "-", + grade.category ?: "-", + grade.description ?: "-", + grade.teacherName ?: "-" + ) notifications += Notification( id = Notification.buildId(grade.profileId, Notification.TYPE_NEW_GRADE, grade.id), title = app.getNotificationTitle(Notification.TYPE_NEW_GRADE), text = text, + textLong = textLong, type = Notification.TYPE_NEW_GRADE, profileId = grade.profileId, profileName = profiles.singleOrNull { it.id == grade.profileId }?.name, @@ -158,10 +199,17 @@ class Notifications(val app: App, val notifications: MutableList, notice.teacherName, Date.fromMillis(notice.addedDate).formattedString ) + val textLong = app.getString( + R.string.notification_notice_long_format, + noticeTypeStr, + notice.teacherName ?: "-", + notice.text.take(200) + ) notifications += Notification( id = Notification.buildId(notice.profileId, Notification.TYPE_NEW_NOTICE, notice.id), title = app.getNotificationTitle(Notification.TYPE_NEW_NOTICE), text = text, + textLong = textLong, type = Notification.TYPE_NEW_NOTICE, profileId = notice.profileId, profileName = profiles.singleOrNull { it.id == notice.profileId }?.name, @@ -193,10 +241,21 @@ class Notifications(val app: App, val notifications: MutableList, attendance.subjectLongName, attendance.date.formattedString ) + val textLong = app.getString( + R.string.notification_attendance_long_format, + attendanceTypeStr, + attendance.date.formattedString, + attendance.startTime?.stringHM ?: "-", + attendance.lessonNumber ?: "-", + attendance.subjectLongName ?: "-", + attendance.teacherName ?: "-", + attendance.lessonTopic ?: "-" + ) notifications += Notification( id = Notification.buildId(attendance.profileId, Notification.TYPE_NEW_ATTENDANCE, attendance.id), title = app.getNotificationTitle(Notification.TYPE_NEW_ATTENDANCE), text = text, + textLong = textLong, type = Notification.TYPE_NEW_ATTENDANCE, profileId = attendance.profileId, profileName = profiles.singleOrNull { it.id == attendance.profileId }?.name, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt index 28e62d59..6597125a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt @@ -148,7 +148,7 @@ class PostNotifications(val app: App, nList: List) { colorRes = R.color.colorPrimary }.toBitmap()) .setStyle(NotificationCompat.BigTextStyle() - .bigText(it.text)) + .bigText(it.textLong ?: it.text)) .setWhen(it.addedDate) .addDefaults() .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt index 6a2a787b..0a049216 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt @@ -21,6 +21,7 @@ data class Notification( val title: String, val text: String, + val textLong: String? = null, val type: Int, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt index b5207300..1b6d58e0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt @@ -45,6 +45,7 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot getLong("id") ?: 0, getString("title") ?: "", getString("text") ?: "", + getString("textLong"), getInt("type") ?: 0, getInt("profileId"), getString("profileName"), @@ -74,4 +75,4 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot override fun hasStableIds() = true override fun getViewTypeCount() = 1 override fun onDestroy() = cursor?.close() ?: Unit -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42be8272..5aa206af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1451,4 +1451,10 @@ Treść podpisu Ustawienia wiadomości lekcja online + Rodzaj: %s\nData: %s (%s)\nLekcja: %s\nNauczyciel: %s + Rodzaj: %s\nPrzedmiot: %s\nTermin: %s (%s), %s\nTreść: %s + Dodane przez: %s\nRodzaj: %s\nPrzedmiot: %s\nTermin: %s (%s), %s\nTreść: %s + Ocena: %s (waga %s)\nPrzedmiot: %s\nKategoria: %s\nOpis: %s\nNauczyciel: %s + Rodzaj: %s\nNauczyciel: %s\nTreść: %s + Rodzaj: %s\nTermin: %s, %s\nNr lekcji: %s\nPrzedmiot: %s\nNauczyciel: %s\nTemat lekcji: %s From 26645ee83cea0b274adc28a0fd2fb0aa1d4dfc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 19:00:24 +0200 Subject: [PATCH 062/240] [DB] Add migration 93. --- .../pl/szczodrzynski/edziennik/data/db/AppDb.kt | 5 +++-- .../edziennik/data/db/migration/Migration93.kt | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index 66fa4348..4547d852 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.* LibrusLesson::class, TimetableManual::class, Metadata::class -], version = 92) +], version = 93) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -177,7 +177,8 @@ abstract class AppDb : RoomDatabase() { Migration89(), Migration90(), Migration91(), - Migration92() + Migration92(), + Migration93() ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt new file mode 100644 index 00000000..9d5df5b3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-5-26. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration93 : Migration(92, 93) { + override fun migrate(database: SupportSQLiteDatabase) { + // notifications - long text + database.execSQL("ALTER TABLE notifications ADD COLUMN textLong TEXT DEFAULT NULL;") + } +} From baa98f25c5987e0eea967ee7f00c96ca3c57de5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 19:09:10 +0200 Subject: [PATCH 063/240] [UI] Fix easter egg prize receiving. --- .../edziennik/ui/modules/login/LoginEggsFragment.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt index 0c11bb5a..a6b688b2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt @@ -95,8 +95,10 @@ class LoginEggsFragment : Fragment(), CoroutineScope { anim.interpolator = AccelerateDecelerateInterpolator() anim.duration = 10 anim.fillAfter = true - activity.getRootView().startAnimation(anim) - nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions) + activity.runOnUiThread { + activity.getRootView().startAnimation(anim) + nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions) + } } }, "EggInterface") loadUrl("https://szkolny.eu/game/runner.html") From 75010c07714915bce16178e97a9809add94a8dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 20:01:08 +0200 Subject: [PATCH 064/240] [4.8] Update build.gradle, signing and changelog. --- app/src/main/assets/pl-changelog.html | 12 +++++++++--- app/src/main/cpp/szkolny-signing.cpp | 2 +- .../data/api/szkolny/interceptor/Signing.kt | 2 +- build.gradle | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index db18101b..27c85f0f 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,7 +1,13 @@ -

    Wersja 4.7.1, 2021-04-12

    +

    Wersja 4.8, 2021-05-26

      -
    • Poprawiono sprawdzanie dostępności e-dziennika.
    • -
    • Zmieniono datę w informacjach o aplikacji. @Luncenok
    • +
    • Dodano ikony dla powiadomień. @Luncenok
    • +
    • Terminarz: opcje konfiguracji, widok kompaktowy, grupowanie wydarzeń, znaczki nieprzeczytanych, nowe ikony i wiele innych usprawnień.
    • +
    • Wiadomości: usprawiono wyszukiwanie - zapisywanie szukanego tekstu po wejściu w wiadomość.
    • +
    • Wiadomości: dodano opcję konfiguracji podpisu przy wysyłaniu wiadomości.
    • +
    • Plan lekcji: dodano znacznik aktualnej pory dnia w planie lekcji.
    • +
    • Powiadomienia: dodano szczegółowy opis po rozwinięciu.
    • +
    • Wydarzenia: nowy rodzaj "lekcja online".
    • +
    • Naprawiono odbieranie nagrody w easter egg'u.


    diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index e2311acc..9af553cb 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0xcc, 0x64, 0xdb, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x71, 0xcf, 0xdf, 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index d07afeb7..a6095449 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDXHhAtZBW===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDZ/2nExVD===.$param2".sha256() } } diff --git a/build.gradle b/build.gradle index 2e05ee75..a2eb932b 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.4.31' release = [ - versionName: "4.7.1", - versionCode: 4070199 + versionName: "4.8", + versionCode: 4080099 ] setup = [ From 4184fbb2cd7bde19fc0447057b0e4373db51b03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 20:44:48 +0200 Subject: [PATCH 065/240] [Actions] Change Firebase token to service account file. --- .github/workflows/build-nightly-apk.yml | 2 +- .github/workflows/build-release-apk.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-nightly-apk.yml b/.github/workflows/build-nightly-apk.yml index bca267be..f04c5564 100644 --- a/.github/workflows/build-nightly-apk.yml +++ b/.github/workflows/build-nightly-apk.yml @@ -124,7 +124,7 @@ jobs: uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{ secrets.FIREBASE_APP_ID }} - token: ${{ secrets.FIREBASE_TOKEN }} + serviceCredentialsFile: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} groups: ${{ secrets.FIREBASE_GROUPS_NIGHTLY }} file: ${{ needs.sign.outputs.signedReleaseFile }} releaseNotesFile: ${{ steps.changelog.outputs.commitLogPlainFile }} diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 169704c1..1589290c 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -116,7 +116,7 @@ jobs: uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{ secrets.FIREBASE_APP_ID }} - token: ${{ secrets.FIREBASE_TOKEN }} + serviceCredentialsFile: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} groups: ${{ secrets.FIREBASE_GROUPS_RELEASE }} file: ${{ needs.sign.outputs.signedReleaseFile }} releaseNotesFile: ${{ steps.changelog.outputs.changelogPlainTitledFile }} From 909899612efb84507cb9f85ca1e0aaf3d5bb4a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 26 May 2021 20:44:48 +0200 Subject: [PATCH 066/240] Revert "[Actions] Change Firebase token to service account file." This reverts commit 4184fbb2cd7bde19fc0447057b0e4373db51b03f. --- .github/workflows/build-nightly-apk.yml | 2 +- .github/workflows/build-release-apk.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-nightly-apk.yml b/.github/workflows/build-nightly-apk.yml index f04c5564..bca267be 100644 --- a/.github/workflows/build-nightly-apk.yml +++ b/.github/workflows/build-nightly-apk.yml @@ -124,7 +124,7 @@ jobs: uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{ secrets.FIREBASE_APP_ID }} - serviceCredentialsFile: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} + token: ${{ secrets.FIREBASE_TOKEN }} groups: ${{ secrets.FIREBASE_GROUPS_NIGHTLY }} file: ${{ needs.sign.outputs.signedReleaseFile }} releaseNotesFile: ${{ steps.changelog.outputs.commitLogPlainFile }} diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 1589290c..169704c1 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -116,7 +116,7 @@ jobs: uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{ secrets.FIREBASE_APP_ID }} - serviceCredentialsFile: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }} + token: ${{ secrets.FIREBASE_TOKEN }} groups: ${{ secrets.FIREBASE_GROUPS_RELEASE }} file: ${{ needs.sign.outputs.signedReleaseFile }} releaseNotesFile: ${{ steps.changelog.outputs.changelogPlainTitledFile }} From 1bf07d736f07b3279e46b9d520549573bf6c5ad5 Mon Sep 17 00:00:00 2001 From: "B.O.S.S" Date: Wed, 2 Jun 2021 22:59:57 +0200 Subject: [PATCH 067/240] [API/Librus] Update client ID (#51) @BxOxSxS --- .../main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index f06ef354..db53282e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -24,7 +24,7 @@ const val FAKE_LIBRUS_ACCOUNTS = "/synergia_accounts.php" val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp" const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0" -const val LIBRUS_CLIENT_ID = "0RbsDOkV9tyKEQYzlLv5hs3DM1ukrynFI4p6C1Yc" +const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP" const val LIBRUS_REDIRECT_URL = "app://librus" const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code" const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action" From 71ca51e813337ffdb3017e6dd2bfb909e67e33a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 6 Jun 2021 16:47:10 +0200 Subject: [PATCH 068/240] [Strings] Update copyright dates. --- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-en/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 431fd960..18190d60 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -856,7 +856,7 @@ Open-Source-Lizenzen Datenschutzrichtlinie E-Klassenbuch - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Mai 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - Juni 2021 Klicken Sie hier, um nach Aktualisierungen zu suchen Aktualisierung Version diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index e519d272..d34932b7 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -858,7 +858,7 @@ Open-source licenses Privacy policy E-register - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - May 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nSeptember 2018 - June 2021 Click to check for updates Update Version diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5aa206af..2043046f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -921,7 +921,7 @@ Licencje open-source Polityka prywatności E-dziennik - © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - maj 2021 + © Kuba Szczodrzyński && Kacper Ziubryniewicz\nwrzesień 2018 - czerwiec 2021 Kliknij, aby sprawdzić aktualizacje Aktualizacja Wersja From ae4405ef781e970ccc3530672dd2f869e50f63f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Sun, 6 Jun 2021 16:50:04 +0200 Subject: [PATCH 069/240] [4.8.1] Update build.gradle, signing and changelog. --- app/src/main/assets/pl-changelog.html | 12 +++--------- app/src/main/cpp/szkolny-signing.cpp | 2 +- .../data/api/szkolny/interceptor/Signing.kt | 2 +- build.gradle | 4 ++-- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 27c85f0f..5c110e87 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,13 +1,7 @@ -

    Wersja 4.8, 2021-05-26

    +

    Wersja 4.8.1, 2021-06-06

      -
    • Dodano ikony dla powiadomień. @Luncenok
    • -
    • Terminarz: opcje konfiguracji, widok kompaktowy, grupowanie wydarzeń, znaczki nieprzeczytanych, nowe ikony i wiele innych usprawnień.
    • -
    • Wiadomości: usprawiono wyszukiwanie - zapisywanie szukanego tekstu po wejściu w wiadomość.
    • -
    • Wiadomości: dodano opcję konfiguracji podpisu przy wysyłaniu wiadomości.
    • -
    • Plan lekcji: dodano znacznik aktualnej pory dnia w planie lekcji.
    • -
    • Powiadomienia: dodano szczegółowy opis po rozwinięciu.
    • -
    • Wydarzenia: nowy rodzaj "lekcja online".
    • -
    • Naprawiono odbieranie nagrody w easter egg'u.
    • +
    • Poprawiono funkcje logowania. @BxOxSxS
    • +
    • MobiDziennik: naprawiono wysyłanie wiadomości (błąd "nie znaleziono wiadomości").


    diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index 9af553cb..273bfd2f 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0x71, 0xcf, 0xdf, 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x90, 0x44, 0x08, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index a6095449..92fecbb2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDZ/2nExVD===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDOlSskDmW===.$param2".sha256() } } diff --git a/build.gradle b/build.gradle index a2eb932b..be590e1e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.4.31' release = [ - versionName: "4.8", - versionCode: 4080099 + versionName: "4.8.1", + versionCode: 4080199 ] setup = [ From a4d604e146c015d99e6de4a9d8d3c0b91c73c4fa Mon Sep 17 00:00:00 2001 From: arin <67313390+6Arin9@users.noreply.github.com> Date: Fri, 11 Jun 2021 00:05:54 +0200 Subject: [PATCH 070/240] [API/Librus] Update JST Client ID (#53) @6Arin9 --- .../main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index db53282e..cb0d5a06 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -43,7 +43,7 @@ const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token" const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST" const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE=" const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398" -const val LIBRUS_API_CLIENT_ID_JST = "49" +const val LIBRUS_API_CLIENT_ID_JST = "59" //const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42" const val LIBRUS_JST_DEMO_CODE = "68656A21" From 4bed62aa6f6690ce5433e30b42674c17f3a3e82a Mon Sep 17 00:00:00 2001 From: doteq Date: Fri, 11 Jun 2021 22:06:07 +0200 Subject: [PATCH 071/240] [UI] Fix timetable crash when syncing (#54) * Fix removeView * Use removeView() instead of removeAllViews() * Remove dayView from layout file --- .../edziennik/ui/modules/timetable/TimetableDayFragment.kt | 2 +- app/src/main/res/layout/timetable_day_fragment.xml | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt index 3fc9ce83..00ce872e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt @@ -151,7 +151,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { } b.scrollView.isVisible = true - b.dayFrame.removeView(b.dayView) + b.dayFrame.removeView(dayView) b.dayFrame.addView(dayView, 0) // Inflate a label view for each hour the day view will display diff --git a/app/src/main/res/layout/timetable_day_fragment.xml b/app/src/main/res/layout/timetable_day_fragment.xml index 5464482d..041f6e1f 100644 --- a/app/src/main/res/layout/timetable_day_fragment.xml +++ b/app/src/main/res/layout/timetable_day_fragment.xml @@ -36,12 +36,6 @@ android:layout_marginHorizontal="8dp" android:background="@color/md_red_500" tools:layout_marginTop="100dp" /> - - From 5a217aca015da97cecd47f364a0296be326e756f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Tue, 15 Jun 2021 18:13:18 +0200 Subject: [PATCH 072/240] [4.8.2] Update build.gradle, signing and changelog. --- app/src/main/assets/pl-changelog.html | 6 +++--- app/src/main/cpp/szkolny-signing.cpp | 2 +- .../edziennik/data/api/szkolny/interceptor/Signing.kt | 2 +- build.gradle | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 5c110e87..fbc5bbd5 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,7 +1,7 @@ -

    Wersja 4.8.1, 2021-06-06

    +

    Wersja 4.8.2, 2021-06-15

      -
    • Poprawiono funkcje logowania. @BxOxSxS
    • -
    • MobiDziennik: naprawiono wysyłanie wiadomości (błąd "nie znaleziono wiadomości").
    • +
    • Poprawiono funkcje logowania. @6Arin9
    • +
    • Naprawiono zatrzymanie aplikacji na ekranie planu lekcji. @doteq


    diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index 273bfd2f..6c412f0b 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0x90, 0x44, 0x08, 0xb8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x1d, 0xa7, 0x6e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index 92fecbb2..fa3f4128 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDOlSskDmW===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MDn7mcwDD+===.$param2".sha256() } } diff --git a/build.gradle b/build.gradle index be590e1e..4d60cda8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { kotlin_version = '1.4.31' release = [ - versionName: "4.8.1", - versionCode: 4080199 + versionName: "4.8.2", + versionCode: 4080299 ] setup = [ From 288c80ea26b468b3f35d36a69baa4a815787748a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 2 Sep 2021 17:40:34 +0200 Subject: [PATCH 073/240] [Gradle] Update wrapper and AGP to match AS 2020.3.1. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4d60cda8..9214ceda 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.2.0-beta06" + classpath 'com.android.tools.build:gradle:7.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9bd0ad16..80cea62c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Feb 17 14:04:38 CET 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 96dbb0a05759c5498947f0c8c5c0953d52a5f9ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 2 Sep 2021 17:48:02 +0200 Subject: [PATCH 074/240] [Gradle] Update Kotlin to v1.5.20. --- .../mobidziennik/data/web/MobidziennikWebAttendance.kt | 2 +- .../edziennik/ui/modules/login/LoginProgressFragment.kt | 2 +- .../edziennik/ui/modules/timetable/TimetableFragment.kt | 4 ++-- build.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt index f23478b6..cbd797b1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt @@ -48,7 +48,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik, //syncWeeks.clear() //syncWeeks += Date.fromY_m_d("2019-12-19") - syncWeeks.minBy { it.value }?.let { + syncWeeks.minByOrNull { it.value }?.let { data.toRemove.add(DataRemoveModel.Attendance.from(it)) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt index 7550b08c..aaebbac0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt @@ -76,7 +76,7 @@ class LoginProgressFragment : Fragment(), CoroutineScope { val maxProfileId = max( app.db.profileDao().lastId ?: 0, - activity.profiles.maxBy { it.profile.id }?.profile?.id ?: 0 + activity.profiles.maxByOrNull { it.profile.id }?.profile?.id ?: 0 ) val loginType = args.getInt("loginType", -1) val loginMode = args.getInt("loginMode", 0) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt index 970daeec..85332fe1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt @@ -120,8 +120,8 @@ class TimetableFragment : Fragment(), CoroutineScope { } val lessonRanges = app.db.lessonRangeDao().getAllNow(App.profileId) - startHour = lessonRanges.map { it.startTime.hour }.min() ?: DEFAULT_START_HOUR - endHour = lessonRanges.map { it.endTime.hour }.max()?.plus(1) ?: DEFAULT_END_HOUR + startHour = lessonRanges.map { it.startTime.hour }.minOrNull() ?: DEFAULT_START_HOUR + endHour = lessonRanges.map { it.endTime.hour }.maxOrNull()?.plus(1) ?: DEFAULT_END_HOUR } deferred.await() if (!isAdded) diff --git a/build.gradle b/build.gradle index 9214ceda..ac506325 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.4.31' + kotlin_version = '1.5.20' release = [ versionName: "4.8.2", From c88056ddb952bef0730abda96fe7ef5f3fce372c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 2 Sep 2021 17:49:01 +0200 Subject: [PATCH 075/240] [Gradle] Update library dependencies. --- app/build.gradle | 26 +++++++++++++------------- build.gradle | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c0af6190..29a434d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,25 +120,25 @@ dependencies { coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" // Android Jetpack - implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.appcompat:appcompat:1.3.1" implementation "androidx.cardview:cardview:1.0.0" - implementation "androidx.constraintlayout:constraintlayout:2.0.4" - implementation "androidx.core:core-ktx:1.3.2" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0" - implementation "androidx.navigation:navigation-fragment-ktx:2.3.4" - implementation "androidx.recyclerview:recyclerview:1.1.0" - implementation "androidx.room:room-runtime:2.2.6" - implementation "androidx.work:work-runtime-ktx:2.5.0" - kapt "androidx.room:room-compiler:2.2.6" + implementation "androidx.constraintlayout:constraintlayout:2.1.0" + implementation "androidx.core:core-ktx:1.6.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" + implementation "androidx.navigation:navigation-fragment-ktx:2.3.5" + implementation "androidx.recyclerview:recyclerview:1.2.1" + implementation "androidx.room:room-runtime:2.3.0" + implementation "androidx.work:work-runtime-ktx:2.6.0" + kapt "androidx.room:room-compiler:2.3.0" // Google design libs - implementation "com.google.android.material:material:1.3.0" + implementation "com.google.android.material:material:1.4.0" implementation "com.google.android:flexbox:2.0.1" // Play Services/Firebase - implementation "com.google.android.gms:play-services-wearable:17.0.0" - implementation "com.google.firebase:firebase-core:18.0.2" - implementation "com.google.firebase:firebase-crashlytics:17.4.0" + implementation "com.google.android.gms:play-services-wearable:17.1.0" + implementation "com.google.firebase:firebase-core:19.0.1" + implementation "com.google.firebase:firebase-crashlytics:18.2.1" implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } } // OkHttp, Retrofit, Gson, Jsoup diff --git a/build.gradle b/build.gradle index ac506325..99ad2eca 100644 --- a/build.gradle +++ b/build.gradle @@ -23,8 +23,8 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.5' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' + classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' } } From e2bf48d1b68e7ba8f0bc67ced9615a15184a7390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 2 Sep 2021 18:27:51 +0200 Subject: [PATCH 076/240] [Actions] Use JDK 11. --- .github/workflows/build-nightly-apk.yml | 8 +++++--- .github/workflows/build-release-aab-play.yml | 8 +++++--- .github/workflows/build-release-apk.yml | 8 +++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-nightly-apk.yml b/.github/workflows/build-nightly-apk.yml index bca267be..c64dba12 100644 --- a/.github/workflows/build-nightly-apk.yml +++ b/.github/workflows/build-nightly-apk.yml @@ -51,10 +51,12 @@ jobs: androidHome: ${{ env.ANDROID_HOME }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} steps: - - name: Setup JDK 1.8 - uses: actions/setup-java@v1 + - name: Setup JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'zulu' + java-version: '11' + cache: 'gradle' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Clean build artifacts diff --git a/.github/workflows/build-release-aab-play.yml b/.github/workflows/build-release-aab-play.yml index ec0a7069..5f3026e6 100644 --- a/.github/workflows/build-release-aab-play.yml +++ b/.github/workflows/build-release-aab-play.yml @@ -43,10 +43,12 @@ jobs: androidHome: ${{ env.ANDROID_HOME }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} steps: - - name: Setup JDK 1.8 - uses: actions/setup-java@v1 + - name: Setup JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'zulu' + java-version: '11' + cache: 'gradle' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Clean build artifacts diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 169704c1..4816ee73 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -43,10 +43,12 @@ jobs: androidHome: ${{ env.ANDROID_HOME }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} steps: - - name: Setup JDK 1.8 - uses: actions/setup-java@v1 + - name: Setup JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'zulu' + java-version: '11' + cache: 'gradle' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Clean build artifacts From 7b4effe8893414a5fbfa6757daa0f29f1c19588b Mon Sep 17 00:00:00 2001 From: doteq Date: Tue, 7 Sep 2021 22:11:29 +0200 Subject: [PATCH 077/240] [UI] Fix block timetable export. (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use MediaStore on SDK level >= 29 for timetable export * Fix imports * Use subdirectory in the Photos folder * Use MediaStore for all API levels * Remove not-null assertion * Remove unnecessary outputStream close. * Use File constructor for directory path * Use File(File, String) constructor Co-authored-by: Kuba Szczodrzyński --- .../timetable/GenerateBlockTimetableDialog.kt | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt index c93d5a6e..9eefffd0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt @@ -4,11 +4,14 @@ package pl.szczodrzynski.edziennik.ui.dialogs.timetable +import android.content.ContentResolver +import android.content.ContentValues import android.content.Intent import android.graphics.* import android.net.Uri import android.os.Build import android.os.Environment +import android.provider.MediaStore import android.util.Log import android.view.View import android.view.View.MeasureSpec @@ -373,25 +376,31 @@ class GenerateBlockTimetableDialog( val today = Date.getToday().stringY_m_d val now = Time.getNow().stringH_M_S + val filename = "plan_lekcji_${app.profile.name}_${today}_${now}.png" + val resolver: ContentResolver = activity.applicationContext.contentResolver + val values = ContentValues() + values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") - val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() } - val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + values.put(MediaStore.MediaColumns.RELATIVE_PATH, File(Environment.DIRECTORY_PICTURES, "Szkolny.eu").path) + } else { + val picturesDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Szkolny.eu") + picturesDirectory.mkdirs() + values.put(MediaStore.MediaColumns.DATA, File(picturesDirectory, filename).path) + } try { - val fos = FileOutputStream(outputFile) - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) - fos.close() + val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null + resolver.openOutputStream(uri).use { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) + } + uri } catch (e: Exception) { Log.e("SAVE_IMAGE", e.message, e) return@withContext null } - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile) - } else { - Uri.parse("file://" + outputFile.absolutePath) - } - uri } progressDialog.dismiss() From 452271e8c0a7b4ca6261aef2acb9eb2d36f54ba2 Mon Sep 17 00:00:00 2001 From: Tomasz F Date: Wed, 8 Sep 2021 19:11:14 +0200 Subject: [PATCH 078/240] [UI] Add list of contributors in Settings. (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Contributors item in settings * Move contributors activity to settings package && actualize branch * Update AndroidManifest.xml * Getting contributors from github api * Cleaning code * Fetching data from szkolny api, displaying content, a lot of changes :D * Strings * Remove androidx legacy library * Revert manifest changes * Remove logging in SzkolnyApi * Fix app name spelling * Revert changes to dimens.xml * Refactor contributors code * Revert changes to dimens.xml Again * Revert changes to build.gradle * Revert changes to gradle-wrapper.properties * Revert changes to gradle.properties * Make user name nullable * Add caching, refactor plurals, add progress bar * Update contributors UI * Shorten activity name in manifest * Remove unneeded line break * Remove fragment_translators.xml Co-authored-by: Kuba Szczodrzyński --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 1 + .../pl/szczodrzynski/edziennik/Extensions.kt | 2 + .../edziennik/data/api/szkolny/SzkolnyApi.kt | 14 ++- .../data/api/szkolny/SzkolnyService.kt | 3 + .../szkolny/response/ContributorsResponse.kt | 20 ++++ .../lazypager/FragmentLazyPagerAdapter.kt | 2 +- .../settings/cards/SettingsAboutCard.kt | 9 ++ .../contributors/ContributorsActivity.kt | 83 +++++++++++++++ .../contributors/ContributorsAdapter.kt | 63 +++++++++++ .../contributors/ContributorsFragment.kt | 47 ++++++++ .../main/res/layout/contributors_activity.xml | 100 ++++++++++++++++++ .../res/layout/contributors_list_fragment.xml | 10 ++ .../res/layout/contributors_list_item.xml | 49 +++++++++ app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-en/plurals.xml | 11 +- app/src/main/res/values-en/strings.xml | 4 + app/src/main/res/values/plurals.xml | 11 ++ app/src/main/res/values/strings.xml | 6 ++ 19 files changed, 432 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ContributorsResponse.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsActivity.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsAdapter.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/contributors/ContributorsFragment.kt create mode 100644 app/src/main/res/layout/contributors_activity.xml create mode 100644 app/src/main/res/layout/contributors_list_fragment.xml create mode 100644 app/src/main/res/layout/contributors_list_item.xml diff --git a/app/build.gradle b/app/build.gradle index 29a434d8..827ae0d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af4b653a..cbe7bd48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -146,6 +146,7 @@ android:configChanges="orientation|keyboardHidden" android:theme="@style/Base.Theme.AppCompat" /> + %d ocen + + + %d commit + %d commity + %d commit\'ów + + + %d tłumaczenie + %d tłumaczenia + %d tłumaczeń + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2043046f..b11631b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1391,6 +1391,10 @@ Zobacz także Wejdź na stronę aplikacji Uzyskaj pomoc lub wesprzyj autorów + Twórcy aplikacji + Lista twórców Szkolnego + Współtwórcy + Tłumacze Kod źródłowy Pomóż w rozwoju aplikacji na GitHubie Nazwa profilu @@ -1457,4 +1461,6 @@ Ocena: %s (waga %s)\nPrzedmiot: %s\nKategoria: %s\nOpis: %s\nNauczyciel: %s Rodzaj: %s\nNauczyciel: %s\nTreść: %s Rodzaj: %s\nTermin: %s, %s\nNr lekcji: %s\nPrzedmiot: %s\nNauczyciel: %s\nTemat lekcji: %s + \@%s - %s + Najłatwiejszy sposób na korzystanie z e-dziennika. From 8f72e11d0c1a1314aad72532e0fd4ea3438f9117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 8 Sep 2021 22:48:21 +0200 Subject: [PATCH 079/240] [UI] Center "no data" text view. (#59) --- app/src/main/res/layout/attendance_list_fragment.xml | 1 + app/src/main/res/layout/attendance_summary_fragment.xml | 1 + app/src/main/res/layout/card_home_grades.xml | 1 + app/src/main/res/layout/card_home_template.xml | 1 + app/src/main/res/layout/grades_item_stats.xml | 1 + app/src/main/res/layout/grades_list_fragment.xml | 1 + app/src/main/res/layout/homework_list_fragment.xml | 1 + app/src/main/res/layout/messages_list_fragment.xml | 1 + app/src/main/res/layout/notifications_list_fragment.xml | 1 + app/src/main/res/layout/template_list_fragment.xml | 1 + app/src/main/res/layout/template_list_page_fragment.xml | 1 + 11 files changed, 11 insertions(+) diff --git a/app/src/main/res/layout/attendance_list_fragment.xml b/app/src/main/res/layout/attendance_list_fragment.xml index 36f54733..bfcbf645 100644 --- a/app/src/main/res/layout/attendance_list_fragment.xml +++ b/app/src/main/res/layout/attendance_list_fragment.xml @@ -24,6 +24,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/attendances_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/attendance_summary_fragment.xml b/app/src/main/res/layout/attendance_summary_fragment.xml index e4d96e80..80b01ed4 100644 --- a/app/src/main/res/layout/attendance_summary_fragment.xml +++ b/app/src/main/res/layout/attendance_summary_fragment.xml @@ -152,6 +152,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/attendances_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/card_home_grades.xml b/app/src/main/res/layout/card_home_grades.xml index faab735b..38ae61de 100644 --- a/app/src/main/res/layout/card_home_grades.xml +++ b/app/src/main/res/layout/card_home_grades.xml @@ -22,6 +22,7 @@ android:layout_gravity="center_horizontal" android:layout_margin="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/card_grades_no_data" android:textSize="16sp" /> diff --git a/app/src/main/res/layout/card_home_template.xml b/app/src/main/res/layout/card_home_template.xml index 8cea8889..1570c8b3 100644 --- a/app/src/main/res/layout/card_home_template.xml +++ b/app/src/main/res/layout/card_home_template.xml @@ -24,6 +24,7 @@ android:layout_gravity="center_horizontal" android:layout_margin="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/card_grades_no_data" android:textSize="16sp" /> diff --git a/app/src/main/res/layout/grades_item_stats.xml b/app/src/main/res/layout/grades_item_stats.xml index 8ffeab4f..8bddb7de 100644 --- a/app/src/main/res/layout/grades_item_stats.xml +++ b/app/src/main/res/layout/grades_item_stats.xml @@ -32,6 +32,7 @@ android:layout_gravity="center_horizontal" android:layout_margin="8dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/grades_stats_no_data" android:textSize="16sp" android:visibility="gone" diff --git a/app/src/main/res/layout/grades_list_fragment.xml b/app/src/main/res/layout/grades_list_fragment.xml index faf8264d..ec2d7a59 100644 --- a/app/src/main/res/layout/grades_list_fragment.xml +++ b/app/src/main/res/layout/grades_list_fragment.xml @@ -29,6 +29,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/grades_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/homework_list_fragment.xml b/app/src/main/res/layout/homework_list_fragment.xml index d53b9d17..53949d24 100644 --- a/app/src/main/res/layout/homework_list_fragment.xml +++ b/app/src/main/res/layout/homework_list_fragment.xml @@ -24,6 +24,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/homework_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/messages_list_fragment.xml b/app/src/main/res/layout/messages_list_fragment.xml index 0399af9b..78de5194 100644 --- a/app/src/main/res/layout/messages_list_fragment.xml +++ b/app/src/main/res/layout/messages_list_fragment.xml @@ -24,6 +24,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/messages_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/notifications_list_fragment.xml b/app/src/main/res/layout/notifications_list_fragment.xml index a5c9aa90..e73d45d5 100644 --- a/app/src/main/res/layout/notifications_list_fragment.xml +++ b/app/src/main/res/layout/notifications_list_fragment.xml @@ -24,6 +24,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/notifications_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/template_list_fragment.xml b/app/src/main/res/layout/template_list_fragment.xml index 79646608..19c7221c 100644 --- a/app/src/main/res/layout/template_list_fragment.xml +++ b/app/src/main/res/layout/template_list_fragment.xml @@ -29,6 +29,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/grades_no_data" android:textSize="24sp" android:visibility="gone" diff --git a/app/src/main/res/layout/template_list_page_fragment.xml b/app/src/main/res/layout/template_list_page_fragment.xml index 09f8bfda..062b4c89 100644 --- a/app/src/main/res/layout/template_list_page_fragment.xml +++ b/app/src/main/res/layout/template_list_page_fragment.xml @@ -24,6 +24,7 @@ android:layout_gravity="center" android:drawablePadding="16dp" android:fontFamily="sans-serif-light" + android:gravity="center" android:text="@string/grades_no_data" android:textSize="24sp" android:visibility="gone" From ea9d801d0884e9aca50ba378ab1f3b99f32a3e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 8 Sep 2021 22:48:35 +0200 Subject: [PATCH 080/240] [DB] Workaround missing event types after profile archiving. (#60) --- .../edziennik/ui/modules/agenda/AgendaFragment.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt index 8d228dcc..8607217a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding @@ -136,9 +137,22 @@ class AgendaFragment : Fragment(), CoroutineScope { } } + private suspend fun checkEventTypes() { + withContext(Dispatchers.Default) { + val eventTypes = app.db.eventTypeDao().getAllNow(app.profileId).map { + it.id + } + val defaultEventTypes = EventType.getTypeColorMap().keys + if (!eventTypes.containsAll(defaultEventTypes)) { + app.db.eventTypeDao().addDefaultTypes(activity, app.profileId) + } + } + } + private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch { if (!isAdded) return@launch + checkEventTypes() delay(500) agendaDefault = AgendaFragmentDefault(activity, app, b) @@ -146,6 +160,7 @@ class AgendaFragment : Fragment(), CoroutineScope { }}} private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch { + checkEventTypes() delay(300) val dayList = mutableListOf() From 8edc581f0b7ecbe5c2dafe81d019c6e31d4af839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 8 Sep 2021 22:48:48 +0200 Subject: [PATCH 081/240] [UI] Fix multiplicated day dialog in Agenda. (#61) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 827ae0d6..cc99abe8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,7 +154,7 @@ dependencies { // Szkolny.eu libraries/forks implementation "eu.szkolny:android-snowfall:1ca9ea2da3" - implementation "eu.szkolny:agendacalendarview:5431f03098" + implementation "eu.szkolny:agendacalendarview:ac0f3dcf42" implementation "eu.szkolny:cafebar:5bf0c618de" implementation "eu.szkolny.fslogin:lib:2.0.0" implementation "eu.szkolny:material-about-library:1d5ebaf47c" From c1062cd7ed83747f6c9f4a876ca5617a3e2ab823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 8 Sep 2021 22:49:00 +0200 Subject: [PATCH 082/240] [UI] Update drawer header background. (#62) --- app/src/main/res/drawable/header.png | Bin 62950 -> 0 bytes app/src/main/res/drawable/header.webp | Bin 0 -> 20798 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/main/res/drawable/header.png create mode 100644 app/src/main/res/drawable/header.webp diff --git a/app/src/main/res/drawable/header.png b/app/src/main/res/drawable/header.png deleted file mode 100644 index 365f66e9963f86211c9adb29e2c800ad1390fc6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62950 zcmeAS@N?(olHy`uVBq!ia0y~yU}9ikV07RBQLd|kV;C41I14-?iy0XBj({-ZRBb+K z1_lKNPZ!6KiaBrgX3o~}EL6)%+vgHC_g+g{-65Or=6CIlvS!J8dWdIEI3jAi=zHEb z*+zjRL2qRhf$kfQCQWT^%YI)eT2*wur{}}ZglBT|Hm0V8UK5KgI~VMqw#2< zJ8x#MfB650{C|Fvj$%H+pZUSFcclKF_*(O!^{3lwf9-i2nVu~#wLJ6JCzICS@y^ZG zQ{FsI=$};U_08a>v$ev%Y_<%cEn>Z{#tyj`A}+K|am;ndR^g3We942$lTk~hWC=s3 zfUJU0seI#W{c}&<{r46AnaVFHsHC;@hwG2i`T~VB*R}aQzV|n4L;p!T-gk@l*2=uO zoG^cq^^`Z46ZqfC-DFf_HsGAeveh*^Ao7Y-Maxvjr4vLnoB~#ySor9*^%7}AGGV5$w;*H1C9)<^awQ&nFc5pCE;HZ>aAn)vAr}X&d z=El7~7j@^mChv7zm8g*fMlxvGlqo2Syu- zxt`9xBDIw(rqo5MgsXMRde`9ZWxqY9(uUGC7D_L}duUenm zdGXZ4dv-Hfe40)&$MSRw|Fk!v zr&hlWx0mgoR?WXqc7fys!wmLCv$wb=2SkVcT5$7{`4S#2-PeslE4Bo4c{ZE>X-pRs znlS0*mEDTI{Pop8&O~2$dSgvVMB8RnmGTQ}8N!>YGlVuVZ{obkIFm)Uf3s_DU~~xE zl!v_{9rvz#uq^2iSy<%oXIlFe%Z7Vv*Dx6Ty!QNf{9glC?d;6jr=DIemGk)94+qVD z6)CXt-`D%>Kk5x`Jg8ZAdo7FQ>EfI39m~(%jcb$6jMZOCWo2pZUOr~vdE&CzSd~3mv%ASb7Udo#8T%9sX0!o_gPxj1v zb$=fh!=}UcW$k{8l|+{2C2}@3?!5V_H!te6{*HgVHqblG!OC&+1hYkxwm7CbWSaQ}W@^NB zw0+WMtghn_l>K_$@!_2h#pln|e=&`^Zlo&D%CIKv=9SD%oEv_g|F-YX))V{c;+VIv zPF-NM!c>Dds`B~yTQ`^lgm(X{OnrO%hE>6YwS4ocCR^Kf@cz72%b;{FZS&Ejz|!50 zOFnvg7j5Awkm%X-+5L^^JI=V%z6V^jTkf99%bEQ8oAejYcfH4M{%h8_82kQP;`A+k z0nr!OZhTAszU5NRzwU49tzL`E|1PfhbMv#Z{hZf#&u_1J*nV8TUY+s5F84($3>GnK z5;pLcEjYvQR!*%tf&IjP-zGCgzAX~W#}W#DYdpRla*t(&1;YWZ)9csRn*48{UtRXE z`T2&kE7kn3WNnq|b=z9^|G{TX-IE9RpP$aj98u+wUwY1O$?MCNcfUoZPs_b)E&ht{ zmd4|gC0Xx<>q|O*i7|R|w=qxJ!+&AP4U4O5zI5IYbm0@#Q2prXqQDil|Mm2r?`58B z{&@FzTz_|_=lk#yfu8iwIy;-y6pbB>6|5bSr}~~yOKG~T&X`--sd|)owWnqrr-$e} zQwJvncdg_1)~#Wf@bA^}Y=CrvhR;GbyEQSfKs z|Ec?aOpcoGye4nj>6~>IuVychO;A$SnXMa_SL$@jXo}cW#@spIqP64VZ@z72QPG-M zd!6Be^Yi}VpEWh#H@X*C=5G4+c>Cjz7QUCSnO3~j5m4v1pTH52v&3uN|8I?VcivSy zT^YOiomc+qe=$-2vy&J;KFE{#`08W9=5N7fuO2PaE}!!1>-sFSd2jcf+qwAToxA_r z-}I|p^yQqznBjeE!N;ok%8Wd}1!K6Z>rEy(Eo~H8^vFP8$f}XSkw5l7!{zsNgDBxj@|4cK5~@v+_@jJ6et!Lt zfRYPR-7zYyUA`e@{{$8|g}gAo!5DpA`ea{y{f{rR4bNwZ2LDMi@t=FVcGIQr5*HjF zW*Kuj=cvv(w=sTcqXo|c=KUTWT&jB>l}rD<6~57D-|wXtr`ttH-H1BgSG=y}_Zx*L z|4-HTF-%wSd0TvLckIjg-j|Jc@5=XlVtDth*t4P~cWX9Z~A}HBhFCMaoOUEMuCYF4BVR@%zHQeTf5Hg-&baTnjC)Z z@}#UgXE*;WTFIWW=X3kqf2|MC?^AHA7dud|GXK@mDQlM9JIvP6Fva_t(2jdI|2;@3 zegE|F^7|}5SDpOR?qSV-C;jUG%MDVKVr)B__9liltoAV7o3T^vuYS<`Teo8;zdLs6 z(o(&>cfU!;e&J|&{k*cy#J})=-KNItij2xL>Ee~bSQp`1rj?}PxRDt>&| z)fn30qBwC;ySBE&yyflw`z)<@?!LINzhm>w9J~3~&9dXnx*Q5Slz*5!Shwzfv}N-b zAMP)XeQl3x=1;B@R;yE(yx?1x=99&ne)><_9OSs+f0xHMM!%wc90818Z?5|yZqm5)&SH0*B7egnVdJ+d3c+>>oa@z2FsItTU5`jpSICdKtujSJL84+dgq#> z?S|E6I~XkfJ?B5O@B7kK`_^lH(K&tWabae6K-XdSj`KI7qg|>F?_$_~>wi-!w?u|_ z&Mzsej=U8;%P-H|+3e~wX|DaG@7>3Op8WaP82_|4*eR&bIhb4$ASv~jj_qVjWskC(SAM5rPP50$LhAoY0%c+ZFR!`j$Hht;u zM|YT(zPh(>{U)!=lSK;N^S%Dd@pCfEA^8xqAN3A4_RA8!r(ACl+P^*{^{^Xo%{6@d_;P`6!^t@Vm2 zm%pTY$nJRgeZF+T~RGHLvM$O}+W`{lR}eKNdUQ$>#Yx#n&Nm*SCxu#sik-fB%IakFW5z znHT!UZhyja{x!eLtTg6)+7q4|<%Nf#t@J2DQS)VMveKomIR&M># z8oQ!tlWa|znLeIm^h`cjA?KBQFd}4=P(tEw?SpDUf|@GJmP}-5IPbS;-p+r9@v=65 zr~UcyRO`iNmCZ+29jL8ZFBR!sc0fU!bBjmPT-T+Ux(o{%o3*_dh5kQg*llC8_wql6 z3G?M-Y<@2Lc695B)e^m*Z1jKLk6N&3Y4aY#RO=5$bM3cpnmr>l*RJHUzWCo)Papff zJ%9ILwxX|gu1;0OtQ6a@>6tjQd8v% z>9Un{7c~F#sXhJv-$g1G<{!=o1WtQ8S!QRR)GvmH|N7mG6}RVaN%hfXxxQch_Y^BG zmkznOr<(Fr+cq&VJh^%Ca{k%(dtKtDhBs?Jd|P9u|IluB8Bg82>oSL#zVCFkd%1Q_ zFo)IU$Soe1-qYzJwVhY_ z74o=vR(k%O<@K`rynOwOw@B@PA`+xsXwwRxweV9kd@7d`qZzfOt z6qlhpi!rkMnoxD4L`=3*+uUhP`?Y#jl?8gq_HRgT5S8Y{+$&ggH_$koK zSy8{NE|@Ka*C2bPr_Ub;xqs`oR4nCWJgI2O@?goVy3BfZgZ~Gwx6Q9N3p4-fx&PeU zWqgxuKV-&hq}JI!nZNio&u>Gqshn%(FKN2%e!%`g@jjK8;tpXq@15M+|M&k(Zu<=X zr>UNoON?fQoc(>NQ$X^EiVwfzSMg-d?YI7GrI>3h$lhnaW7;D>_s35kIC}p06v8s8 zXO*s2x7QClN9hZ3^=M@MMerN*^WR53BY+>r<7NyIcE-cfxXCzfPv< zM*gvz{M8nwJXrmk?}S>$_Y+Z)xthy!Y@RY-KcCp{AM#eVHBKsNz3cxi`-?xz*X92B zxtw`<=GFhg4gXd5onOYfcuP)!C0GAne$M!c*MD|u?bPM0G~E<&rai$xQe+>;!=Hlh z?Il-!e;xS$n7A^}3xC}VdC_pCB^}e3)?@jQR ze++9`H>-bG^&oNco@>7x)lPm>(pqK1q*x%>I^kRE-c!q&84C2IW$)I0>fI4_>~Qz% zu=)uXGp3|Qmj2O}vaeIzt`xQT%f-h{O~Eg;_WfTSQk8!64$H~UZwh7|`q!+o)ML;0 z`)WZiE{6YbT6@%aZ=2`x1K&Fx{TFr#1v4_lynlA_#cKJvHLr`4`pk`Z1Li$_Jm<*8 zd|{!KwY;fyH-8>BEDNx?WwmP2n!k1qKjv4KKWz!tUm#kodT!_8^n0`9|A=*V+`3#| zxG~~P+U8}Z6>pD!ux3*_#57GzjCK3FKL`K4ja2KzG4e|=v#U*&~=d|%hy%L2>Q5?%h>WG`fR`M$33 z&%y1&7E#krc|B-5EvoXJduOx=qnfpX^eHWs2NUg8q;(GKEeP;qpR#_j!hw4GMSmCn zW|6S}zVYRDJNH#O_`gB(#l0oZrf+I}9dBTNt2pRa zQOJQ3t%HyIYmEcvTK{}0raxo(uC6)*h(4@nQR%lys+K z>-9eziY%4#*cg6RaA$zU`hL&-d6xDSe*|aDofh+dtHrs7-#K0U=K9P zt@`ErH3wh%?<@RuQ+>uAs~5jkb-tP-tt+ecTczVXbw6EOuXZ{5f6d>wWAgt0_fN;yh(BH*eeuqpq^(g`AK7J3?|mUQ=esl$gI?J0 z@GoogU7uvdIEeo}9Dknub%{+BpPY!yuB{SUf4SKAE-l@b{{GuV2e%B)DNe^~xvW|p zDnyu{ICf?sV=GvPvYA(+?S-kfoM{!^4wjFFMC(U^k z@cgd$pCDH`)0L~``enO|4St_A_xtyeGv(fgT~kdxgzu++)^rVFTWlrL`0;<0-=)5y z%Z=|B{tti5wCmiaXr@kw+FcQ6+|NBoh_`rgc0s}4Z02KK8{bc~U%|67pewA>XID{I zK;^b{jX(eDPJKP?Qu^~^^?#M!zh`IZu9|dmXT&-&Eyb0uce_^|==q=GwNcD({brwK zPo^jeJ(K5Ox#+;6E|0)PS^L?h-Tm;my1jotWB6&cxpMtxkL@1MUy!=hL2~;IO_kE6 z=hpvP-(_w$?Q>0Q_SOHfn;PSaZ%%8gYTth1^Ph}eEq~c zQ-crx!}!)Eg3YU<7T3I({Fa}AFX7Wxf1iJEFNYqQKWVq+`MVm24}9b_YfNdl)^wr1 zYM+1ni-~)Zd#98|ao=CA6mY6Ai**IV%0qE!Aq~vaGv_O_nbr-_I`HChzR}ZN2&brF^;zmDYXWYgspA?-g^^ ztzPf#{&D#DG2N|SJC&>Ba^=(O?WIjz6Eo!t=3g~ka4k|qpZT7y=e{T2`{!Mnt#4biLqGD? z{Fhg?Z2e^4-94UqJo$=!CiA?nlVkrsTCkwucgj`yFH=6LoS!sH~pBvs?o$0sybgHGthILQ9gBvV^6;pmT*~~kq&$)5-iu~m2$4|-_ zcQ_QDlkHpf?woT_x0i;z6GMYyXUsEK=M`TTacz4(<4gRE`mU%aapqso{Cs@pQTlzm zpJx`gIJ^$pX2R$<-BRthN@Co#AIv749PciR-w^y9e`!@G)8*!GoL5EKriV98+V-l~ z;<=yQ-xrUb-w)zdyi+kJeD&1|o4)?!qp#xkbF5wc-Rb1?g86@?Z%AuBk79^g_3zz> ztmz8kB7#zV$4?(*zGpty<7wgPs{34LnfIsNGf$oQsgcQMV)JC9w=91SpPSG0`u_Tz zPt=yP_uDhQTy*qRO04m$^+kFw^^V4|#CxYT95edQfBfpqw~||&PX6xodcm^K-{oEc z>x`=NY|~Odettiv?$`UI()$ngMn3+X!zp-p)+YY-ttKb0|2R5n)y@ki>nscUx%d89 z`t^Uzey__$b=#i)7lCA%D`5AIuN%~Lb+TXKYXdqIZxQTr!gQah$ zj1m*`|EWHKFQo6`*o{l9yOY){W$X9-+IlTvnNQh?DIF67d`u? zX^UI*NReUwqM*^lH=1) ztA4JXcJ0Bkw@vkdKhFNSzuBVxQD*3@RBO$DRTqnzf9+>u-uoi;xs}K9_cvJA-M;jo z!s5)gc;y9CB&KTc$!mT&vE}@NHS51fmrUkM`Lcp#^0~<;JtI|LMAlqtJpJz9w-2@T zP3I=wnvhsx72mY?F}Ehafv3%vVu2n1nAgf_dGEWFKgG{caost?sit?*`=^BpvOmuM zbNI*W`K$P}Lu@P=zu#Q+`ubP9AKgdf+c-CUZI;@uKR3F1CYGr>h5tQ%hJwN~Jv&5$H9oI4Vcm3o!Z+1D`K-5#=ghdZ-|GDaFMhfDBfamg zs?9p~`e4q)Ww)5?T7u7Swk`H;RXTsJLCi~oG8J4Ej@GSRqqDmCzKref zUGnqrdW>(7Vl>cr@mJU=6&KKhySQ5nI{npVgdv-*cZD{jUGYeZaLn z_*_9)&h;Ga)uB~eP87bc>vx~R760guVcXV81^*0`d-g%eMU$JHX zyUBC@J)Yb>zh?20ybW)P7hKhvZ{b_kpZzkxh+%fcB6GbzlN!wbJ4OeIO(|o&yMN94 z1zVieD%C_Doj1LZv_564P0gd;yc!qTAJR#8(pMJFQ4-kSw@o1Xshs!uS6^!~%)ha&)DOe&4Mz4OSKpIP84YRzJB@7 z^1wO=$xBilDleyZ+B=K6K3U54Lh-S*Mfs&(CT6M0R1PDgj9TFB{7V&8FUam3$yZ<~(^sQ=Y+ zzSVFs=DDoXL4CjPp}7G87Iet)<7sF!vngE4I5 z7L~tA8+ne~^Hwyo*q&8qc*0P*COWV5=+>!nb|1r4yN}k)$d16(S!!SfanXH zFC4xf+aNFORja6Ze5L0K89}Wt4kAuEB9ob4NLwA<`S|<)$X}N}haMIAy^Jw&=DKA^ zWNyXWF=Q{DIk1l41DE3`zsa%IcH8(6v~*D3Ei|F1Qa;nU3zA1+>hl(msF zy!A!leBW#Xw{7-o*0R)GSo?mN$emYzXDf)W_FcF0cTW4H=Ql5wJbY)PP#}`6*;2Fg z)^z@#zmi%`XzpYEnC|G$|DyYE(userea!J+#qVzP`Tuvm+xgm@vuESe=kJnn)BeK3 zKPSvq!B~NNB{JZ}~6~pa$r9TgEU!8Dl@579( z|0kp?MJHEJ3sUi_xPL%;`JB(Lt`qfJ4|yF6Ip5In;I*KVQu3Pf_piQIUVZq!oZauR zl69+6y)I0-s8m|Ue>RR|?c^E%=e=nZZb&}G9{=*XvwiDJ;igOd7w!HQ|81UK^SXHP zbGui6jZIB%9sMPx`q#|!X$%AZ=YH>6#vPxROrPg_evad=@2l?rwcqVpdH0k~%tD{# zlRNi0mz_@*Ub}pO>(l-tj*cAnTy%Zb*3Dd@7cFb|kImes zqxW=F`RAOOdS4H$U$gI?zIBs|sDk5L$FO}(X^tz@=h(~qo67aRL#svS_p?xDhK4sU zo*rI)zvm~{#u$&CR#zCmN5(ea7H>WAzQ4m%V{1Y8Yqh)8SKt4;f5N@KJ^TDm`3qOG zO81ED^Jo0WEo3P8HlK&{jr)lc0YB_SkG8H@vEaA+hdW;umT$WIuG8Xq+y5_L-W-q5 zU*#fk=j>`7>E};Y+S)GN=4l|=p#S;N>Q$1O%h&u-5dN8@&lU2-r@QHuZF9)R9XX{R zj=a9}LZc_!`$4(cY+W(d&q2YP3kvg&eowsC{=rh|SZ4h7;L>|uI_c{qy}2>ZV_Bc~KTFcDt9@$FRU~n0 z#Z=>|zZA~#{(t%MC;$JYnUTHxX^~+^Yp?BkY!Eedqe3lrw(}mgHG6kVeXX;8-K+!F z|HT%6Ofjr*;5czaH8)V*?&9=pfvW5KuN^rdzs_|j$KLPpO_LiZPr5MizSsKO@wO9c z%(cKKgs zeZy03`(58ZmbAUZD>&5B!`y&K`!a{fy9tQ-qEXU>y66#XIyF4!rvDbn)_gN0w%+X47<8m0}V9)aOUCP%3){d&15akL9E^moh9o zVJ&`zCv?iu_wio8Tq7eHWfr_%JZV`LcrOVOgo)wU+(*{QZp5iLlkxr)Ai`8(wB^SGri_bLn?Y*Z;sS z{ol(vFKTK%u(Q2wY!zU*YS-@-cN+xD@_w|Yr{Dko$t~FU^yk8LHt#NnS8QkAQk~Gk zerWoyq+RvAx5TRzKRxz|@e&RwWIa3M)57#PQ-%|pzr5%)f1+NVd^}1ZBK*SVpL?v| zUEZJ2?!Q6YS?R{DqqCQoC0!RemaS=WTK;O>s=x1I-`#$CEp}b;=3o9FfA_{_?$7?} zdi?p~r?U#oZjl-<5pzSyFm{jlf0{{bg{a=q~X;KzCX^BK?mWxekwbML7r=}NF& zs>UgLY$gA?(kZXgibLJ&e$JjRQ~PB7#=cmEZT@qM4<9`h*|(bi0vAh%xnIvrNNwCo9=@~t^mFQ|Iut;Cgox4hgJn|JrAmHF!bR(1D_I>k7b zhs3iK%5vpgjWf2}{@B&bY_-_~<&XE56}K<`m{efTD#WDu?a+%U0nd(l-J0H_8k)H1 z*C~|)jqyich@@zXP82xE_~7THx}O(c+C8}a&q{i!&7RLE4=(P1P;^Tu|HGM3?PEjkyaTUXZ{40>`$u-k z{+}=R^8XiIq^!5}n!_?*7ls2h^SAz3E!N3!a^ZZ_+I3eWuU_O3*3FaQFUpXdHTBl< z>p6i_zA}ep-p`Bqw)_9Ck}cn+iR-@Io);N@?!vxraU1XD?>wEf)2394{ddpLhbAHG zC$;o4Yzf$3_+Wa(V`I_O3%?irO1?e+rtnVl=U-3f{yJwTy6Ye@zjtMH)B%7@?C)L%)%$-uUq*k`|!GFgoN>58H`G2JL)$f~exAyzt zJj+bayIhJh6#_s5&03-$JYzqsEk z{!ga3#QChPTszKdJ+Y5tyI|iEcRIL&@j&}Z6Y=EUu&?*G)wLE?*GYICw_c|wJNMJu zyf3R?)d#QF+wyU((ATVQyZJxflx$)y$tXHOo;wV@<$i zgD3GXCw$=!dUIX8^1Er>&TGmOMXDJURJ6GM1kFe`_nCD}*LrXDhbz(w=Qrr-NS@A^ zmsWQFcs1+G{$pa23_)+TuLK(lOnkv;_^I9Iw(&xpEji!v6KZxJdi%aI<=fupZSfV; z_Po=}{8q7LZt?Qv%eUGyx)?82VR#Vu-^PORd#B6S%d4z<-ZDr3emS$gyD$8x7Y7)`~Oto!_1|>UtWE>v1HHhuJGNL+C+D3tA6{JXYZpO#nYny{?H48Pqa2)*KoEThdyw%>M^7f~z4=3lJdcXJ5lq9Vvw!(Sq zo=x%Kn>(?{Dn)2h^~6{G8uCnQ=gB87yUM(<*3HHLe?Up$>wodZC;i{a*?o08vw78{ zby+__!}{5}vx5v5Y!Y&q$kcYVkSG1<=4HEUqpCMl&3(0P-?y@*Z|gFGZR7U7pZb0O zHhb~A|BD26>^}1LuGzzzYp1R6t-W2wdhD6O`p?y~Ht>At);ZnxT+-%)z^n(e{c3(j zu5(Il`RFU!GKE!kqrVSpP~#`YiH-lC)cm|M@8czlg2na!E_|{77a3~em&tqLbIlw< zJ|X`vViUG9AN*gi!&NCcCk-HRM`!ZQj)q zf!Tk*UTqG){eN11N#S4n>qj!An$$SEww5Wb?F-!Do%?&*?`v^3HCHyBxng&r{qV%y zp9PZZSmq|_NJu~0o5GW^@{9MaHG+S$&-(D6C^^FPm*wHF^w0NydR~(*oUrf5pO5VK z&(+LVz0302uKl7B`)>ENfPz;aow6r?_1Mj*x$4XbdFEN!wpD(%F7m&74Yo4;z5MUN zofqEnGittf@6g+jx2O2F&H47jiGKtcrd(_25?E|gTYYcIwFh>K<#NsJA0P95?(|vD zCNF;V?_GCRf9qTS>|eeh^9rx-f4qwWc3&{feE5$2{I!j_>)SdGU#!S{Kb>j6Rp*BP zhg|s2IIJvXQ2W)QAKy2}d*X)RCRq`M7W*1*@xT@LV>;`Dx6hHa`DLK9JVHD{#XqpW zkVv$9n)0`7LUUX|aedV51#Y}%aF&WE;0r(E6sJY4p+*3573 z?tb|f-%)sF(}KG?k$ThAC3o(%&Y9oj%Qa6vm;07{YGC9Awv!x^d#^2+qRh0??a(|f zCKZZb$!F)cZXIB zhWz+B@Vft#{79pSIm(}j1cmGJx_fPz%J)Qsj zt4g_BdfLag9Y24$DEejafBkJLPky>Tv?)60s!{&)VF_EgT!OF5qy9eOi8@*5+^;5Z zEi%8-;)$44{>O1PJHdBon-?FdHIp3<@a>wl4{L__c=QYpmeBA%n_t$Fqkn2}2-C#P9 zwRM&5w;)^X82QvM*RuBg&Yo9ZbEtRQ*Nt-`qy88?-}A;&x@7-F*YE$|a5K0i%vPKK z`{#a#@0X+h{7c{cdGAAm+tm}^IA{qLt(wm3l0VJnqO^o_*WOD~Qy7gzB<@d1>pP#Y zsK_+xTIag*Jtj9lym)cad_wfpE54>|260Pf-u=F7p7%dKmPA#a+tW&dru_P}L7t4pFHA|Kupgv3E^yasB-njoa`m;QJ1LIM7ZiX$NYuoxCJ#Cnt`G3|1hK6;z zx*yNHdRMisZspo9hqt|5D0DS@SI*0$tY6cUe@F3_UHoew&chHT`|H9%-)m2QS?Ax> zJ7?)I!!JEq=#Dz)#o6C~uWuAw`igsn&~4-EO5rin!aVLbD4p+j<_v$g#-Ss%p1&^5K;{>}&A`Cq%NG`_k$`s~MBr`a@P zPpwVc3K*K4G*TfA%U0^cbYv|G2VKXG2|h}WBU;wSchSXJetDZbSZWEe3}Z zee>$F%l_Qn+Oz(vs@R@Ikvd|RUv4q^=UdHcZ=0OYzo7k`Qc}+!&CHnwi=($BzRaC` zUP0o7>RZWqGqX==ZJ1*Vq2{{k!MFzw8@AMjVWG^4hNN+`MFe^}iR*`RXmb8@@bLDlhA2{Fn0Wkko-G{0b+pF1)w?pnxm?4Edum z`HF8(&;IXlZs+Mme|EYQ8vHtS{Mh|pYgTdpnRdj|T!t@tk8^}LXG4bk1LXrXTeltE zVl_J}J^pg%g+wd!t?w_*@146d?C<(}Uxa?Gw_bI(EOVFBO7rvyS>Mg#cwc{0z5Cl> zR*(2e<|#Y}_&nIp`=1Ert>cV;;=TE5et;yS+9ah*>omSKb)33vr(9&S)}AkmfnoYN zKDk=mGj3b&adzA}d--YBO7;}C6t)>Ztm5B%xxR<O<45`O81LetG`ud;HWUdq_*l!%{^-^?=Jh*$(N&>9_9dinI6c6-!%u zYi(%eiuBDli*~Mwm3}4T$k19nYkFhT+vO(SQE7P>uReQn5)kiegvU znI$Ivl;g={KIQc$ytTHWJRm?}xr#&4i_eoL9LV2N?e>#_@0tD2V<*$+FDQ+v3iV`s zU~pF9=J%LY`<{hQ;y8EedZ^mUFWCXo@m`IqHL{nhEMR(ZSnEvqnYk}--e6+bG1>p$ z+fV!Vg@@k18d|nBsx?Xft#0!#_mGXRzCSttM{%l}_l$E}KZ{F+1YKT!_v^CvuPWER zFaEaw_t(e8>ns1?PGZ=4YI}*wzv$}Cx8nDf-FSy*|Cm3yD`#Gr_JlC!-ahpgc3KNqYWbQu_S{OsRf_-W>GO{p_| zJI?n$ie?gv?5q*kBfetB3{}x<>m-vj=ibmu-%;;{{7>% zroR5VtqcBrtcW>OyX|=6PEMuS|6feC`?3G_$_>#w7j8POYwT8Ld*z_p`Qndj{qi$o zew(DwAHRpg^3~s!i%g8vn{N1S^OxuYzqTEachNX;H)ac4OLO>_z4;d1 zXU@L*pLfF{`)hT@tyOQmpO@_ne=B^mL@&?T?a3T#slL1aoHY&{dGJTGVEz;4{fB;e zGX8w*ysyyo)`6eGh0nj~y}Z6u|NpBOfAf_~q;1bFPJPyLv%Vp%;pBnOwPL4pSFC0h zioB?}PVPqCk_Xn|7jy#tw*-A#ZU5*%@BVL{C(Z4BXBW(8II!dVrH{*M8AQCD-Y_XO z$S^pp+AAJ3_mJx0ywByCX8&e=ar^q~ZFojj*xrA+|Gnk6?rbnS7M1-q=dI?|=7%RL zb9$#0TgPAPRhztXFMn=RT7M>|N3^1nUe=xh9*1T#hqf(3JQ+JobPfyLCJ}t9qlath)PH+B2o(Cc>xt$8kY+P(` zfBA7Wu@V!HTN9a<%5v%RdR>j*dAjuTpE>0>=34)}visToAAXa)xF$XQc|}ual}7cG ze%(3tD^zzgZBabAnj=v(Hsvbk8+S%chNJykh3${=PUd~QKT6K#TgU8qoBH>0emZY7 z`>a?u>yMc?{uTd__6U$XeQ(~@gKsPrTW57Hdwe_VK$PYA&wsSu|9^M((l`C!TYKi@ z-h6SX{nfRVw``w(I($*&y$y?*qp?GF?Y8Nxnm-TClV7Onuu1S~vtCa9GIeL3gH{et zC1xm{^t$ftId`-D=Kp`bom?*e^~A(u35%H}^orA)Ue_y36tz}xWQ_c9vGl?;l}$+j zGwisPcm>RCKed)H+-xm0{vN9P`2B&oHLvb=&#&K|^832iv{dPzcG6-^hqzgi{v9`L z@LK|{aI)_6Pd)h9u6FPJ6?c?hDlr5cs5`-WnL$5m;%=>z>gw(N_D!p& za7+yfeJgygPyFVoH=3GnjTEH+{A1?$c~$VF5X<%beHmY;u!S62@>X)5Ywlg2xA*nB z|Nl9+*Z;5lq^oNU+q-M6Rrj60mAW9$Rzas=zTP&bzf}RowO(S;0ij#_&DZT#dt~NY zwP0oK)u0~h%O&4$|0{p0VWd{{&;YxB}x@lvG@l#980PoPRX=ob3MZxi_`-1w^AbUpc6q4z+XEjWgZt ze&%~7yUGjyvVRo?Yo7;wUAphcpYoi4zpdVTusoQ#Wn$i<&&y*!ojmz4xj$jssugcG zMX@)0tT=NtZq}!Dv-N~_sxzRK0pY6_Msm>+z4$NJ>{ubPm=1xHNp zEEh7W3jEb&y6?+|bVe`s$@Rb6FJ8Bgyks6~-db!B#mL9D?^JSAnnTciL&oet<5%_% zcb&SoG_5I__gcYxjgtTOW^^xjXaZ!PJXv?SANu3z@zeM?mtPn|Bd z;9&eL9ChvcYQ=J)*F0x(b8p}3pB0zCCo1>x>`VWbO%uQMfAh}eh1zfHGlO=wT+{1! zKO9*1oPWaHi)JEe=2v|HZ4U2_^BW=Q2cQ*ehAJ^KIAgs8&|7 z{qgnT=6u)l8B(j&BQ6`6#H8$$JNx0!hd16!&6d-2e_V>{!lw4|?qVKeb zSIgP%<@P4uB~AVF4jDBtJWye%S)00gqwU`_!rk+0m3RDGy^2+kQStm8i}09d;%Da_ zul@APN23&Tzt!H>->3@UsvxvVR`-1sa@0M`_&8X*Xw8b8OmLZSXT34={}d=?CZB2 z2xPj9>Pd9bbKad+aXrIGg`>C;RW~7Ra9X@}i)`GRa%k zXBIi0bXfFy(*L7@V&~Rfc+jjZV8%Ep%5lMz$4;jdlDqAe35EZ%eZF~PwB_L+?Z*E0 zpMU&We*eaSTh1&DmGk=D7OUL|b$xmKocztXi+0rhaQ6JCRl4t&?ycqZ$NOi^yS;YZ z<)wdl-~ON4Q(u2aL{`kXJl`Vfoo!2}!KDShOTOiV)N@T~sh{~la>_TaRUZPxZ`sfI zUw1E8bpD;N-_D=c@3VVYuKBa1;`>JTgsm$s|H!UN-*dufv-+EPWeoG)Pxc9XFJ8j% zf5ZRd2@d}P4*qOz{H*>f?z-3G=}&kb_jjDzd9=Rv{}bW&TB?7;WPknn_2kKspA!<+ zUU6kQvAT{!F`nT=@&02!i)U_nJGuVuyvEu0{%_W2kT!3)>c9OKOK$b$WnV4#%scxm z*+=81dfJkS7Al|jRkr(I@I96g^W3(`r&?jcj^)jhoZQ@U<>&9oJMW_N;=zyF`e}dv z?O%0a$|TuYX+LFp;=|azY;q31w^vBMAn_0z^3M$ub zG8%jdp7f?>Zn>z&)|=ndqJl-n}>5{zre^pB?z) zo7qBh-LBn_UCT6%vd*=Z>-%iIUNQM(uz>g0a6zUf4Ua{P%=uyZSS9er9p)In~5>Riwsl z?zI@ddy4sMZ)`kP#Cl6!uO|K3vj6}5A3wJXd-OZTIrJxE`Rg6$#ctb+?c?76CA%Ty zm#Y1?t-_lh-+t8{wQ`MTFMD*}t-9&A|Cz_`T)Fz;(R0tPhV{Mio^~YC6U+jW`L{AR)Xn^_=M7r1!`sjDFmnAbLBZ#lX5nr! z+4opk)EbZM-^%%lrDFNgon4#Q)^^rr@44*c6x6rmlD@#Ec}^=sE3O{im>ns*ujI>@ z*^Du3F0W2LZ6wvp^lr1~<6u=)>26!b|LXI-3Lh@6yf}M#_q1^T|K~-SJL21ypQ^O{ zJhfc=bV=nTCAHu4KI$ed_~OIACG7cWg|!duE>8dT>4LwD%9E;=qg}VO-}0a1wf}wP z&GGo-R=-7*HyfV5YZ1J!W0`8=-z_P^mjn-aMH{uXyubWGVYkv6r%4PwjNc`XeY+L- za&G(`E!&zuygz@;h$`K+X8r3O{wsdetU8`tWX{kxPhk=Bf?Fcz`ZjOH8F&(C{m&IzoEq``d!cpF5^4PzzuSHD>)OM+b!L2rNtBhq()j}Ff4i=}|M|5g(X)A* z)8ddRzk-6I+-0)w{cPsid#vf+(pt-c<@2l4zAawARpYP0p`PD8rL|YZ#p|EsANpoj zsgb+U#wB;xFM&tjZlAkZeD32r+1EGTZ2PaeGLd<0>9ZZzjw z55{)ASDm5zCsys(nrUgq4_gax;h%@BP{=Zk_kLVc%Un;2v-JCYyuZ|7>o3KhIW-^N z**9HXv{XcwLE%u!POqgmxQq9;YV`|cd}I~XVXyPdOi0NU4c1P*qNH^7-`mv;3<`gr z_WRrY3g6nDZL-aXDdx@6%x;F)cJeD`#9vfC#n@1_Sa;qz-zc>$`lf9eW#u{irClpu zE`GP1h4=Et{~srY=l;(YYgk)SGX0q2`|0yezcaVGeadk5S+^MpMhwrEYI50%<**3; z3J&<6G{;CQFr%Jh>Ze3c#^vH_|Cd&;&@YoMefOg|-#`9W>!MAU^mW-Xr};Zh6#0?b z^*`WSc7V>7TU?*qf8WXyd^|fjsF&mWOTC_tx#5q?=DmKh`QqL3|NarW8}kyp3-6vx znY1iZ=DqIGy6kX1mnRpPUf0%jZ%Uk+H|5v;e`Sx|buaIH8T6KU$JM0?(npJK|G!kY zDmK^b(S$?H;kHY~G_RH{Sv)6kUutBTN%W(Ce^b7nP}49t;$WL!yKcR!TK%_soD)u@ z6#OmTD1QEn!;=Xzi(fM=Cf2M@AGeSKWl03GjVz-$5y$@ zInG=4*L}{@S=G|DML*tYW=Y_)=`BAOa;$p%&T#8)sqLF%+n*oX%m44`tEJCp-?0#w z`C;`{tp|45TWek_Fc{r$e!sAL3)ipx`;4dF+;#1$@zD=uPcObT+Oz%ddfV*V!3+-h zyCZg%vJ{>^I89t!m+#KSf*J4sC$6yC-S#t4$Sc`l^XvJN_e%L}KVARZ z+pP@#SE%L2e~@LUdi7^!-EZ#0`t=jK8gpXUnBHyfZun!hM}O%D@vbC&qZz){A1B59 zU;b;`|J{?;U0$>Dcwv~+w&PIs!u}aumu~-C^tbzg ziU#kJqmBzj5(1|hxE+n*wD`-U8ZT?}v8|>;LcDtAE-8+}jYZs>RMhO7v)L1bo-#8` zVtB(Isa5l{LSLiocT1qF1FM$Tueaeot*Z8QPd~ggUwGQPYQM;#r`H!>)HuEAl7GNo z-36PZ89Hhw7b$#E%-dFc^WP@E1ar6VeowC6{BOK!!v@clwew~j5=^i7^eA}OoOg4) zr*~Itvsafh>Dc=H;&r{}`NK|f)}p{ulQcrQZoJ*>_oI2vcj;Y)f6w;h9%+=WduO{@ zzGkWHC;yI_#fy0=a}NI4IQ6JP*T!sSryq~KH8ajiGMxFYIZs}RrM2bUF=ps;3VCty3g3cw&;92de;xAgxBtZF#N)YN^~C-Pbpf|-nI8Y|TdU_qDRZu? zzv5}~;OLDe_rKgb%t8YDx-xdv@viA8ULIfZ{!M#)ZdBysh37ukSjbG9#d>@Gdhz{7 zlMX+*@m_vCOX%nO-&b5b9W(3Y-F@-PkG<`_u&w+}{Qry9|MfQikDd_~YR#m4*=`Mc z#KxQFpZ&BvtYPu|9dG-~2aNGgPJ2l@%@*n0H|O>0BVXT7e7US3AYjUuZjP3gNsCqP zb5y4;vSNtv;gkET?D0DC+2f-Lv!1Qwb6HhA@2#a~k&eo~q?_B_PF=4Qf8yWr%l+YR z7x_gz)~8m>O`6-){B(VN)qCal9LkZUe~%l<@ZD_5IvC$N^F{yBw7Q900<%J_L+76U z=q_#|WIg5fzxQ6R0v$qc-dH>B^_+*=uKZubUf-^(^M$&S4hxH*BkQ$n?4`^x=v2fs{GHxa-H};k=#0SRnOkJvXWti>g)icg!BAd zLLmq26L_?`kHnd7UiVj1CFJ-sdxnIGQ5VGv&*?5NzyH^L!yMDhKM_;?mtTHbB>DQ+ zw53X09_8py=<0v)_h0sj`D@v>x!Rg;UH|vh-Sq*xPFUKqZ~y!LUi-S>yvI*>y?DJj zH`nT$!RbwpZ!&$C|6r0b@5Aw13;rH`x&Hdjj?$RkmdSP8$IKmAS1>em%y}*Qu)p~7 z``XGMN55}dF_Xbpa;?ma#`W*41?Kn9_^)_u-jAbKi&pA?*x@fUeU_MyZh`322Y)&b zO}Cx&n18==?Z@R;y|O|r{%38K>OX!X=lAjbM|T~WCoj(4_Ve-DEhpDKIb(j8Lu;o@ z<a*!`J(UW-CYP3tGeReOJX=`S#?l-ktr>$_h)|9^L< zkPf50b6(HyeD&$_;lt{2{d&i(>^4OR_g=QSo|wbldi*}m0j@{FPPV40GyR&KHj1Y@ z2(6OzY29@=yNYzu!vnrk4x0 z&wCoX^Udz~KimG_wSQap`Vi}tx}$ns>#OH)`)Zw=)xY>;McTWL=f_nt*jEUz|H~`k zoiv#(qRL^}XDNYI6W9;WFTZfeo0*~G!senUdZ+p3SJc*3ZC~}vdsD>IVv~7Jp&VaN zsLkLx!87IjhQ$s))`)2I)@7^iGv#WG=c$>`z|e7F`&3Z|h27-^AC9~}cr`qqpNXNv zLaH}xb!cg>#@|+>ca!(bZ)YfSNvv+OI@MmVTTOFi)3;;Mf8(U{?=CI;-hJoW{@-i6 zJInt`?zk%P_QR>nS@*nUdEPNhm!B9EKj%;Ci8aA?EKAp$PDs4^K6{deiqPpR%WEy~ zGq&#y`p*ZRV6J^&fBm2}dxGDE+5aE?dVVyNd*!MJb~nGSx0q_R z9`B3Oa+r=8@$UWpC*y+D7S*M!GyPa~ z9Sijsot=pZAMzf#{r}VWvi$$fRlB{J7|!i{Sir%%%Px*h!RZ@Az>)8q z3T?Vh@9kc3K**dso&eyFz^jcs1;(xcj+ddku;?tcqZ32xk&i{kyII2(R$- zJ0IQuRee0vpY+_nx11?q>Tb_<4Z@l}A@^2yFhonHKlaw-Sj^TKMll z@;|5V<QJ*~#lzT!)Ng5g_%qoQ}x*Y1`7uNUzAlsea~{X44P_H?TUP8NE(DCUv% zQL%f+ioL}Wf14aHO?!O%Uwp=Y{UaN4jhxNOwp4|kwwjW9^I;C>q%YbhO7iFY;(Fin z+oZ?cBqT$l-)DJ1ht`HkiVhBccmJEY;Uflns z!5tp^ZappV!y!ms5A5*2=H4bcc_GucV3!6*(G~TczD}OMivdxsl76?Zy&APqoNslbR*yHV~U z)rd8BWJFn?_sb?2O_;Rb)m1i{XA_sFXpr~u!>fOo-ajOMk)>pJ!M~X8swt&a2Ruc$ zuKP17O}_tl&+i`lyH8A+T#o-|RB-w|rPk%4>v7JASs4p`-!jL1%AB$;<`U1ZP|bUt+NtB*I9T0Ea~x^I3}aM_>VTNkcbeS?+Z z^wT1%xk-1oE%^1i;^2?^9~;)CmiVpa%ledXMAut>XIoN$u5$3)qq@sBu4WO;|6RZE z@A{Pjd#4uUKhlo-a;NN^Z2z%$=bAIvn_Mq4U;O8$_4kvJLsWC(DSu8T;mooh9k1(q zKS(y7U|baP>s4shq~gk7yPvWXPk-)JQa^mL=z{{MKx?f? zo6TEeBdtK=ll`u#6L>URQyg~4H{}Jz+!y2!6qyk9eCbUCqwA;kZjQY4<=y?i{^s)A z)Bh+v(m6Zr)P#s%%$nb%i*$0{{4rdjtGWLO*Sp|_N*_G;bFer(@D{atp5k2a_lb=C zis*mu#E;AWC|>cw)?sdUy@}M*N=w@vnd=`+wR82{zlY5w`9=IT4YeCjf^+33eEW9Q ze|C*{tlipErZ4M%FE`)v|LLL+q4j>^H+M|u49eYbRrdMkn;a&5-wsGGVSU(t<@eJG z>sezNE;(jiXnZ!g^QEa)QnJK?6|Q|94;ZedUH>oX_A={8BXjxt$M0kQy>d5vv%E}r z!>PMBUMgKQ-=KAq>1XpSPQ@Glg}XAcj$6k2{8)VC;o&*w9l7Ju@1&pRzxAubYI}@~ z-7kYP$KO`eN5-dbKALoKAKP>m2Bj+9M#dDYz7@d}zs&w&QLti- zxE-#_)JshHiQ)W`?FbG$xv zmGfV`d$GFF;vqNVtF1gPaW^j|Sn0>^EBZd|?e+}wa!~w7BLb|s`UOE&#$|8O(=1f>~}Y=Wf$&qi{dyZzff`WfdcgYaB?Oc79G*&VRExKe;>bmHOnWS*ETMXUW{jXl;Dg8(cs0b-dOqkFwU3F!7}#Huvn0T-|vh{N&g3 z>(^J=lxgxE*!kzhOnX26_#FQgX`Yv-6wVLrjsJc5#A8l@fdBs(4oq9RZ(FI_7IVA9 z$NtWFe(&4=KeZW_<>^}d*43qlIzs$c*3O!Hn!i1{{J!%~K!FK*UZeDL-%e(k*A zw|M;)^FPWtN{TB?uiKY>3cRBbx%bbZi`D-{TCc9vJ)g0s${x5+xQ26_c~??eadE#SXjew{u5i5WTuU5Z@Mb;gH`$)tEwmZzgFqr$AcT1`+feIinDu}>~KqMxt`d? zx_b4m(ir74E1yld<@r{hZ=vzSSzF&``|o`F|LOPmW*MIAL{+TmH z+T)w#+(+GeTVj?3yp65i=b!lfh||UIyu2U-`x<=G>0+*8CGv)r=M{?Dx=^*}Ue1cR=Hx zkBOeHD?}D3)SqCl<6g%8Ce&4neZ!CQ`E%=j-PL{Nd5?)NSitzc+5^wfR7sAK{G5NM zGgdF^ND~Q3*U-KFk8kG$K8pj{8zkPv`PqN17dv0S+weYpPkw_Y*1!OwT_vylv=FJ)Lu&Gn4t)qD>yBoLAMj+l9y z%Y##X{wepf`}!_Ob?dSPc@KW;m}Zx;uwH8q(leZ*^Y>NCO^vtvT_-PI@RI8;bKUct zfa>qlUf=n)|Jx?tOD9-EzP>EqDZgdb|Etwk%^o*D{#df=yQ=I-6@|c4EIq1Y3v8{r63ayli)Om&b9=)0gfl_S}7Q?e@R;(Eq!`SXSTD@AG9Z zUN5qtubesBbJnu&(A`O=-Qx>*{TRc$Ndq=Fb4 zIQR~H<(}KrFuDHkl^^l;qS}9>{8<_Jf*+VpJ!mf%W8Dz&)4RIJcCN^ZRZ1ULPZT-2 zD>R#bG(_$HoBZMs5!Uu@WUH-zq6;Xr?4d~y|!4r&LAM5F6-F;4dNb6 zlb1A@h@4^J@nxKss#n7K((268xt+~dwe@A}eyhLuX%`*yf3eB=-sP1KnDomU%ov|l zZT8go)^x1u_$iYE3DNK^#Yi7dUp8szzPhS7uHf)L5@yJKNdur{r{!{YP zeQ-ek*OK`9ty>iw?<`$*b4#>%*;UC4NmsAdiOBQBFZ`<%yX)@8zq$XTf7P$^Nm$X) z`-pwZwr<&yb=H!+mk<7ZqM5;^QFZWhPR-xK<|yTv3|@>u^L{YyF>)um&&gn zns96B4jtQyH(T?nE_La%xXf99hsn3`_me03%6bZ0ULTDUG2SPsSUdT~dN!pHs~i6s zo-|CD&mOccKAD^0=H2@;`=0j;%1k^|x?4`mZ{EX#8L5${|1Ef@Q)kqzdSv#0Q?;lW zCBFQHrBc_nt=a2$b?=3L+pg^U=C*bDmNN%#|BH|Mzdf4ak$&*2s<+)2>K0$tI#FXM z|5WPzoAx)s{eR_MPRRHF4mtDdz=bIrczzUx)Uh`>be!_!VPKS)b5C9Oso%ec7muFb z7aRI-<6cjfoy~=PGgLCz&aCh%b2ye{q>#*fcDeuN2~k!nL^F69eOGgch+q7&@xD^V zO>Ga+?r7ck$~S^D<|NJi7+1ABLg(4%%D2mXHzsK)?PV}(&RSMdn&x=8rGKewd`X~~ z>Eey+E*9MVmwmDPqi){sdf#9B6B$>;#_hV3y!eV9XXG?l-)A2V)K9nCVy0-!ux!D_ zW3ScxznxrH&*{x}%={k91ZP>sOO~dc*75Taw$@tz7vE!2$nf&~|Hn@zzgKEnuuw~m z?Wfh@D~&H?lU^oWJ=qcsGA@>FWFO`z!Xo`=fbg%PTwUJ!|jYaxQuKC_7|9`P=1F z%07g;iq(sh>*qC;I(p>2UzD@(_P=lxi)G2hiT^YBzz)ra1i*Z%c&+_kmaUcz$7RusOC|RNlOrdD|!M z(w{3<72aAO|4U=fyBqUw|C|5$SHZG><#RV)-MaDRZ^$_T_1?o#tCULg?tn_WVP*GkotDs$2l?+rbU`D>4T{`0v0U-XCNH*-opyf{2TclEA)t1c|R{IY0gPUpdpdT|%0kVK<4`R=Xu z*Jl3T8oy-M{JdXH(X#yF?N+r{O!9Q)V>A9o>($-0}~(Q(jGK#7A%N%ifzlRj>fCC`R!+ntu}ee2YY zG?TagVxl*j7KyVn28p;%Wa*!(5qOA!SwO{4PQc=bFM~6m!3^Q%nT%-$4A0oAcJ1D^ zYW3@i@29`~eJD6_;zako;jwpPV}twVIqf@hg5&%Np6?v|-_LVf)d{`1r(emzl45wG z=j;81e-#z{Y|{=j?wcBx$;PBT^Nz^Y*$>~X?!K_j)a2ExquN{SD=WS~-?sl(xZe|# zcUDSD3OQ#RWDcaKeA`=|4+Z) zaz@DP-<$2v(&sB(HGMt(@z0uln>Sy!{r=r`|8eHqD)A4tbe>QAej~GbLapWfi{7WN z?avW*|MR|artO_gzqu5CWbM8kF-=`DE$;RdoA>8FA6HRq)G++Hyyr{5LYIT5nb6Id zA~Ignr*yG=b>tCYn-i3vDabFW^YqW5nOph)R>@9Wvrp>Ayux)t1&r}&vI&3cS)y-0 zt#%ZhSavD?(7r9MHOAFi9T~sH7#~b1)nQ=x@L}HhcQs!ppZjuOE`;&So}D{yu24L2 zLor+bG^c1h9oO_1i4j#bO-e=?;1 zx+`t?q2}+KU-SQ1DV3M3Sv+^{T$}S7cR4@!UAyf{f_M-^*`xDi(hKH%E${!<=Y24= zeA&e-n-f&;Zr7>XwENSs)$V`Z_g;N>O1zZ$DeE=`snffoZ{^AKyGOSkt4sT2eT>s( zql%)Vth31CE)VJT_03=XmF7)huuGo#{H|)1N${0Tfz~4Y?1m4nrRU4qS6yHC?O9Wz zwff{MN7pdKEOO`modJx^I=qvk8E&?&mD^NdweV-n+iBZHo7XMi za=-kt#6;?C6pvPSgV*w#$>MA=miI62{pv0IxMXG30;?AN<@2_Ey4x%FvA*=j{yS5x z!#UR+a?F4BNS@`@3XQI>a}B@rSMPLBVSSKzLHhpx6ANso-=6gJoJv=;)vXp454L%7 z2Yxij=_#64e|p&K|BwAp+s^u-`f&g3jm?Ma-538pU9W7<_*0vqyi(Ehkk~?z)fd?_)-L+D_e~?~ zPu_L;c~7<3-d~=jyoPTA^Z76RW-6jvzBzPGt*Dvhv%llmm-qHZ9|{E>&8Ysm_dox+ z^xka}j)w(Zr)xTbPOUTKDg7thLD-+z9bwXT0Xg6NG5&vYt{{C$P?vG@ zmzRqs#Jr2l^}QtY#hY)kk<;{Nu756Hl&BY2wIZe9#uEF2jUO-GpF6kGX=T6*7N(52 z4;vJwE)iKS^h?{${@Ul`=6|CDcn#O={ByGDd*%ZZfr%4N)IHmDx@c{6^8C(9lbF3v z-x|zMQlGX;rsIBkz?zGa*Y7^!6<{iOthz;7N2!o0>d!RosGsxlEB}3{U#cDdYMSfb z2C^rwp0%&fa`lvGBx_>Js*A?~NCpI2Q6+=)LgCo#8W0r*W^C z)+RB5Km0fogTb2GKjqKP+uqat_0(tHb8BmB$N%LMau@$eEp6xtEiNnMKJZjRRf=0D zwDPXb<{d{Lcl>WYy#HIc9^+5-W~O)P$Jfl?%OGZ6Au~NY`u3h8ucMl-D;?D~{QGD2 z(L1F`fJ>?O+nbB0-uN>KN(UshCVl&;7G1F8lc15q#=5e<%=_7561tBt{{Hajd*bddaYA+KmS)A?@>aclnciqVala{`+ ze6)Y!|Ej0qjrDsUc;v0#xOi`a`h2tP+tObaZjsZJIL_peaOr&1hW1OJ-DiiyubC1g zr!~{qUzll%<9>z%JS+I7PdN2lJg+u=<6gHFb1Jf?SYMVow%`PV-Sm!CBHRn&ass|> z$^WJQJ;Xt?Nb~9iiE9jFr+)uWC%X^{R2z%3X2J3Tt1zHM=LeC0f7!$bY3jsX0vyxTO%# z^>uy;&(izptnY)L?DPJ?=dwUYO;yrIl+7+>W}Rfhw=avO>wmJllo@YhO=Do#mVQR7 zO{3A`sKsFmwZ}{;0rEL>Vp(ok{NtXn?fI{j4I&FZDa!9z`6lu6pTvf>`ZXD!K7_wM zv`g>c?vHlOyBQd=1UvpIG0aMsFRr-%L(s8||M!;q+~~f(UxdHl$bY7ir$6qW<~p!8 z>5pr>@jLcd;hJmW`nl(-)!&>;N%>>ndOUART}2*C%&DZKwTqA6bsP&1AG{(_oa1 zma*;rxj6UC^vmlX1*|^ZkR|ka*3^G$y-_n=gd3*4yK(IOLk7XmNBjSt`*P;{MzP@V z_YZ&8?AyCn_jsP*ort(s+t^>sWi-gk@{?D%(w1tlGAc!iiZd=Y|IY4z*NSti?acFm+M-h@?v0Ca z?ffC;=~J2_>!2+7t1iOS!es9zORE%FZIhY@-qG+QX!*Jlt?Afyus`vdvwtGc)^uyL~nEmQ#4F3=HompiTkN#Udt&~24XWD+;n$tJYO*Zh!xg$>k>`za);!*S8*JG7~okvvEQ`Kll+{(0;7 z{flqJxcYx#U*yiG_Bkm=K~3z!{ae2-KCr2tz|Zz|q0iEWbB`}Cu${d(UP0%n$RkDL zzswH%3jZBi`#y_jRl}^5`}gk6JO4a4f&blts>RmK3>;$L?|)q-9o(yY+V}Dwrgtfi z_x2vIv+Y%Voi4UBH9WK6$bZx4``7Wj`lG*!;hWB1$pjmzd_T$GE9aITTcI4JwsiW9 z8V{L3m7Coj8nTuyN&1SNW(z$XIy4;jC;ehHVS0Vx!Z&eyo0<>bg7$7bW6s^S_9V}u zH9}?!*UsL*@n^>7yNi;#e=PIev3||!DcygZZvIXU@lg7~zwo-f$j?{5@6XJSd$Hl! zZtuw_Q!HezJ*lq#CCl*P!Mz1%-kmP%rFbE<%=1eFHgjcDYNsu00V_w~(`}gWxGgj9GwST9av^14B-npk=HuLJV zUuo+Og=8#@o{@guZri_)-&vII?7#cJ^5vzai!^RsagJ5J!R#R1)A22~P~mU$wgss- z`MP^AxW3AKzx1wZ_wJ>K7t|d2UwXWrv-Hy4Kli4bfA;K|fqKZSDNA0J3RkA)T3-`- zyJ*50$B<(+6PX$rHWk-RlxU!0RogIc^AITx?iNci}>Br(1vqsM;PRBgUxB}<>D^78r}2bFadMoepXIvx7B zdD)szrd+!5Uv}2vuk%FzG8}uo|7P5q`8jVU^a>C36{f{rNU%5_qkGC4Hpw$2QY|Z}1mA99A%$^mq z>cgfj3#!)H{S*9uUHCsQSA%u(?k4YfhO3<_=fy`vGs|`#mAZ4~(uqR--D`^982;-B zuc{C27y)E{5LUcdZp+vq(wsVmQFCxc%LZFF*MWO?Gc#z4YhO zQtyB5Z}&GnSX!a}^ndEpkbj>a^L#t@XWtnIahWT1w!Ow@muB5#_ICfXyK0@u|Fs@1 zUp4*z&WK*HWM*Y5vOaJ_^A}P3J;Z0V>6#aVr#aGv?@Cl#1@Dv zu_~EaC%E9oo`QF^`Hz#{`QMw(t(=#ie?D#Vf*Hb(db=18Wca&mJHD);O^$Ho3|WTd^ThUYvZN?ZB^|mG^E(Wv`8PPFWRPXa47g>@87-%BY14C$Z}8xv(>0U?+G>l6N0*}DJPed_k9_=yYGm6lw7d3nNl-N!r!?nSvCQff7AD3VCZ zylkK_rR!p4&9r0kYz@vTOC%c7CvWBdZ}viC*UQv*r(X&PX#bseO=`wMgD3_@fg3mF z1S0=2S2HoVaHi zS>6BI@i^zFS646NKAWI@D=mH9)LmzCMPy{v#X}Ugu8OELHG9~1`_+2`_Za7Y%}MTm z^ou@i5<0Zw{PgVCH>XaW`mkVw@%fD!J#&)S(w;HRkmO?7uxWqdkCQE3DRo(&_c&+% zv9!4)&oH6#@#Xi@>;E}?|5fsd)kgOD^mEk`b5wV3@8D4HUmJ7hR$7_{*OR91LnWr- z?@E=8Z>>LT#&%BUc-r06!mI}ua#wMmRTH=SbDP1z(@ov~bIO$bwT`^~mnZV{Xms_- z+wa@DmG$j2sd77wg!-_3m-vDmqGzW1|C%ng^3Gfiz0&))f1EhIsp^Qm49`FDsY_(7 zmapv%UhelgiO=<*>%lq3;s+M-yZ9NNj^9wPo8RVWaye9}c!F`4?7F|6Y;v|`@1I@I z3-;_Po#%S|m#yBjm?vk>TsrT{=gPbfIK#f|`MiTFi&Yk@c;Elfui^EE{f5K4V|DxIFIyQuHGWy9i^=*& z3r{|He6HmDvnJz%j(cnV1{D}t0*vgXeQNe)p35kD_ky_ft9N6#(Y+W%k9 z&G4wCnxxvysGW+xJOA@t(3!d5YxI*iA@x8O*MmQ0KF#Ugqv3M#+`%tpd@&9Ow{7Q6 zFJNx0_^?c{{nGp5KYM>~d-r4gloXNg27ZR;u56UvZ2tE$Yqmw@rzI?ZcZnxv@#)$d zX=*o#x6VEJ>5lo{2N7@eIDMP9#O<@Y$)gh(Q~dUG{kD_)o-_ZytIDtNzju|Y#^x=$ zOV@Q+sT9phGyh^dqsQXB<$1;V&UVjh^n^q8iZr-1BwP$aMP!?225h|h{jRdg-^OK& zBK{wFFMc@J%gD#5C)o3`-2W5VmuBDR2`K1Y_5Jh7I=2{yd7EF}uzX&8u2|z)z!^n- zhh4XBbj+BU+iq-Zu&Jh?JttQ6$ZhFt`QNrZ!al}cLS9BM?>!N{qqYUyO}~ zFP@yN-aI+?Y3yc(2W5o^HgoG|``P=}}5ZBNHOZM-x zeDwds-~apaq4M{Dde>+FM3yT^$}C>;{o4Gw@ix&hHqv`8J}4D>*crFUa9*)t zdkx!X%QMey=8Jxo?A#@~I6zOknaP6v+)VD2gfu4g6?`?X)~R;xjQIcAUV|2T0v+d8MuR9aB?|dRG*cz!0%tK-TKm2VWakf z%PTJ%+-dqb<+J7a=Ne9@)^HY`E6!Fh3gb@MR(56A>UmdAea2du|LOY=1U~y-yqM=y($#*^^XUfjKh~W3JoB%4dj4M4of=V_V|A`6 zu3z=RwvFfgx7L`{TT|yBjGZrMQ)R9udDPY`=J>Kn`oh)6{r`OaKXvLi>&+Y6Xa2mg z@o|)UZjpV$N)0ZC`;X!`%KlWnxVWLxeEI6rtlgcKZ-}VKWF?rL1 zT@N0=tnp~kjI$MHx@p3Zj2~)FebzjaKIeRd)y);Ro4T1kF`Ygu@z{>#?TQxJ{GYP| z%wTTU?K^kCB&9k8ww^PmrfWuhlY(Yx=csPuc$yf3y0am~MB6`JIBGth8r~ zGWH$bc`c5AnL#@}h%4y}{h#y#s@7Qfy9yQOkW z|NcHU`?vCA+q+la9JIBMSIzibp%K*i+<05ofipqH8@W%4n3^}OJTZ}5uP0~GF5%N( zz4rXP$?td0F4O0~XIEj`vK4_|6Q6kc1*res{x9d#D|6}Yy@KCemnzL%&M@CY`NVRY z*#~kCZ8kikW;1(=cjM1FpDPt-v$RS)dNxOVPIrq|%1PF%r`88+J-fWM|Btoaj7dE% zU#5Gn7I{2F#d=xa@BdHs`^Wub)9Z4Z$@i3L`eF^M!bdKFBA5O-yNw>;2|h?M)DET=dyA?(xQYZlz_JRy(#F3q1d%Y6*wu zLa~M(pOXHvOtC6mXeJgwAEj1gOQ7%?unlE7!PfA#C$8I_-Ym|y?* z%0A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 diff --git a/app/src/main/res/drawable/header.webp b/app/src/main/res/drawable/header.webp new file mode 100644 index 0000000000000000000000000000000000000000..d112e2f36c17195d7140c5043a23566b55644840 GIT binary patch literal 20798 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 literal 0 HcmV?d00001 From 680a5dfea35018d2f762210fdafe6e94982443c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 8 Sep 2021 22:49:14 +0200 Subject: [PATCH 083/240] [Mobidziennik] Fix missing linebreaks when sending messages. (#63) --- .../modules/messages/compose/MessagesComposeFragment.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt index 091dabef..33744a98 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt @@ -503,10 +503,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope { text.toString() } + textHtml = textHtml + .replace("", "") + .replace("", "") + .replace("p style=\"margin-top:0; margin-bottom:0;\"", "p") + if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) { textHtml = textHtml - .replace("p style=\"margin-top:0; margin-bottom:0;\"", "span") - .replace("

    ", "") + .replace("


    ", "

    ") .replace("", "") .replace("", "") .replace("", "") From cf4906f2f414c885e7a88c91b3012d92a17f1048 Mon Sep 17 00:00:00 2001 From: Czapla <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Thu, 9 Sep 2021 18:52:04 +0200 Subject: [PATCH 084/240] [API/Vulcan] Fix sending messages. (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix sending message * Add checking for address name/hash when sending message. Co-authored-by: Kuba Szczodrzyński --- .../edziennik/data/api/Errors.kt | 1 + .../data/api/edziennik/vulcan/DataVulcan.kt | 10 +++++++++ .../vulcan/data/hebe/VulcanHebeMain.kt | 6 ++++++ .../vulcan/data/hebe/VulcanHebeSendMessage.kt | 21 +++++++++++++++++-- app/src/main/res/values/errors.xml | 2 ++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 5738064c..eefc1d31 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -195,6 +195,7 @@ const val ERROR_VULCAN_HEBE_FIREBASE_ERROR = 362 const val ERROR_VULCAN_HEBE_CERTIFICATE_GONE = 363 const val ERROR_VULCAN_HEBE_SERVER_ERROR = 364 const val ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND = 365 +const val ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY = 366 const val ERROR_VULCAN_API_DEPRECATED = 390 const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt index 55185745..e3516ad4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt @@ -222,6 +222,16 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext } set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value } + private var mSenderAddressHash: String? = null + var senderAddressHash: String? + get() { mSenderAddressHash = mSenderAddressHash ?: profile?.getStudentData("senderAddressHash", null); return mSenderAddressHash } + set(value) { profile?.putStudentData("senderAddressHash", value) ?: return; mSenderAddressHash = value } + + private var mSenderAddressName: String? = null + var senderAddressName: String? + get() { mSenderAddressName = mSenderAddressName ?: profile?.getStudentData("senderAddressName", null); return mSenderAddressName } + set(value) { profile?.putStudentData("senderAddressName", value) ?: return; mSenderAddressName = value } + val apiUrl: String? get() { val url = when (apiToken[symbol]?.substring(0, 3)) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt index c017e3c9..5e2d31c0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt @@ -97,6 +97,10 @@ class VulcanHebeMain( val studentSemesterId = period.getInt("Id") ?: return@forEach val studentSemesterNumber = period.getInt("Number") ?: return@forEach + val senderEntry = student.getJsonObject("SenderEntry") + val senderAddressName = senderEntry.getString("Address") + val senderAddressHash = senderEntry.getString("AddressHash") + val hebeContext = student.getString("Context") val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true) @@ -143,6 +147,8 @@ class VulcanHebeMain( studentData["schoolSymbol"] = schoolSymbol studentData["schoolShort"] = schoolShort studentData["schoolName"] = schoolCode + studentData["senderAddressName"] = senderAddressName + studentData["senderAddressHash"] = senderAddressHash studentData["hebeContext"] = hebeContext } dateSemester1Start?.let { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt index 8f5f5afa..c6a02ae7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe import com.google.gson.JsonObject import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe @@ -27,6 +28,22 @@ class VulcanHebeSendMessage( } init { + if (data.senderAddressName == null || data.senderAddressHash == null) { + VulcanHebeMain(data).getStudents(data.profile, null) { + if (data.senderAddressName == null || data.senderAddressHash == null) { + data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY) + } + else { + sendMessage() + } + } + } + else { + sendMessage() + } + } + + private fun sendMessage() { val recipientsArray = JsonArray() recipients.forEach { teacher -> recipientsArray += JsonObject( @@ -40,10 +57,10 @@ class VulcanHebeSendMessage( val senderName = (profile?.accountName ?: profile?.studentNameLong) ?.swapFirstLastName() ?: "" val sender = JsonObject( - "Address" to senderName, + "Address" to data.senderAddressName, "LoginId" to data.studentLoginId.toString(), "Initials" to senderName.getNameInitials(), - "AddressHash" to senderName.sha1Hex() + "AddressHash" to data.senderAddressHash ) apiPost( diff --git a/app/src/main/res/values/errors.xml b/app/src/main/res/values/errors.xml index d90af9ed..827227c5 100644 --- a/app/src/main/res/values/errors.xml +++ b/app/src/main/res/values/errors.xml @@ -165,6 +165,7 @@ ERROR_VULCAN_HEBE_CERTIFICATE_GONE ERROR_VULCAN_HEBE_SERVER_ERROR ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND + ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY ERROR_VULCAN_API_DEPRECATED ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN @@ -363,6 +364,7 @@ VULCAN®: urządzenie usunięte. Zaloguj się ponownie do dziennika. VULCAN®: błąd serwera. Dziennik może być przeciążony. VULCAN®: nie znaleziono bytu + Błąd wysyłania wiadomości - brak informacji o nadawcy. W związku z wygaszeniem aplikacji Dzienniczek+ przez firmę Vulcan, należy zalogować się ponownie.\n\nAby móc dalej korzystać z aplikacji Szkolny.eu, otwórz Ustawienia i wybierz opcję Dodaj nowego ucznia.\nNastępnie zaloguj się do dziennika Vulcan zgodnie z instrukcją.\n\nPrzepraszamy za niedogodności. Błędny email lub hasło From b31bf5c1abf4f2b3d07a5713dcefe059e26db66e Mon Sep 17 00:00:00 2001 From: Czapla <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Thu, 9 Sep 2021 23:13:39 +0200 Subject: [PATCH 085/240] [API/Vulcan] Fix missing timetable entries. (#67) --- .../api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt index 03a28a9e..ffbb0289 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt @@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGE import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE @@ -47,7 +48,7 @@ class VulcanHebeTimetable( ?: previousWeekStart val dateTo = dateFrom.clone().stepForward(0, 0, 13) - val lastSync = null + val lastSync = 0L apiGetList( TAG, @@ -106,6 +107,8 @@ class VulcanHebeTimetable( "Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}" ) + data.toRemove.add(DataRemoveModel.Timetable.between(dateFrom, dateTo)) + data.lessonList.addAll(lessonList) data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS) From 9fdee6e0c74b9666e64faa4f6a9650e6fd83149c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 9 Sep 2021 23:14:07 +0200 Subject: [PATCH 086/240] [UI] Fix restoring header background dialog. (#65) --- .../edziennik/ui/modules/settings/cards/SettingsThemeCard.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt index a009279d..1018e92c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt @@ -88,7 +88,7 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { text = R.string.settings_theme_drawer_header_text, icon = CommunityMaterial.Icon2.cmd_image_outline ) { - if (app.config.ui.appBackground == null) { + if (app.config.ui.headerBackground == null) { setHeaderBackground() return@createActionItem } From dd6a2c0979c2f01a96bea39726d57d523a7a6f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 9 Sep 2021 23:14:24 +0200 Subject: [PATCH 087/240] [API/Mobidziennik] Add Web timetable scrapper. (#66) * [API] Add utils for getting teacher, subject and team by name. * [API/Mobidziennik] Add Web timetable scrapper. * [API/Mobidziennik] Add missing Regexes. --- .../edziennik/data/api/Regexes.kt | 11 + .../edziennik/edudziennik/DataEdudziennik.kt | 31 -- .../data/web/EdudziennikWebExams.kt | 2 +- .../data/web/EdudziennikWebGrades.kt | 2 +- .../data/web/EdudziennikWebHomework.kt | 2 +- .../data/web/EdudziennikWebStart.kt | 2 +- .../data/web/EdudziennikWebTimetable.kt | 3 +- .../mobidziennik/MobidziennikFeatures.kt | 7 + .../mobidziennik/data/MobidziennikData.kt | 4 + .../data/web/MobidziennikWebTimetable.kt | 333 ++++++++++++++++++ .../api/edziennik/podlasie/DataPodlasie.kt | 35 -- .../data/api/PodlasieApiFinalGrades.kt | 2 +- .../podlasie/data/api/PodlasieApiGrades.kt | 2 +- .../podlasie/data/api/PodlasieApiMain.kt | 8 +- .../podlasie/data/api/PodlasieApiTimetable.kt | 11 +- .../edziennik/data/api/models/Data.kt | 105 ++++++ 16 files changed, 484 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt index 5d847e3f..2bbf1894 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -117,6 +117,17 @@ object Regexes { } + val MOBIDZIENNIK_TIMETABLE_TOP by lazy { + """
    .+?
    """.toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_TIMETABLE_CELL by lazy { + """
    .+?style="(.+?)".+?title="(.+?)".+?>\s+(.+?)\s+
    """.toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_TIMETABLE_LEFT by lazy { + """
    .+?
    """.toRegex(DOT_MATCHES_ALL) + } + + val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy { """""".toRegex(DOT_MATCHES_ALL) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt index 40601249..49a9d4a0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt @@ -111,37 +111,6 @@ class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Dat val courseStudentEndpoint: String get() = "Course/$studentId/" - fun getSubject(longId: String, name: String): Subject { - val id = longId.crc32() - return subjectList.singleOrNull { it.id == id } ?: run { - val subject = Subject(profileId, id, name, name) - subjectList.put(id, subject) - subject - } - } - - fun getTeacher(firstName: String, lastName: String, longId: String? = null): Teacher { - val name = "$firstName $lastName".fixName() - val id = name.crc32() - return teacherList.singleOrNull { it.id == id }?.also { - if (longId != null && it.loginId == null) it.loginId = longId - } ?: run { - val teacher = Teacher(profileId, id, firstName, lastName, longId) - teacherList.put(id, teacher) - teacher - } - } - - fun getTeacherByFirstLast(nameFirstLast: String, longId: String? = null): Teacher { - val nameParts = nameFirstLast.split(" ") - return getTeacher(nameParts[0], nameParts[1], longId) - } - - fun getTeacherByLastFirst(nameLastFirst: String, longId: String? = null): Teacher { - val nameParts = nameLastFirst.split(" ") - return getTeacher(nameParts[1], nameParts[0], longId) - } - fun getEventType(longId: String, name: String): EventType { val id = longId.crc16().toLong() return eventTypes.singleOrNull { it.id == id } ?: run { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt index f6abf637..c700100c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt @@ -40,7 +40,7 @@ class EdudziennikWebExams(override val data: DataEdudziennik, val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) ?: return@forEach val subjectName = subjectElement.text().trim() - val subject = data.getSubject(subjectId, subjectName) + val subject = data.getSubject(subjectId.crc32(), subjectName) val dateString = examElement.child(2).text().trim() if (dateString.isBlank()) return@forEach diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt index 459f13aa..6a247290 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt @@ -53,7 +53,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik, val subjectId = subjectElement.id().trim() val subjectName = subjectElement.child(0).text().trim() - val subject = data.getSubject(subjectId, subjectName) + val subject = data.getSubject(subjectId.crc32(), subjectName) val gradeType = when { subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt index 169a5127..b843abe9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt @@ -41,7 +41,7 @@ class EdudziennikWebHomework(override val data: DataEdudziennik, val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) ?: return@forEach val subjectName = subjectElement.text() - val subject = data.getSubject(subjectId, subjectName) + val subject = data.getSubject(subjectId.crc32(), subjectName) val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date) val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt index 66cb7a59..3fdd6e0e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt @@ -73,7 +73,7 @@ class EdudziennikWebStart(override val data: DataEdudziennik, EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach { val id = it[1].trim() val name = it[2].trim() - data.getSubject(id, name) + data.getSubject(id.crc32(), name) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt index 94db15ef..33feb75e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.crc32 import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik @@ -89,7 +90,7 @@ class EdudziennikWebTimetable(override val data: DataEdudziennik, val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) ?: return@forEachIndexed val subjectName = subjectElement.text().trim() - val subject = data.getSubject(subjectId, subjectName) + val subject = data.getSubject(subjectId.crc32(), subjectName) /* Getting teacher */ diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt index 7396ac52..7b39e455 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt @@ -18,6 +18,7 @@ const val ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE = 2050 const val ENDPOINT_MOBIDZIENNIK_WEB_MANUALS = 2100 const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200 const val ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK = 2300 // not used as an endpoint +const val ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE = 2400 const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000 val MobidziennikFeatures = listOf( @@ -38,6 +39,12 @@ val MobidziennikFeatures = listOf( + /** + * Timetable - web scraping - does nothing if the API_MAIN timetable is enough. + */ + Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_TIMETABLE, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_MOBIDZIENNIK_WEB + ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), /** * Agenda - "API" + web scraping. */ diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt index 09e1e9fb..b82f6355 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt @@ -84,6 +84,10 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) MobidziennikWebManuals(data, lastSync, onSuccess) }*/ + ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE-> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + MobidziennikWebTimetable(data, lastSync, onSuccess) + } else -> onSuccess(endpointId) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt new file mode 100644 index 00000000..221c50aa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt @@ -0,0 +1,333 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-9-8. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import android.annotation.SuppressLint +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week +import kotlin.collections.set +import kotlin.text.replace + +class MobidziennikWebTimetable( + override val data: DataMobidziennik, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : MobidziennikWeb(data, lastSync) { + companion object { + private const val TAG = "MobidziennikWebTimetable" + } + + private val rangesH = mutableMapOf, Date>() + private val hoursV = mutableMapOf>() + private var startDate: Date + + private fun parseCss(css: String): Map { + return css.split(";").mapNotNull { + val spl = it.split(":") + if (spl.size != 2) + return@mapNotNull null + return@mapNotNull spl[0].trim() to spl[1].trim() + }.toMap() + } + + private fun getRangeH(h: Float): Date? { + return rangesH.entries.firstOrNull { + h in it.key + }?.value + } + + private fun stringToDate(date: String): Date? { + val items = date.split(" ") + val day = items.getOrNull(0)?.toIntOrNull() ?: return null + val year = items.getOrNull(2)?.toIntOrNull() ?: return null + val month = when (items.getOrNull(1)) { + "stycznia" -> 1 + "lutego" -> 2 + "marca" -> 3 + "kwietnia" -> 4 + "maja" -> 5 + "czerwca" -> 6 + "lipca" -> 7 + "sierpnia" -> 8 + "września" -> 9 + "października" -> 10 + "listopada" -> 11 + "grudnia" -> 12 + else -> return null + } + return Date(year, month, day) + } + + init { + val currentWeekStart = Week.getWeekStart() + val nextWeekEnd = Week.getWeekEnd().stepForward(0, 0, 7) + if (Date.getToday().weekDay > 4) { + currentWeekStart.stepForward(0, 0, 7) + } + startDate = data.arguments?.getString("weekStart")?.let { + Date.fromY_m_d(it) + } ?: currentWeekStart + + val syncFutureDate = startDate > nextWeekEnd + // TODO: 2021-09-09 make DataRemoveModel keep extra lessons + val syncExtraLessons = false && System.currentTimeMillis() - (lastSync ?: 0) > 2 * DAY * MS + if (!syncFutureDate && !syncExtraLessons) { + onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE) + } + else { + val types = when { + syncFutureDate -> mutableListOf("podstawowy")//, "pozalekcyjny") + syncExtraLessons -> mutableListOf("pozalekcyjny") + else -> mutableListOf() + } + + syncTypes(types, startDate) { + // set as synced now only when not syncing future date + // (to avoid waiting 2 days for normal sync after future sync) + if (syncExtraLessons) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS) + onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE) + } + } + } + + private fun syncTypes(types: MutableList, startDate: Date, onSuccess: () -> Unit) { + if (types.isEmpty()) { + onSuccess() + return + } + val type = types.removeAt(0) + webGet(TAG, "/dziennik/planlekcji?typ=$type&tydzien=${startDate.stringY_m_d}") { html -> + MobidziennikLuckyNumberExtractor(data, html) + readRangesH(html) + readRangesV(html) + readLessons(html) + syncTypes(types, startDate, onSuccess) + } + } + + private fun readRangesH(html: String) { + val htmlH = Regexes.MOBIDZIENNIK_TIMETABLE_TOP.find(html) ?: return + val docH = Jsoup.parse(htmlH.value) + + var posH = 0f + for (el in docH.select("div > div")) { + val css = parseCss(el.attr("style")) + val width = css["width"] + ?.trimEnd('%') + ?.toFloatOrNull() + ?: continue + val value = stringToDate(el.attr("title")) + ?: continue + + val range = posH.rangeTo(posH + width) + posH += width + + rangesH[range] = value + } + } + + private fun readRangesV(html: String) { + val htmlV = Regexes.MOBIDZIENNIK_TIMETABLE_LEFT.find(html) ?: return + val docV = Jsoup.parse(htmlV.value) + + for (el in docV.select("div > div")) { + val css = parseCss(el.attr("style")) + val top = css["top"] + ?.trimEnd('%') + ?.toFloatOrNull() + ?: continue + val values = el.text().split(" ") + + val time = values.getOrNull(0)?.let { + Time.fromH_m(it) + } ?: continue + val num = values.getOrNull(1)?.toIntOrNull() + + hoursV[(top * 100).toInt()] = time to num + } + } + + private val whitespaceRegex = "\\s+".toRegex() + private val classroomRegex = "\\((.*)\\)".toRegex() + private fun cleanup(str: String): List { + return str + .replace(whitespaceRegex, " ") + .replace("\n", "") + .replace("<small>", "$") + .replace("</small>", "$") + .replace("<br />", "\n") + .replace("<br/>", "\n") + .replace("<br>", "\n") + .replace("
    ", "\n") + .replace("
    ", "\n") + .replace("
    ", "\n") + .replace("", "%") + .replace("", "%") + .replace("", "") + .replace("", "") + .split("\n") + .map { it.trim() } + } + + @SuppressLint("LongLogTag", "LogNotTimber") + private fun readLessons(html: String) { + val matches = Regexes.MOBIDZIENNIK_TIMETABLE_CELL.findAll(html) + + val noLessonDays = mutableListOf() + for (i in 0..6) { + noLessonDays.add(startDate.clone().stepForward(0, 0, i)) + } + + for (match in matches) { + val css = parseCss("${match[1]};${match[2]}") + val left = css["left"]?.trimEnd('%')?.toFloatOrNull() ?: continue + val top = css["top"]?.trimEnd('%')?.toFloatOrNull() ?: continue + val width = css["width"]?.trimEnd('%')?.toFloatOrNull() ?: continue + val height = css["height"]?.trimEnd('%')?.toFloatOrNull() ?: continue + + val posH = left + width / 2f + val topInt = (top * 100).toInt() + val bottomInt = ((top + height) * 100).toInt() + + val lessonDate = getRangeH(posH) ?: continue + val (startTime, lessonNumber) = hoursV[topInt] ?: continue + val endTime = hoursV[bottomInt]?.first ?: continue + + noLessonDays.remove(lessonDate) + + var typeName: String? = null + var subjectName: String? = null + var teacherName: String? = null + var classroomName: String? = null + var teamName: String? = null + val items = (cleanup(match[3]) + cleanup(match[4])).toMutableList() + + var length = 0 + while (items.isNotEmpty() && length != items.size) { + length = items.size + val toRemove = mutableListOf() + items.forEachIndexed { i, item -> + when { + item.isEmpty() -> + toRemove.add(item) + item.contains(":") && item.contains(" - ") -> + toRemove.add(item) + + item.startsWith("%") -> { + subjectName = item.trim('%') + toRemove.add(item) + toRemove.add(items[0]) + } + + item.startsWith("&") -> { + typeName = item.trim('&') + toRemove.add(item) + } + typeName != null && (item.contains(typeName!!) || item.contains("")) -> { + toRemove.add(item) + } + + item.contains("(") && item.contains(")") -> { + classroomName = classroomRegex.find(item)?.get(1) + items[i] = item.replace("($classroomName)", "").trim() + } + classroomName != null && item.contains(classroomName!!) -> { + items[i] = item.replace("($classroomName)", "").trim() + } + + item.contains("class=\"wyjatek tooltip\"") -> + toRemove.add(item) + } + } + items.removeAll(toRemove) + } + + if (items.size == 2 && items[0].contains(" - ")) { + val parts = items[0].split(" - ") + teamName = parts[0] + teacherName = parts[1] + } + else if (items.size == 2 && typeName?.contains("odwołana") == true) { + teamName = items[0] + } + else if (items.size == 4) { + teamName = items[0] + teacherName = items[1] + } + + val type = when (typeName) { + "zastępstwo" -> Lesson.TYPE_CHANGE + "lekcja odwołana", "odwołana" -> Lesson.TYPE_CANCELLED + else -> Lesson.TYPE_NORMAL + } + val subject = subjectName?.let { data.getSubject(null, it) } + val teacher = teacherName?.let { data.getTeacherByLastFirst(it) } + val team = teamName?.let { data.getTeam( + id = null, + name = it, + schoolCode = data.loginServerName ?: return@let null, + isTeamClass = false + ) } + + Lesson(data.profileId, -1).also { + it.type = type + if (type == Lesson.TYPE_CANCELLED) { + it.oldDate = lessonDate + it.oldLessonNumber = lessonNumber + it.oldStartTime = startTime + it.oldEndTime = endTime + it.oldSubjectId = subject?.id ?: -1 + it.oldTeamId = team?.id ?: -1 + } + else { + it.date = lessonDate + it.lessonNumber = lessonNumber + it.startTime = startTime + it.endTime = endTime + it.subjectId = subject?.id ?: -1 + it.teacherId = teacher?.id ?: -1 + it.teamId = team?.id ?: -1 + it.classroom = classroomName + } + + it.id = it.buildId() + + val seen = profile?.empty == false || lessonDate < Date.getToday() + + if (it.type != Lesson.TYPE_NORMAL) { + data.metadataList.add( + Metadata( + data.profileId, + Metadata.TYPE_LESSON_CHANGE, + it.id, + seen, + seen + ) + ) + } + data.lessonList += it + } + } + + for (date in noLessonDays) { + data.lessonList += Lesson(data.profileId, date.value.toLong()).also { + it.type = Lesson.TYPE_NO_LESSONS + it.date = date + } + } + } +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt index d117c139..7962de7e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt @@ -81,39 +81,4 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a val loginShort: String? get() = studentLogin?.split('@')?.get(0) - - fun getSubject(name: String): Subject { - val id = name.crc32() - return subjectList.singleOrNull { it.id == id } ?: run { - val subject = Subject(profileId, id, name, name) - subjectList.put(id, subject) - subject - } - } - - fun getTeacher(firstName: String, lastName: String): Teacher { - val name = "$firstName $lastName".fixName() - return teacherList.singleOrNull { it.fullName == name } ?: run { - val id = name.crc32() - val teacher = Teacher(profileId, id, firstName, lastName) - teacherList.put(id, teacher) - teacher - } - } - - fun getTeam(name: String? = null): Team { - if (name == "cała klasa" || name == null) return teamClass ?: run { - val id = className!!.crc32() - val teamCode = "$schoolShortName:$className" - val team = Team(profileId, id, className, Team.TYPE_CLASS, teamCode, -1) - teamList.put(id, team) - return team - } else { - val id = name.crc32() - val teamCode = "$schoolShortName:$name" - val team = Team(profileId, id, name, Team.TYPE_VIRTUAL, teamCode, -1) - teamList.put(id, team) - return team - } - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt index 7c4575aa..bb4f696c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt @@ -36,7 +36,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List) } val subjectName = grade.getString("SchoolSubject") ?: return@forEach - val subject = data.getSubject(subjectName) + val subject = data.getSubject(null, subjectName) val addedDate = if (profile.empty) profile.getSemesterStart(semester).inMillis else System.currentTimeMillis() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt index d5696567..c3946c76 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt @@ -34,7 +34,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List) { val teacher = data.getTeacher(teacherFirstName, teacherLastName) val subjectName = grade.getString("SchoolSubject") ?: return@forEach - val subject = data.getSubject(subjectName) + val subject = data.getSubject(null, subjectName) val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis } ?: System.currentTimeMillis() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt index 6fe77b5c..f108d35e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt @@ -22,7 +22,13 @@ class PodlasieApiMain(override val data: DataPodlasie, init { apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json -> - data.getTeam() // Save the class team when it doesn't exist. + // Save the class team when it doesn't exist. + data.getTeam( + id = null, + name = data.className ?: "", + schoolCode = data.schoolShortName ?: "", + isTeamClass = true + ) json.getInt("LuckyNumber")?.let { PodlasieApiLuckyNumber(data, it) } json.getJsonArray("Teacher")?.asJsonObjectList()?.let { PodlasieApiTeachers(data, it) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt index a9a12d5d..13cff861 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt @@ -43,14 +43,21 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List) { val startTime = lesson.getString("TimeFrom")?.let { Time.fromH_m_s(it) } ?: return@forEach val endTime = lesson.getString("TimeTo")?.let { Time.fromH_m_s(it) } ?: return@forEach - val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(it) } + val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(null, it) } ?: return@forEach val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach val teacher = data.getTeacher(teacherFirstName, teacherLastName) - val team = lesson.getString("Group")?.let { data.getTeam(it) } ?: return@forEach + val team = lesson.getString("Group")?.let { + data.getTeam( + id = null, + name = it, + schoolCode = data.schoolShortName ?: "", + isTeamClass = it == "cała klasa" + ) + } ?: return@forEach val classroom = lesson.getString("Room") Lesson(data.profileId, -1).also { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index 8dda9543..2b7bdf1a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -2,6 +2,7 @@ package pl.szczodrzynski.edziennik.data.api.models import android.util.LongSparseArray import android.util.SparseArray +import androidx.core.util.set import androidx.core.util.size import androidx.room.OnConflictStrategy import com.google.gson.JsonObject @@ -376,4 +377,108 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt fun startProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + + /* _ _ _ _ _ + | | | | | (_) | + | | | | |_ _| |___ + | | | | __| | / __| + | |__| | |_| | \__ \ + \____/ \__|_|_|__*/ + fun getSubject(id: Long?, name: String, shortName: String = name): Subject { + var subject = subjectList.singleOrNull { it.id == id } + if (subject == null) + subject = subjectList.singleOrNull { it.longName == name } + if (subject == null) + subject = subjectList.singleOrNull { it.shortName == name } + + if (subject == null) { + subject = Subject( + profileId, + id ?: name.crc32(), + name, + shortName + ) + subjectList[subject.id] = subject + } + return subject + } + + fun getTeam(id: Long?, name: String, schoolCode: String, isTeamClass: Boolean = false): Team { + if (isTeamClass && teamClass != null) + return teamClass as Team + var team = teamList.singleOrNull { it.id == id } + + val namePlain = name.replace(" ", "") + if (team == null) + team = teamList.singleOrNull { it.name.replace(" ", "") == namePlain } + + if (team == null) { + team = Team( + profileId, + id ?: name.crc32(), + name, + if (isTeamClass) Team.TYPE_CLASS else Team.TYPE_VIRTUAL, + "$schoolCode:$name", + -1 + ) + teamList[team.id] = team + } + return team + } + + fun getTeacher(firstName: String, lastName: String, loginId: String? = null): Teacher { + val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" } + return validateTeacher(teacher, firstName, lastName, loginId) + } + + fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher { + val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" } + return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId) + } + + fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher { + val nameParts = nameLastFirst.split(" ") + return if (nameParts.size == 1) + getTeacher(nameParts[0], "", loginId) + else + getTeacher(nameParts[1], nameParts[0], loginId) + } + + fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher { + val nameParts = nameFirstLast.split(" ") + return if (nameParts.size == 1) + getTeacher(nameParts[0], "", loginId) + else + getTeacher(nameParts[0], nameParts[1], loginId) + } + + fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher { + val nameParts = nameFDotLast.split(".") + return if (nameParts.size == 1) + getTeacher(nameParts[0], "", loginId) + else + getTeacher(nameParts[0][0], nameParts[1], loginId) + } + + fun getTeacherByFDotSpaceLast(nameFDotSpaceLast: String, loginId: String? = null): Teacher { + val nameParts = nameFDotSpaceLast.split(".") + return if (nameParts.size == 1) + getTeacher(nameParts[0], "", loginId) + else + getTeacher(nameParts[0][0], nameParts[1], loginId) + } + + private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher { + val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply { + id = fullName.crc32() + teacherList[id] = this + } + return obj.also { + if (loginId != null && it.loginId != null) + it.loginId = loginId + if (firstName.length > 1) + it.name = firstName + it.surname = lastName + } + } } From 59137075197d8e4bbbc755b536e4b41f9d4caa95 Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Fri, 10 Sep 2021 06:49:17 +0200 Subject: [PATCH 088/240] [UI] Use number keyboard in the PIN field in Vulcan. (#68) --- .../edziennik/ui/modules/login/LoginFormFragment.kt | 3 +++ .../pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt | 2 ++ 2 files changed, 5 insertions(+) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt index ea6bbe4d..3360e93b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt @@ -85,6 +85,9 @@ class LoginFormFragment : Fragment(), CoroutineScope { if (credential is LoginInfo.FormField) { val b = LoginFormFieldItemBinding.inflate(layoutInflater) b.textLayout.hint = app.getString(credential.name) + if (credential.isNumber) { + b.textEdit.inputType = InputType.TYPE_CLASS_NUMBER + } if (credential.hideText) { b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt index 77036f19..8a5e60d1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt @@ -179,6 +179,7 @@ object LoginInfo { ERROR_LOGIN_VULCAN_INVALID_PIN_2_REMAINING to R.string.error_312_reason ), isRequired = true, + isNumber = true, validationRegex = "[0-9]+", caseMode = FormField.CaseMode.LOWER_CASE ) @@ -401,6 +402,7 @@ object LoginInfo { val validationRegex: String, val caseMode: CaseMode = CaseMode.UNCHANGED, val hideText: Boolean = false, + val isNumber: Boolean = false, val stripTextRegex: String? = null ) : BaseCredential(keyName, name, errorCodes) { enum class CaseMode { UNCHANGED, UPPER_CASE, LOWER_CASE } From b9aca981e51bffedae6fae7f483af36f4768eb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 10 Sep 2021 16:56:52 +0200 Subject: [PATCH 089/240] [App] Change app-wide storage dir to Download subfolder. --- .../edziennik/ui/modules/views/AttachmentsView.kt | 4 +--- app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt index 354a8921..564fbf12 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt @@ -37,9 +37,7 @@ class AttachmentsView @JvmOverloads constructor( } private val storageDir by lazy { - val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu") - storageDir.mkdirs() - storageDir + Utils.getStorageDir() } fun init(arguments: Bundle, owner: Any) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java index 910fc6f0..d4ffae05 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java @@ -776,7 +776,8 @@ public class Utils { public static File getStorageDir() { if (storageDir != null) return storageDir; - storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu"); + storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + storageDir = new File(storageDir, "Szkolny.eu"); storageDir.mkdirs(); return storageDir; } From 83f84de01911e0702d0eeb3677c4adf1f6a737d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 10 Sep 2021 17:11:21 +0200 Subject: [PATCH 090/240] [UI] Fix attachments view cut off on API 30+. --- .../edziennik/ui/modules/views/AttachmentsView.kt | 1 + app/src/main/res/layout/message_fragment.xml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt index 564fbf12..c981a4ba 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt @@ -80,6 +80,7 @@ class AttachmentsView @JvmOverloads constructor( list.adapter = adapter list.apply { setHasFixedSize(false) + isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(context) addItemDecoration(SimpleDividerItemDecoration(context)) } diff --git a/app/src/main/res/layout/message_fragment.xml b/app/src/main/res/layout/message_fragment.xml index 1bae5bb5..2ca32c3d 100644 --- a/app/src/main/res/layout/message_fragment.xml +++ b/app/src/main/res/layout/message_fragment.xml @@ -57,7 +57,7 @@ android:visibility="visible" tools:visibility="gone"/> - - + From efa63452e7c4332d0427ca2a54c8bf2bb8d1f26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 10 Sep 2021 17:17:31 +0200 Subject: [PATCH 091/240] [App] Fix Apply Changes not working due to manifest changes. --- app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index cc99abe8..62f4c95c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,6 +36,9 @@ android { buildTypes { debug { minifyEnabled = false + manifestPlaceholders = [ + buildTimestamp: 0 + ] } release { minifyEnabled = true From 21ddb9d706273b9a8ea2809fafec3518ceaf735f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 10 Sep 2021 17:20:12 +0200 Subject: [PATCH 092/240] [Git] Update .gitignore for .idea. --- .gitignore | 1 + .idea/discord.xml | 9 --------- .idea/kotlinc.xml | 6 ------ .idea/runConfigurations.xml | 13 ------------- 4 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 .idea/discord.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/runConfigurations.xml diff --git a/.gitignore b/.gitignore index 07e9f679..375b15e2 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,4 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,gradle,java,kotlin signatures/ +.idea/*.xml diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index a04e4e5f..00000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 0dd4b354..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index e497da99..00000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file From 2f7fcb6dc30351a223e3b80cf38a5e1e9d955f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 10 Sep 2021 17:41:39 +0200 Subject: [PATCH 093/240] [API/Mobidziennik] Fix Web timetable scrapper. --- .../mobidziennik/data/web/MobidziennikWebTimetable.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt index 221c50aa..6ceb3fd9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt @@ -228,8 +228,15 @@ class MobidziennikWebTimetable( item.startsWith("%") -> { subjectName = item.trim('%') + // I have no idea what's going on here + // ok now seriously.. the subject (long or short) item + // may NOT be 0th, as the HH:MM - HH:MM item may be before + // or even the typeName item. As these are always **before**, + // they are removed in previous iterations, so the first not removed + // item should be the long/short subjectName needing to be removed now. + toRemove.add(items[toRemove.size]) + // ...and this has to be added later toRemove.add(item) - toRemove.add(items[0]) } item.startsWith("&") -> { From 118f5e1794318264d8a5e4c4b57162dbf4fce78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Fri, 10 Sep 2021 23:45:29 +0200 Subject: [PATCH 094/240] [API/Vulcan] Fix missing attendance. (#72) --- .../data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt index 9e41bb6d..b3ebafa4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt @@ -38,7 +38,7 @@ class VulcanHebeAttendance( lastSync = lastSync ) { list, _ -> list.forEach { attendance -> - val id = attendance.getLong("AuxPresenceId") ?: return@forEach + val id = attendance.getLong("Id") ?: return@forEach val type = attendance.getJsonObject("PresenceType") ?: return@forEach val baseType = getBaseType(type) val typeName = type.getString("Name") ?: return@forEach From 2e3e3dcf3cb147c6811d3ecbc6098685b9b00b87 Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Fri, 10 Sep 2021 23:47:46 +0200 Subject: [PATCH 095/240] [Lab] Add button to disable devmode and Chucker toggle. (#70) * Add option to disable/enable chucker and option to disable dev mode from lab page * Change "chucker" to "enableChucker" * Update App.kt --- .../java/pl/szczodrzynski/edziennik/App.kt | 10 ++++-- .../szczodrzynski/edziennik/config/Config.kt | 5 +++ .../ui/modules/debug/LabPageFragment.kt | 34 +++++++++++++++++++ .../ui/modules/debug/LabProfileFragment.kt | 2 +- app/src/main/res/layout/lab_fragment.xml | 17 +++++++++- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index a9186d94..71e6c7e6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -58,6 +58,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { val profileId get() = profile.id + var enableChucker = false var debugMode = false var devMode = false } @@ -115,9 +116,11 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { HyperLog.initialize(this) HyperLog.setLogLevel(Log.VERBOSE) HyperLog.setLogFormat(DebugLogFormat(this)) - val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) - val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) - builder.addInterceptor(chuckerInterceptor) + if (enableChucker) { + val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) + val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) + builder.addInterceptor(chuckerInterceptor) + } } http = builder.build() @@ -172,6 +175,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { App.profile = Profile(0, 0, 0, "") debugMode = BuildConfig.DEBUG devMode = config.debugMode || debugMode + enableChucker = config.enableChucker || devMode if (!profileLoadById(config.lastProfileId)) { db.profileDao().firstId?.let { profileLoadById(it) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt index 160deb23..0274912f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt @@ -80,6 +80,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig { get() { mDebugMode = mDebugMode ?: values.get("debugMode", false); return mDebugMode ?: false } set(value) { set("debugMode", value); mDebugMode = value } + private var mEnableChucker: Boolean? = null + var enableChucker: Boolean + get() { mEnableChucker = mEnableChucker ?: values.get("enableChucker", false); return mEnableChucker ?: false } + set(value) { set("enableChucker", value); mEnableChucker = value } + private var mDevModePassword: String? = null var devModePassword: String? get() { mDevModePassword = mDevModePassword ?: values.get("devModePassword", null as String?); return mDevModePassword } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt index b1b84f9d..4d8205bf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt @@ -5,10 +5,12 @@ package pl.szczodrzynski.edziennik.ui.modules.debug import android.os.Bundle +import android.os.Process import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.sqlite.db.SimpleSQLiteQuery +import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -21,6 +23,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.fslogin.decode import kotlin.coroutines.CoroutineContext +import kotlin.system.exitProcess class LabPageFragment : LazyFragment(), CoroutineScope { companion object { @@ -75,6 +78,37 @@ class LabPageFragment : LazyFragment(), CoroutineScope { app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}") } + b.chucker.isChecked = app.config.enableChucker + + b.chucker.onChange { _, isChecked -> + app.config.enableChucker = isChecked + MaterialAlertDialogBuilder(activity) + .setTitle("Restart") + .setMessage("Wymagany restart aplikacji") + .setPositiveButton(R.string.ok) { _, _ -> + Process.killProcess(Process.myPid()) + Runtime.getRuntime().exit(0) + exitProcess(0) + } + .setCancelable(false) + .show() + } + + + b.disableDebug.onClick { + app.config.debugMode = false + MaterialAlertDialogBuilder(activity) + .setTitle("Restart") + .setMessage("Wymagany restart aplikacji") + .setPositiveButton(R.string.ok) { _, _ -> + Process.killProcess(Process.myPid()) + Runtime.getRuntime().exit(0) + exitProcess(0) + } + .setCancelable(false) + .show() + } + b.unarchive.onClick { app.profile.archived = false app.profile.archiveId = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt index ee3ea2c6..f4b03f55 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt @@ -166,7 +166,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { json.add("App.profile", app.gson.toJsonTree(app.profile)) json.add("App.profile.studentData", app.profile.studentData) json.add("App.profile.loginStore", loginStore?.data ?: JsonObject()) - json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values))) + json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values.toSortedMap()))) } adapter.items = LabJsonAdapter.expand(json, 0) adapter.notifyDataSetChanged() diff --git a/app/src/main/res/layout/lab_fragment.xml b/app/src/main/res/layout/lab_fragment.xml index 8152f917..6c4bca77 100644 --- a/app/src/main/res/layout/lab_fragment.xml +++ b/app/src/main/res/layout/lab_fragment.xml @@ -3,7 +3,8 @@ ~ Copyright (c) Kuba Szczodrzyński 2020-4-3. --> - @@ -38,6 +39,12 @@ android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" />--> + +