Compare commits

...

114 Commits

Author SHA1 Message Date
18c306b9ea [4.10] Update build.gradle, signing and changelog. 2021-09-22 13:55:01 +02:00
c6be1a7954 [Strings] Update copyright dates. 2021-09-22 13:52:06 +02:00
e8e9f04050 [UI] Show attendance info in timetable and lesson dialog. (#74)
* Show attendance in timetable view

* Optimize code and UX

* Update some code

* Update attendance layout

* Fix timetable view

* Bump iconics version

* Change umbrella icon

* Update app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt

* Update app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt

* Update app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt

* Update app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt

* Update lesson cell margins

* Add attendance info in lesson dialog

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-09-22 00:03:31 +02:00
3700a71c39 Merge pull request #75 from szkolny-eu/feature/availability-refactor
[API] Refactor register availability checking module.
2021-09-18 21:13:01 +02:00
60f0628f5e [Lab] Fix disabling Chucker and Dev Mode. Add new Lab options. 2021-09-18 16:37:24 +02:00
80dcd9aa69 [API] Refactor register availability checking module. 2021-09-18 16:36:17 +02:00
91b685576b [4.9] Update build.gradle, signing and changelog. 2021-09-10 23:54:09 +02:00
2e3e3dcf3c [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
2021-09-10 23:47:46 +02:00
118f5e1794 [API/Vulcan] Fix missing attendance. (#72) 2021-09-10 23:45:29 +02:00
e902352a4b Merge pull request #69 from szkolny-eu/hotfix/message-attachments
[Messages] Fix downloading and displaying attachment on API 30+.
2021-09-10 23:44:26 +02:00
2f7fcb6dc3 [API/Mobidziennik] Fix Web timetable scrapper. 2021-09-10 17:41:39 +02:00
21ddb9d706 [Git] Update .gitignore for .idea. 2021-09-10 17:20:12 +02:00
efa63452e7 [App] Fix Apply Changes not working due to manifest changes. 2021-09-10 17:17:31 +02:00
83f84de019 [UI] Fix attachments view cut off on API 30+. 2021-09-10 17:11:21 +02:00
b9aca981e5 [App] Change app-wide storage dir to Download subfolder. 2021-09-10 16:56:52 +02:00
5913707519 [UI] Use number keyboard in the PIN field in Vulcan. (#68) 2021-09-10 06:49:17 +02:00
dd6a2c0979 [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.
2021-09-09 23:14:24 +02:00
9fdee6e0c7 [UI] Fix restoring header background dialog. (#65) 2021-09-09 23:14:07 +02:00
b31bf5c1ab [API/Vulcan] Fix missing timetable entries. (#67) 2021-09-09 23:13:39 +02:00
cf4906f2f4 [API/Vulcan] Fix sending messages. (#64)
* Fix sending message

* Add checking for address name/hash when sending message.

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-09-09 18:52:04 +02:00
680a5dfea3 [Mobidziennik] Fix missing linebreaks when sending messages. (#63) 2021-09-08 22:49:14 +02:00
c1062cd7ed [UI] Update drawer header background. (#62) 2021-09-08 22:49:00 +02:00
8edc581f0b [UI] Fix multiplicated day dialog in Agenda. (#61) 2021-09-08 22:48:48 +02:00
ea9d801d08 [DB] Workaround missing event types after profile archiving. (#60) 2021-09-08 22:48:35 +02:00
8f72e11d0c [UI] Center "no data" text view. (#59) 2021-09-08 22:48:21 +02:00
452271e8c0 [UI] Add list of contributors in Settings. (#15)
* 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 <kuba@szczodrzynski.pl>
2021-09-08 19:11:14 +02:00
7b4effe889 [UI] Fix block timetable export. (#57)
* 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 <kuba@szczodrzynski.pl>
2021-09-07 22:11:29 +02:00
e2bf48d1b6 [Actions] Use JDK 11. 2021-09-02 18:27:51 +02:00
c88056ddb9 [Gradle] Update library dependencies. 2021-09-02 17:49:01 +02:00
96dbb0a057 [Gradle] Update Kotlin to v1.5.20. 2021-09-02 17:48:02 +02:00
288c80ea26 [Gradle] Update wrapper and AGP to match AS 2020.3.1. 2021-09-02 17:40:34 +02:00
5a217aca01 [4.8.2] Update build.gradle, signing and changelog. 2021-06-15 18:13:18 +02:00
4bed62aa6f [UI] Fix timetable crash when syncing (#54)
* Fix removeView

* Use removeView() instead of removeAllViews()

* Remove dayView from layout file
2021-06-11 22:06:07 +02:00
a4d604e146 [API/Librus] Update JST Client ID (#53)
@6Arin9
2021-06-11 00:05:54 +02:00
ae4405ef78 [4.8.1] Update build.gradle, signing and changelog. 2021-06-06 16:50:04 +02:00
71ca51e813 [Strings] Update copyright dates. 2021-06-06 16:47:10 +02:00
1bf07d736f [API/Librus] Update client ID (#51)
@BxOxSxS
2021-06-02 22:59:57 +02:00
909899612e Revert "[Actions] Change Firebase token to service account file."
This reverts commit 4184fbb2cd.
2021-05-26 21:51:52 +02:00
4184fbb2cd [Actions] Change Firebase token to service account file. 2021-05-26 20:44:48 +02:00
75010c0771 [4.8] Update build.gradle, signing and changelog. 2021-05-26 20:01:08 +02:00
5562498e84 Merge pull request #48 from szkolny-eu/feature/notification-long-text
[Notifications] Add long description text
2021-05-26 20:00:08 +02:00
c2d0940a80 Merge pull request #46 from szkolny-eu/hotfix/mobidziennik-message-list
[Mobidziennik] Fix sending messages returning a not found error
2021-05-26 19:59:49 +02:00
baa98f25c5 [UI] Fix easter egg prize receiving. 2021-05-26 19:09:10 +02:00
26645ee83c [DB] Add migration 93. 2021-05-26 19:00:24 +02:00
85d74bec1c [UI] Add long text for notification descriptions. 2021-05-26 19:00:09 +02:00
fd0fc652a3 Merge pull request #47 from szkolny-eu/feature/elearning-events
[Agenda] Add e-learning event type.
2021-05-26 18:52:41 +02:00
c85dac2e4d [Strings] Update copyright dates. Fix event mark as done translation. 2021-05-26 18:00:37 +02:00
c855f08f9c [API/Mobidziennik] Update messages search query to contain student name. 2021-05-26 17:46:03 +02:00
a31c68e87a [Agenda] Add e-learning event type and DB migration. 2021-04-19 14:18:28 +02:00
99021f6b3a Merge pull request #39 from szkolny-eu/feature/messages-fixes
[Messages] UI updates and fixes
2021-04-14 22:45:44 +02:00
e2b47db3fd Merge branch 'develop' into feature/messages-fixes 2021-04-14 22:42:54 +02:00
8609956ae7 [UI/Timetable] Add current time marker line. (#30) 2021-04-14 22:41:06 +02:00
e25ca930e0 Merge pull request #28 from szkolny-eu/feature/agenda-updates
[UI] Agenda updates
2021-04-14 22:40:27 +02:00
47ec1899a1 [UI/Messages] Add greeting text configuration dialog. 2021-04-14 21:08:27 +02:00
1e8fb6a9ae [UI/Messages] Restore search string after closing a message. Add message counter. 2021-04-14 20:19:04 +02:00
02eb5b7ee4 [UI/Agenda] Disable not available config options. 2021-04-14 17:03:44 +02:00
776806caef [UI/Agenda] Update DayDialog for showing event group. 2021-04-14 17:03:30 +02:00
755b846b50 [UI/Agenda] Move common code to EventManager. 2021-04-14 16:34:31 +02:00
73f3ba17de Merge branch 'develop' into feature/agenda-updates 2021-04-14 14:57:00 +02:00
07fb1e0e12 [UI] Refactor dropdown inputs code. 2021-04-14 11:59:58 +02:00
297867cbf3 [UI] Add event type colors to type dropdown. 2021-04-14 11:21:45 +02:00
db598af28a [UI] Add legend in event details dialog. 2021-04-14 10:20:00 +02:00
ec765c9070 [UI] Fix updating event dialog when editing or removing. 2021-04-14 10:16:22 +02:00
5eaa754401 [UI] Add icons for done and manual events. 2021-04-13 20:50:51 +02:00
b48b5589f4 [UI] Add eggfall. (#22)
* 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 <noreply@github.com>
Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-04-12 17:18:53 +02:00
634ef16bc5 [UI] Add notification icons. (#23)
* 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 <kuba@szczodrzynski.pl>
2021-04-12 17:17:52 +02:00
ccf0bdaf05 [4.7.1] Update build.gradle, signing and changelog. 2021-04-12 14:35:43 +02:00
4647da7803 [IDE] Fix XML tags reordering when formatting. 2021-04-12 14:27:51 +02:00
613f271c4e Merge pull request #29 from szkolny-eu/hotfix/flavor-aware-availability
[App] Make registerAvailability flavor-aware.
2021-04-12 14:25:49 +02:00
8b1529f240 [App] Make registerAvailability flavor-aware. 2021-04-12 13:13:10 +02:00
3eb09033bf [Strings] Update copyright date (#26)
* Update copyright date

* Update copyright in other languages
2021-04-12 12:26:52 +02:00
12619f6bde [Agenda] Update scroll listeners code. 2021-04-12 11:50:54 +02:00
f5ceaa9afe [Agenda] Add unread badges to events and groups. 2021-04-11 22:08:33 +02:00
777ae945e0 [Agenda] Implement grouping events by type. 2021-04-10 22:26:43 +02:00
3eae8fb58b [Agenda] Add config dialog. Add compact mode. 2021-04-10 20:51:29 +02:00
b14ef5cd78 [Agenda] Limit event text to 3 lines max. 2021-04-10 18:49:24 +02:00
98bf4f3bdc [Agenda] Implement updating event list when changed. 2021-04-09 21:52:04 +02:00
2d6cf50ca7 [Agenda] Refactor agenda UI code. 2021-04-09 15:32:44 +02:00
95baf9fb9c [UI/Dialogs] Add context menu for teachers' names. (#21)
* [UI] Rearrange BetterLink code.

* [UI/Dialogs] Add context menu for teachers' names.

* [Proguard] Update rules for BetterLink reflection.

* [Proguard] Fix rules for BetterLink reflection.
2021-04-08 00:39:59 +02:00
dd0972b528 [Actions] Fix publishing in Google Play release workflow. 2021-04-07 18:42:50 +02:00
d17f6297d3 [4.7] Update build.gradle, signing and changelog. 2021-04-07 18:22:54 +02:00
3ae785a45c [Actions] Limit Google Play changelog to 500 chars. 2021-04-06 21:50:51 +02:00
dd254d4bec [Actions] Fix nightly workflow changes detection. 2021-04-06 18:20:09 +02:00
e04bd75f1f [UI/Login] Add version information in login chooser. 2021-04-06 18:17:24 +02:00
929ccb53db [Strings] Fix invalid format exception in attendance. 2021-04-05 21:59:23 +02:00
72319a4613 [API] Disable API caching for signed builds. 2021-04-05 21:52:38 +02:00
e389e6c073 [4.7-rc.2] Update build.gradle, signing and changelog. 2021-04-05 21:04:29 +02:00
cd6951dcbb [UI] Add highlighting user mentions in changelog. 2021-04-05 21:02:49 +02:00
02d60754b6 [UI] Fix changelog dialog link highlighting. 2021-04-05 20:38:51 +02:00
6884251646 [UI] Fix English translation typo. 2021-04-05 20:12:38 +02:00
582e2059d8 [App] Move buildTimestamp to manifest to improve build performance. 2021-04-05 20:00:38 +02:00
ea2974bfae [App] Improve unofficial build type info on debug. 2021-04-05 19:46:22 +02:00
b8ff649c96 [UI] Move privacy policy dialog to login chooser. 2021-04-05 19:45:57 +02:00
8661ecdafb Finish English translation (#12)
* [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 b925d3137a.

* 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 <kuba@szczodrzynski.pl>
2021-04-05 19:07:25 +02:00
fe8cbc061d Merge pull request #14 from szkolny-eu/feature/readme-update
README update
2021-04-05 19:06:41 +02:00
b4459e1fd4 [Git] Add workflow badges to README. 2021-04-05 13:08:21 +02:00
fd6553871f [Git] Add banner image to README. 2021-04-05 13:00:30 +02:00
a4ca44e1ce [Git] Add README banner image. 2021-04-05 12:58:54 +02:00
e124c429d1 [Actions] Update nightly build schedule. 2021-04-05 11:25:22 +02:00
e9a2dae1e4 [Gradle] Fix moving artifacts to release folder, again. 2021-04-05 10:56:13 +02:00
8b0f3490e3 [UI/Settings] Fix hiding hiding sticks from old without devMode. 2021-04-04 22:41:20 +02:00
131606a6cf Merge pull request #13 from szkolny-eu/feature/gh-actions
Add GitHub Actions
2021-04-04 22:36:55 +02:00
cacafa205e Merge pull request #9 from Zaptyp/patch-1
Correction of spelling and punctuation errors
2021-04-04 22:36:42 +02:00
9c620de1e7 [Actions] Add nightly build workflow. 2021-04-04 21:43:00 +02:00
3e98fb967b [Actions] Add Google Play build workflow. 2021-04-04 21:42:08 +02:00
8db81478f3 [Actions] Add release build workflow. 2021-04-04 21:41:55 +02:00
8f9861bac6 [Actions] Add workflow utilities scripts. 2021-04-04 21:41:36 +02:00
5b35e3500e [Gradle] Fix moving app bundles to release folder. 2021-04-04 20:37:02 +02:00
fc4c297bef [App] Add more info to build details dialog. 2021-04-03 22:22:18 +02:00
e7cb699bcf [App] Fix unofficial build notice formatting. 2021-04-03 15:58:59 +02:00
5301b4efad [Gradle] Add moving app bundles to release folder. 2021-04-03 15:35:00 +02:00
bf595dd09c [App] Fix detecting correct remote repository name. 2021-04-03 15:13:10 +02:00
cb4b168b2a Update README.md 2021-04-02 10:12:13 +02:00
b2fcbb8289 Correction of spelling and punctuation errors
Correction of spelling and punctuation errors and improved text appearance.
2021-04-02 06:55:27 +02:00
223 changed files with 6253 additions and 2021 deletions

BIN
.github/readme-banner.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

2
.github/utils/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
__pycache__/

57
.github/utils/_get_password.py vendored Normal file
View File

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

142
.github/utils/_utils.py vendored Normal file
View File

@ -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"<h3>(.+?)</h3>", changelog).group(1)
content = re.search(r"(?s)<ul>(.+)</ul>", changelog).group(1).strip()
content = "\n".join(line.strip() for line in content.split("\n"))
if format != "html":
content = content.replace("<li>", "- ")
content = content.replace("<br>", "\n")
if format == "markdown":
content = re.sub(r"<u>(.+?)</u>", "__\\1__", content)
content = re.sub(r"<i>(.+?)</i>", "*\\1*", content)
content = re.sub(r"<b>(.+?)</b>", "**\\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"<li>{commit[3]} <i> - {commit[0]}</i></li>")
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)

69
.github/utils/bump_nightly.py vendored Normal file
View File

@ -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 <project dir>")
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=5&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"<h3>(.+?)</h3>", f"<h3>{version_name}</h3>", changelog)
changelog = re.sub(r"(?s)<ul>(.+)</ul>", f"<ul>\n{commit_log}\n</ul>", changelog)
with open(
f"{project_dir}/app/src/main/assets/pl-changelog.html", "w", encoding="utf-8"
) as f:
f.write(changelog)

41
.github/utils/bump_version.py vendored Normal file
View File

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

72
.github/utils/extract_changelogs.py vendored Normal file
View File

@ -0,0 +1,72 @@
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 <project dir>")
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")
# plain text changelog - Firebase App Distribution
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)
# plain text changelog, max 500 chars - Google Play
with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f:
changelog_lines = changelog.split("\n")
changelog = ""
for line in changelog_lines:
if len(changelog) + len(line) < 500:
changelog += "\n" + line
changelog = changelog.strip()
f.write(changelog)
print("::set-output name=changelogPlainFile::" + dir + "whatsnew-pl-PL")
# markdown changelog - Discord webhook
(_, 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")
# html changelog - version info in DB
(_, 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")

26
.github/utils/rename_artifacts.py vendored Normal file
View File

@ -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 <project dir>")
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)

122
.github/utils/save_version.py vendored Normal file
View File

@ -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 <project dir>")
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)

84
.github/utils/sign.py vendored Normal file
View File

@ -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 <project dir> [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,
)

118
.github/utils/webhook_discord.py vendored Normal file
View File

@ -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 <project dir>")
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,
)

153
.github/workflows/build-nightly-apk.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: Nightly build
on:
schedule:
# 23:30 UTC, 0:30 or 1:30 CET/CEST
- cron: "30 23 * * *"
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 == 'true' }}
outputs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- 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

View File

@ -0,0 +1,130 @@
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 11
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- 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 }}
whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }}
- name: Upload workflow artifact
uses: actions/upload-artifact@v2
if: always()
with:
name: ${{ steps.changelog.outputs.appVersionName }}
path: |
app/release/whatsnew*/
app/release/*.apk
app/release/*.aab
app/release/*.json
app/release/*.txt

153
.github/workflows/build-release-apk.yml vendored Normal file
View File

@ -0,0 +1,153 @@
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 11
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- 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

1
.gitignore vendored
View File

@ -265,3 +265,4 @@ fabric.properties
# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,gradle,java,kotlin
signatures/
.idea/*.xml

View File

@ -15,6 +15,7 @@
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -25,6 +26,7 @@
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -36,6 +38,7 @@
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -46,6 +49,7 @@
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -56,6 +60,7 @@
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -66,6 +71,7 @@
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -76,6 +82,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
@ -87,6 +94,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
@ -98,6 +106,7 @@
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>

View File

@ -5,6 +5,7 @@
<w>ciasteczko</w>
<w>csrf</w>
<w>edziennik</w>
<w>elearning</w>
<w>gson</w>
<w>hebe</w>
<w>idziennik</w>

9
.idea/discord.xml generated
View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
</component>
<component name="ProjectNotificationSettings">
<option name="askShowProject" value="false" />
</component>
</project>

6
.idea/kotlinc.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
</project>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -1,9 +1,7 @@
# Szkolny.eu
Nieoficjalna aplikacja do obsługi najpopularniejszych dzienników elektronicznych w Polsce.
<div align="center">
![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)
@ -12,11 +10,15 @@ Nieoficjalna aplikacja do obsługi najpopularniejszych dzienników elektroniczny
[![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)
</div>
## 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&reg; 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&reg; 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 +32,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 +55,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 +70,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.**

View File

@ -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'
@ -18,8 +19,10 @@ android {
versionName release.versionName
buildConfigField "java.util.Map<String, String>", "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
@ -33,6 +36,9 @@ android {
buildTypes {
debug {
minifyEnabled = false
manifestPlaceholders = [
buildTimestamp: 0
]
}
release {
minifyEnabled = true
@ -98,7 +104,10 @@ 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/"),
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'
@ -115,25 +124,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
@ -147,7 +156,8 @@ dependencies {
implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2"
// Szkolny.eu libraries/forks
implementation "eu.szkolny:agendacalendarview:1799f8ef47"
implementation "eu.szkolny:android-snowfall:1ca9ea2da3"
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"
@ -162,10 +172,10 @@ dependencies {
kapt "eu.szkolny.selective-dao:codegen:27f8f3f194"
// Iconics & related
implementation "com.mikepenz:iconics-core:5.3.0-b01"
implementation "com.mikepenz:iconics-views:5.3.0-b01"
implementation "com.mikepenz:iconics-core:5.3.1"
implementation "com.mikepenz:iconics-views:5.3.1"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
implementation "eu.szkolny:szkolny-font:1.3"
implementation "eu.szkolny:szkolny-font:77e33acc2a"
// Other dependencies
implementation "cat.ereza:customactivityoncrash:2.3.0"
@ -175,7 +185,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"

View File

@ -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(", ") + ")"
}

View File

@ -32,8 +32,9 @@
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
-keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); }
-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; }
-keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); }
-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; }
-keepclassmembernames class androidx.appcompat.view.menu.MenuItemImpl { private *; }
-keepclassmembernames class com.mikepenz.materialdrawer.widget.MiniDrawerSliderView { private *; }

View File

@ -29,6 +29,8 @@
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
<meta-data android:name="buildTimestamp" android:value="${buildTimestamp}" />
<!-- __ __ _ _ _ _ _
| \/ | (_) /\ | | (_) (_) |
| \ / | __ _ _ _ __ / \ ___| |_ ___ ___| |_ _ _
@ -144,6 +146,7 @@
android:configChanges="orientation|keyboardHidden"
android:theme="@style/Base.Theme.AppCompat" />
<activity android:name=".ui.modules.base.BuildInvalidActivity" />
<activity android:name=".ui.modules.settings.contributors.ContributorsActivity" />
<!-- _____ _
| __ \ (_)

View File

@ -1,14 +1,8 @@
<h3>Wersja 4.7-rc.1, 2021-04-01</h3>
<h3>Wersja 4.10, 2021-09-22</h3>
<ul>
<li><u>Szkolny.eu jest teraz open source!</u> Zapraszamy na stronę https://szkolny.eu/ po więcej ważnych informacji.</li>
<li>Poprawiono wybieranie obrazków (tła nagłówka, tła aplikacji oraz profilu) z dowolnego źródła.</li>
<li>Naprawiono zatrzymanie aplikacji na Androidzie 4.4 i starszych.</li>
<li>Naprawiono problemy z połączeniem internetowym na Androidzie 4.4 i starszych.</li>
<li>Dodano ekran informacji o kompilacji w Ustawieniach.</li>
<li>Zaktualizowano ekran licencji open source.</li>
<li>Zoptymalizowano wielkość aplikacji.</li>
<li>Dodano wyświetlanie informacji o frekwencji w planie lekcji. @Antoni-Czaplicki</li>
</ul>
<br>
<br>
Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; Kuba Szczodrzyński, Kacper Ziubryniewicz 2021</i>
<i>&copy; [Kuba Szczodrzyński](@kuba2k2), [Kacper Ziubryniewicz](@kapi2289) 2021</i>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0xdd, 0x0a, 0x72, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0xda, 0x2a, 0x5f, 0xbe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -58,6 +58,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val profileId
get() = profile.id
var enableChucker = false
var debugMode = false
var devMode = false
}
@ -70,6 +71,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val permissionManager by lazy { PermissionManager(this) }
val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) }
val db
get() = App.db
@ -115,9 +117,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()
@ -171,7 +175,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
App.config = Config(App.db)
App.profile = Profile(0, 0, 0, "")
debugMode = BuildConfig.DEBUG
devMode = config.debugMode || debugMode
devMode = config.devMode ?: debugMode
enableChucker = config.enableChucker ?: devMode
if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) }

View File

@ -14,7 +14,9 @@ import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
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 +554,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
}
@ -718,6 +738,7 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
is Short -> putShort(property.first, property.second as Short)
is Double -> putDouble(property.first, property.second as Double)
is Boolean -> putBoolean(property.first, property.second as Boolean)
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
}
}
}

View File

@ -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,11 @@ 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.managers.AvailabilityManager
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget
import pl.szczodrzynski.navlib.*
@ -470,9 +470,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) {
@ -624,45 +636,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
return
}
app.profile.registerName?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheckAt < currentTimeUnix()) {
val api = SzkolnyApi(app)
val result = withContext(Dispatchers.IO) {
return@withContext api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
availability[registerName]
}, onError = {
if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) {
return@withContext false
}
return@withContext it
})
}
when (result) {
false -> {
Toast.makeText(this@MainActivity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
return@let
}
is Throwable -> {
errorSnackbar.addError(result.toApiError(TAG)).show()
return
}
is RegisterAvailabilityStatus -> {
status = result
}
}
}
if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) {
val error = withContext(Dispatchers.IO) {
app.availabilityManager.check(app.profile)
}
when (error?.type) {
Type.NOT_AVAILABLE -> {
swipeRefreshLayout.isRefreshing = false
loadTarget(DRAWER_ITEM_HOME)
if (status != null)
RegisterUnavailableDialog(this, status)
RegisterUnavailableDialog(this, error.status!!)
return
}
Type.API_ERROR -> {
errorSnackbar.addError(error.apiError!!).show()
return
}
Type.NO_API_ACCESS -> {
Toast.makeText(this, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
}
}
swipeRefreshLayout.isRefreshing = true
@ -689,10 +679,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event)
app.profile.registerName?.let { registerName ->
event.data[registerName]?.let {
RegisterUnavailableDialog(this, it)
}
val error = app.availabilityManager.check(app.profile, cacheOnly = true)
if (error != null) {
RegisterUnavailableDialog(this, error.status!!)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)

View File

@ -12,10 +12,7 @@ import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.config.utils.ConfigMigration
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.toHashMap
import pl.szczodrzynski.edziennik.config.utils.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.AppDb
import kotlin.coroutines.CoroutineContext
@ -75,10 +72,15 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mPrivacyPolicyAccepted = mPrivacyPolicyAccepted ?: values.get("privacyPolicyAccepted", false); return mPrivacyPolicyAccepted ?: false }
set(value) { set("privacyPolicyAccepted", value); mPrivacyPolicyAccepted = value }
private var mDebugMode: Boolean? = null
var debugMode: Boolean
get() { mDebugMode = mDebugMode ?: values.get("debugMode", false); return mDebugMode ?: false }
set(value) { set("debugMode", value); mDebugMode = value }
private var mDevMode: Boolean? = null
var devMode: Boolean?
get() { mDevMode = mDevMode ?: values.getBooleanOrNull("debugMode"); return mDevMode }
set(value) { set("debugMode", value?.toString()); mDevMode = value }
private var mEnableChucker: Boolean? = null
var enableChucker: Boolean?
get() { mEnableChucker = mEnableChucker ?: values.getBooleanOrNull("enableChucker"); return mEnableChucker }
set(value) { set("enableChucker", value?.toString()); mEnableChucker = value }
private var mDevModePassword: String? = null
var devModePassword: String?
@ -120,6 +122,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert }
set(value) { set("apiInvalidCert", value); mApiInvalidCert = value }
private var mApiAvailabilityCheck: Boolean? = null
var apiAvailabilityCheck: Boolean
get() { mApiAvailabilityCheck = mApiAvailabilityCheck ?: values.get("apiAvailabilityCheck", true); return mApiAvailabilityCheck ?: true }
set(value) { set("apiAvailabilityCheck", value); mApiAvailabilityCheck = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {

View File

@ -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<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus>
get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson<Map<String, RegisterAvailabilityStatus>>(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.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<Map<String, RegisterAvailabilityStatus>>(){}.type)
}
return mRegisterAvailability ?: mapOf()
}
set(value) {
config.setMap("registerAvailability", value)
config.set("registerAvailabilityFlavor", BuildConfig.FLAVOR)
mRegisterAvailability = value
}
}

View File

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

View File

@ -15,8 +15,58 @@ 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<HomeCardModel>? = null
var homeCards: List<HomeCardModel>
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 }
}

View File

@ -59,6 +59,9 @@ fun HashMap<String, String?>.get(key: String, default: String?): String? {
fun HashMap<String, String?>.get(key: String, default: Boolean): Boolean {
return this[key]?.toBoolean() ?: default
}
fun HashMap<String, String?>.getBooleanOrNull(key: String): Boolean? {
return this[key]?.toBooleanStrictOrNull()
}
fun HashMap<String, String?>.get(key: String, default: Int): Int {
return this[key]?.toIntOrNull() ?: default
}

View File

@ -67,7 +67,7 @@ class ConfigMigration(app: App, config: Config) {
if (dataVersion < 3) {
update = null
privacyPolicyAccepted = false
debugMode = false
devMode = null
devModePassword = null
appInstalledTime = 0L
appRateSnackbarTime = 0L

View File

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

View File

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

View File

@ -117,6 +117,17 @@ object Regexes {
}
val MOBIDZIENNIK_TIMETABLE_TOP by lazy {
"""<div class="plansc_top">.+?</div></div>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_TIMETABLE_CELL by lazy {
"""<div class="plansc_cnt_w" style="(.+?)">.+?style="(.+?)".+?title="(.+?)".+?>\s+(.+?)\s+</div>""".toRegex(DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_TIMETABLE_LEFT by lazy {
"""<div class="plansc_godz">.+?</div></div>""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
"""<input type="hidden".+?name="([A-z0-9_]+)?".+?value="([A-z0-9_+-/=]+)?".+?>""".toRegex(DOT_MATCHES_ALL)

View File

@ -18,7 +18,6 @@ import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
@ -27,6 +26,7 @@ import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object {
@ -90,35 +90,21 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
return
}
profile.registerName?.also { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheckAt < currentTimeUnix()) {
val api = SzkolnyApi(app)
api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
status = availability[registerName]
}, onError = {
val apiError = it.toApiError(TAG)
if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) {
return@also
}
taskCallback.onError(apiError)
return
})
}
if (status?.available != true
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
val error = app.availabilityManager.check(profile)
when (error?.type) {
Type.NOT_AVAILABLE -> {
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky(
RegisterAvailabilityEvent(app.config.sync.registerAvailability)
)
EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
}
cancel()
taskCallback.onCompleted()
return
}
Type.API_ERROR -> {
taskCallback.onError(error.apiError!!)
return
}
else -> return@let
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

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

View File

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

View File

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

View File

@ -0,0 +1,340 @@
/*
* 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<ClosedFloatingPointRange<Float>, Date>()
private val hoursV = mutableMapOf<Int, Pair<Time, Int?>>()
private var startDate: Date
private fun parseCss(css: String): Map<String, String> {
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<String>, 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<String> {
return str
.replace(whitespaceRegex, " ")
.replace("\n", "")
.replace("&lt;small&gt;", "$")
.replace("&lt;/small&gt;", "$")
.replace("&lt;br /&gt;", "\n")
.replace("&lt;br/&gt;", "\n")
.replace("&lt;br&gt;", "\n")
.replace("<br />", "\n")
.replace("<br/>", "\n")
.replace("<br>", "\n")
.replace("<b>", "%")
.replace("</b>", "%")
.replace("<span>", "")
.replace("</span>", "")
.split("\n")
.map { it.trim() }
}
@SuppressLint("LongLogTag", "LogNotTimber")
private fun readLessons(html: String) {
val matches = Regexes.MOBIDZIENNIK_TIMETABLE_CELL.findAll(html)
val noLessonDays = mutableListOf<Date>()
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<String?>()
items.forEachIndexed { i, item ->
when {
item.isEmpty() ->
toRemove.add(item)
item.contains(":") && item.contains(" - ") ->
toRemove.add(item)
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)
}
item.startsWith("&") -> {
typeName = item.trim('&')
toRemove.add(item)
}
typeName != null && (item.contains(typeName!!) || item.contains("</small>")) -> {
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
}
}
}
}

View File

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

View File

@ -36,7 +36,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List<JsonObject>)
}
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()

View File

@ -34,7 +34,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
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()

View File

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

View File

@ -43,14 +43,21 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List<JsonObject>) {
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,4 @@
package pl.szczodrzynski.edziennik.data.api.events
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
data class RegisterAvailabilityEvent(
val data: Map< String, RegisterAvailabilityStatus>
)
class RegisterAvailabilityEvent()

View File

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

View File

@ -22,10 +22,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.ApiCacheIntercept
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.data.api.szkolny.request.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.*
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Notification
@ -373,6 +370,15 @@ class SzkolnyApi(val app: App) : CoroutineScope {
throw SzkolnyApiException(null)
}
@Throws(Exception::class)
fun getContributors(): ContributorsResponse {
val response = api.contributors().execute()
if (response.isSuccessful && response.body() != null) {
return parseResponse(response)
}
throw SzkolnyApiException(null)
}
@Throws(Exception::class)
fun getFirebaseToken(registerName: String): String {
val response = api.firebaseToken(registerName).execute()

View File

@ -27,6 +27,9 @@ interface SzkolnyService {
@POST("appUser")
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Unit>>
@GET("contributors/android")
fun contributors(): Call<ApiResponse<ContributorsResponse>>
@GET("updates/app")
fun updates(@Query("channel") channel: String = "release"): Call<ApiResponse<List<Update>>>

View File

@ -15,6 +15,7 @@ class ApiCacheInterceptor(val app: App) : Interceptor {
val request = chain.request()
if (request.url().host() == "api.szkolny.eu"
&& Signing.appCertificate.md5() == app.config.apiInvalidCert
&& !app.buildManager.isSigned
) {
val response = ApiResponse<Unit>(
success = false,

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDy+5jm3L0===.$param2".sha256()
return "$param1.MTIzNDU2Nzg5MDY8+Uq3So===.$param2".sha256()
}
}

View File

@ -0,0 +1,20 @@
package pl.szczodrzynski.edziennik.data.api.szkolny.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
data class ContributorsResponse(
val contributors: List<Item>,
val translators: List<Item>
) {
@Parcelize
data class Item(
val login: String,
val name: String?,
val avatarUrl: String,
val profileUrl: String,
val itemUrl: String,
val contributions: Int?
) : Parcelable
}

View File

@ -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<Notification>, val profiles: List<Profile>) {
companion object {
@ -42,13 +43,22 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
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<Notification>,
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<Notification>,
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<Notification>,
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<Notification>,
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<Notification>,
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,

View File

@ -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<AppNotification>) {
.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,8 +144,11 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.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))
.bigText(it.textLong ?: it.text))
.setWhen(it.addedDate)
.addDefaults()
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
@ -160,6 +170,10 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.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)

View File

@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
LibrusLesson::class,
TimetableManual::class,
Metadata::class
], version = 91)
], version = 93)
@TypeConverters(
ConverterTime::class,
ConverterDate::class,
@ -176,7 +176,9 @@ abstract class AppDb : RoomDatabase() {
Migration88(),
Migration89(),
Migration90(),
Migration91()
Migration91(),
Migration92(),
Migration93()
).allowMainThreadQueries().build()
}
}

View File

@ -64,6 +64,8 @@ abstract class AttendanceDao : BaseDao<Attendance, AttendanceFull> {
getRawNow("$QUERY WHERE notified = 0 $ORDER_BY")
fun getNotNotifiedNow(profileId: Int) =
getRawNow("$QUERY WHERE attendances.profileId = $profileId AND notified = 0 $ORDER_BY")
fun getAllByDateNow(profileId: Int, date: Date) =
getRawNow("$QUERY WHERE attendances.profileId = $profileId AND attendanceDate = '${date.stringY_m_d}' $ORDER_BY")
// GET ONE - NOW
fun getByIdNow(profileId: Int, id: Long) =

View File

@ -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<EventType>
fun addDefaultTypes(context: Context, profileId: Int): List<EventType> {
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
}

View File

@ -84,6 +84,8 @@ abstract class TimetableDao : BaseDao<Lesson, LessonFull> {
"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) =

View File

@ -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")
@ -116,14 +116,7 @@ open class Event(
var showAsUnseen: Boolean? = null
val startTimeCalendar: Calendar
get() = Calendar.getInstance().also { it.set(
date.year,
date.month - 1,
date.day,
time?.hour ?: 0,
time?.minute ?: 0,
time?.second ?: 0
) }
get() = date.getAsCalendar(time)
val endTimeCalendar: Calendar
get() = startTimeCalendar.also {

View File

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

View File

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

View File

@ -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")
@ -19,6 +21,7 @@ data class Notification(
val title: String,
val text: String,
val textLong: String? = null,
val type: Int,
@ -96,4 +99,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
}
}

View File

@ -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"
@ -137,7 +140,7 @@ open class Profile(
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null
else -> "unknown"
}
override fun getImageDrawable(context: Context): Drawable {

View File

@ -30,7 +30,7 @@ class MessageFull(
@Ignore
var filterWeight = 0
@Ignore
var searchHighlightText: String? = null
var searchHighlightText: CharSequence? = null
// metadata
var seen = false

View File

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

View File

@ -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;")
}
}

View File

@ -60,7 +60,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
) ?: return@launch
app.config.sync.registerAvailability = data
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky(RegisterAvailabilityEvent(data))
EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
}
}
}

View File

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

View File

@ -42,8 +42,6 @@ class RegisterUnavailableDialog(
init { run {
if (activity.isFinishing)
return@run
if (status.available && status.minVersionCode <= BuildConfig.VERSION_CODE)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App

View File

@ -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() {
}
}

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod
import kotlin.coroutines.CoroutineContext
class ChangelogDialog(
@ -43,9 +44,14 @@ class ChangelogDialog(
val textView = TextView(activity)
textView.setPadding(24.dp, 24.dp, 24.dp, 0)
val text = app.assets.open("pl-changelog.html").bufferedReader().use {
var text = app.assets.open("pl-changelog.html").bufferedReader().use {
it.readText()
}
val commitsUrlPrefix = "https://github.com/szkolny-eu/szkolny-android/commits?author="
text = text.replace("""\[(.+?)]\(@([A-z0-9-]+)\)""".toRegex(), "<a href=\"$commitsUrlPrefix$2\">$1</a>")
text = text.replace("""\s@([A-z0-9-]+)""".toRegex(), " <a href=\"$commitsUrlPrefix$1\">@$1</a>")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
textView.text = Html.fromHtml(text)
}
@ -53,6 +59,8 @@ class ChangelogDialog(
textView.text = Html.fromHtml(text.replace("<li>", "<br><li> - "))
}
textView.movementMethod = BetterLinkMovementMethod.getInstance()
val scrollView = ScrollView(activity)
scrollView.addView(textView)
@ -67,4 +75,4 @@ class ChangelogDialog(
}
.show()
}}
}
}

View File

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

View File

@ -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 {
@ -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
@ -92,6 +94,10 @@ class EventDetailsDialog(
b.eventShared = eventShared
b.eventOwn = eventOwn
if (!event.seen) {
manager.markAsSeen(event)
}
val bullet = ""
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
@ -100,6 +106,8 @@ class EventDetailsDialog(
}
catch (_: Exception) {}
manager.setLegendText(b.legend, event)
b.typeColor.background?.setTintColor(event.eventColor)
b.details = mutableListOf(
@ -135,6 +143,7 @@ class EventDetailsDialog(
launch(Dispatchers.Default) {
app.db.eventDao().replace(event)
}
update()
b.checkDoneButton.isChecked = true
}
.setNegativeButton(R.string.cancel, null)
@ -145,6 +154,7 @@ class EventDetailsDialog(
launch(Dispatchers.Default) {
app.db.eventDao().replace(event)
}
update()
}
}
b.checkDoneButton.attachToastHint(R.string.hint_mark_as_done)
@ -156,6 +166,14 @@ class EventDetailsDialog(
activity,
event.profileId,
editingEvent = event,
onSaveListener = {
if (it == null) {
dialog.dismiss()
return@EventManualDialog
}
event = it
update()
},
onShowListener = onShowListener,
onDismissListener = onDismissListener
)
@ -199,10 +217,14 @@ class EventDetailsDialog(
}
b.downloadButton.attachToastHint(R.string.hint_download_again)
BetterLink.attach(b.topic, onActionSelected = dialog::dismiss)
b.topic.text = event.topic
BetterLink.attach(b.topic) {
dialog.dismiss()
event.teacherName?.let { name ->
BetterLink.attach(
b.teacherName,
teachers = mapOf(event.teacherId to name),
onActionSelected = dialog::dismiss
)
}
if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) {
@ -220,10 +242,7 @@ class EventDetailsDialog(
b.bodyTitle.isVisible = true
b.bodyProgressBar.isVisible = false
b.body.isVisible = true
b.body.text = event.homeworkBody
BetterLink.attach(b.body) {
dialog.dismiss()
}
BetterLink.attach(b.body, onActionSelected = dialog::dismiss)
}
if (event.attachmentIds.isNullOrEmpty() || event.attachmentNames.isNullOrEmpty()) {
@ -322,8 +341,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 {

View File

@ -33,7 +33,8 @@ class EventListAdapter(
) : RecyclerView.Adapter<EventListAdapter.ViewHolder>(), 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<CharSequence?>(
@ -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

View File

@ -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
@ -20,23 +18,18 @@ 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
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.entity.*
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
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
@ -49,6 +42,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 {
@ -323,57 +317,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 +359,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
}
})
@ -416,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
@ -451,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
@ -487,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
@ -498,7 +476,7 @@ class EventManualDialog(
val metadataObject = Metadata(
profileId,
when (type) {
when (type?.id) {
Event.TYPE_HOMEWORK -> Metadata.TYPE_HOMEWORK
else -> Metadata.TYPE_EVENT
},
@ -597,10 +575,14 @@ class EventManualDialog(
}
}
onSaveListener?.invoke(eventObject.withMetadata(metadataObject).also {
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()
if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA)
activity.reloadTarget()
}
private fun finishRemoving() {
editingEvent ?: return
@ -611,9 +593,8 @@ class EventManualDialog(
}
removeEventDialog?.dismiss()
onSaveListener?.invoke(null)
dialog.dismiss()
Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show()
if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA)
activity.reloadTarget()
}
}

View File

@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext
@ -68,6 +69,14 @@ class GradeDetailsDialog(
GradesConfigDialog(activity, reloadOnDismiss = true)
}
grade.teacherName?.let { name ->
BetterLink.attach(
b.teacherName,
teachers = mapOf(grade.teacherId to name),
onActionSelected = dialog::dismiss
)
}
launch {
val historyList = withContext(Dispatchers.Default) {
app.db.gradeDao().getByParentIdNow(App.profileId, grade.id)

View File

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

View File

@ -8,15 +8,20 @@ import android.content.Intent
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
import pl.szczodrzynski.edziennik.onClick
@ -24,7 +29,9 @@ import pl.szczodrzynski.edziennik.setText
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
@ -33,6 +40,7 @@ import kotlin.coroutines.CoroutineContext
class LessonDetailsDialog(
val activity: AppCompatActivity,
val lesson: LessonFull,
val attendance: AttendanceFull? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
@ -49,7 +57,10 @@ class LessonDetailsDialog(
get() = job + Dispatchers.Main
private lateinit var adapter: EventListAdapter
private val manager by lazy { app.timetableManager }
private val manager
get() = app.timetableManager
private val attendanceManager
get() = app.attendanceManager
init { run {
if (activity.isFinishing)
@ -168,6 +179,27 @@ class LessonDetailsDialog(
b.teamName = lesson.teamName
}
b.attendanceDivider.isVisible = attendance != null
b.attendanceLayout.isVisible = attendance != null
if (attendance != null) {
b.attendanceView.setAttendance(attendance, app.attendanceManager, bigView = true)
b.attendanceType.text = attendance.typeName
b.attendanceIcon.isVisible = attendance.let {
val icon = attendanceManager.getAttendanceIcon(it) ?: return@let false
val color = attendanceManager.getAttendanceColor(it)
b.attendanceIcon.setImageDrawable(
IconicsDrawable(activity, icon).apply {
colorInt = color
sizeDp = 24
}
)
true
}
b.attendanceDetails.onClick {
AttendanceDetailsDialog(activity, attendance, onShowListener, onDismissListener)
}
}
adapter = EventListAdapter(
activity,
showWeekDay = false,
@ -216,5 +248,19 @@ class LessonDetailsDialog(
b.eventsNoData.visibility = View.VISIBLE
}
})
lesson.displayTeacherName?.let { name ->
lesson.displayTeacherId ?: return@let
BetterLink.attach(
b.teacherNameView,
teachers = mapOf(lesson.displayTeacherId!! to name),
onActionSelected = dialog::dismiss
)
BetterLink.attach(
b.oldTeacherNameView,
teachers = mapOf(lesson.displayTeacherId!! to name),
onActionSelected = dialog::dismiss
)
}
}
}

View File

@ -12,10 +12,6 @@ import android.widget.Toast
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.applandeo.materialcalendarview.EventDay
import com.github.tibolte.agendacalendarview.CalendarPickerController
import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent
import com.github.tibolte.agendacalendarview.models.CalendarEvent
import com.github.tibolte.agendacalendarview.models.IDayItem
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
@ -25,21 +21,14 @@ 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
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.ui.dialogs.lessonchange.LessonChangeDialog
import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeCounter
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEvent
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEventRenderer
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceCounter
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
@ -59,7 +48,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
get() = job + Dispatchers.Main
private var type: Int = Profile.AGENDA_DEFAULT
private var actualDate: Date? = null
private var agendaDefault: AgendaFragmentDefault? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (getActivity() == null || context == null) return null
@ -82,38 +72,61 @@ class AgendaFragment : Fragment(), CoroutineScope {
.withTitle(R.string.menu_add_event)
.withDescription(R.string.menu_add_event_desc)
.withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline)
.withOnClickListener(View.OnClickListener {
.withOnClickListener {
activity.bottomSheet.close()
EventManualDialog(activity, app.profileId, defaultDate = actualDate)
}),
EventManualDialog(
activity,
app.profileId,
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)
.withOnClickListener(View.OnClickListener {
.withOnClickListener {
activity.bottomSheet.close()
type = if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT
type =
if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT
app.config.forProfile().ui.agendaViewType = type
activity.reloadTarget()
}),
},
BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(View.OnClickListener { launch {
activity.bottomSheet.close()
withContext(Dispatchers.Default) {
App.db.metadataDao().setAllSeen(app.profileId, Metadata.TYPE_EVENT, true)
.withOnClickListener {
launch {
activity.bottomSheet.close()
withContext(Dispatchers.Default) {
App.db.metadataDao()
.setAllSeen(app.profileId, Metadata.TYPE_EVENT, true)
}
Toast.makeText(
activity,
R.string.main_menu_mark_as_read_success,
Toast.LENGTH_SHORT
).show()
}
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
}})
}
)
activity.navView.bottomBar.fabEnable = true
activity.navView.bottomBar.fabExtendedText = getString(R.string.add)
activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon3.cmd_plus
activity.navView.setFabOnClickListener(View.OnClickListener {
EventManualDialog(activity, app.profileId, defaultDate = actualDate)
})
activity.navView.setFabOnClickListener {
EventManualDialog(
activity,
app.profileId,
defaultDate = AgendaFragmentDefault.selectedDate
)
}
activity.gainAttention()
activity.gainAttentionFAB()
@ -124,151 +137,30 @@ 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)
val eventList = mutableListOf<CalendarEvent>()
val minDate = Calendar.getInstance().apply {
add(Calendar.MONTH, -2)
set(Calendar.DAY_OF_MONTH, 1)
}
val maxDate = Calendar.getInstance().apply { add(Calendar.MONTH, 2) }
/**
* LESSON CHANGES
*/
if (!isAdded)
return@launch
val lessons = withContext(Dispatchers.Default) { app.db.timetableDao().getChangesNow(app.profileId) }
val lessonChangeCounters = mutableListOf<LessonChangeCounter>()
lessons.forEach { lesson ->
lessonChangeCounters.firstOrNull { it.lessonChangeDate == lesson.displayDate }?.let {
it.lessonChangeCount += 1
} ?: run {
lessonChangeCounters.add(LessonChangeCounter(
lesson.displayDate ?: return@forEach,
1
))
}
}
lessonChangeCounters.forEach { counter ->
eventList.add(LessonChangeEvent(
counter.lessonChangeDate.inMillis,
0xff78909c.toInt(),
Colors.legibleTextColor(0xff78909c.toInt()),
counter.startTime,
counter.endTime,
app.profileId,
counter.lessonChangeDate,
counter.lessonChangeCount
))
}
/**
* TEACHER ABSENCES
*/
if (!isAdded)
return@launch
val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true)
if (showTeacherAbsences) {
val teacherAbsenceList = withContext(Dispatchers.Default) { app.db.teacherAbsenceDao().getAllNow(app.profileId) }
val teacherAbsenceCounters = mutableListOf<TeacherAbsenceCounter>()
teacherAbsenceList.forEach { absence ->
val date = absence.dateFrom.clone()
while (date <= absence.dateTo) {
teacherAbsenceCounters.firstOrNull { it.teacherAbsenceDate == date }?.let {
it.teacherAbsenceCount += 1
} ?: run {
teacherAbsenceCounters.add(TeacherAbsenceCounter(date.clone(), 1))
}
date.stepForward(0, 0, 1)
}
}
teacherAbsenceCounters.forEach { counter ->
eventList.add(TeacherAbsenceEvent(
counter.teacherAbsenceDate.inMillis,
0xffff1744.toInt(),
Colors.legibleTextColor(0xffff1744.toInt()),
counter.startTime,
counter.endTime,
app.profileId,
counter.teacherAbsenceDate,
counter.teacherAbsenceCount
))
}
}
/**
* EVENTS
*/
if (!isAdded)
return@launch
val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) }
val unreadEventDates = mutableSetOf<Int>()
events.forEach { event ->
eventList.add(BaseCalendarEvent(
"${event.typeName ?: "wydarzenie"} - ${event.topic}",
"",
(if (event.time == null) getString(R.string.agenda_event_all_day) else event.time!!.stringHM) +
(event.subjectLongName?.let { ", $it" } ?: "") +
(event.teacherName?.let { ", $it" } ?: "") +
(event.teamName?.let { ", $it" } ?: ""),
event.eventColor,
Colors.legibleTextColor(event.eventColor),
event.startTimeCalendar,
event.endTimeCalendar,
event.time == null,
event.id,
!event.seen
))
if (!event.seen) unreadEventDates.add(event.date.value)
}
b.agendaDefaultView.init(eventList, minDate, maxDate, Locale.getDefault(), object : CalendarPickerController {
override fun onDaySelected(dayItem: IDayItem?) {}
override fun onScrollToDate(calendar: Calendar) { this@AgendaFragment.launch {
val date = Date.fromCalendar(calendar)
actualDate = date
// Mark as read scrolled date
if (date.value in unreadEventDates) {
withContext(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, date, true) }
unreadEventDates.remove(date.value)
}
}}
override fun onEventSelected(event: CalendarEvent) {
val date = Date.fromCalendar(event.instanceDay)
when (event) {
is BaseCalendarEvent -> DayDialog(activity, app.profileId, date)
is LessonChangeEvent -> LessonChangeDialog(activity, app.profileId, date)
is TeacherAbsenceEvent -> TeacherAbsenceDialog(activity, app.profileId, date)
}
}
}, LessonChangeEventRenderer(), TeacherAbsenceEventRenderer())
b.progressBar.visibility = View.GONE
agendaDefault = AgendaFragmentDefault(activity, app, b)
agendaDefault?.initView(this@AgendaFragment)
}}}
private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch {
checkEventTypes()
delay(300)
val dayList = mutableListOf<EventDay>()

View File

@ -0,0 +1,310 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-4-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
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.*
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.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
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
import pl.szczodrzynski.edziennik.utils.models.Date
import java.util.*
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<Int>()
private val events = mutableListOf<CalendarEvent>()
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()
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.
*/
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
}
suspend fun initView(fragment: AgendaFragment) {
isInitialized = false
withContext(Dispatchers.Default) {
if (profileConfig.agendaLessonChanges)
addLessonChanges(events)
if (profileConfig.agendaTeacherAbsence)
addTeacherAbsence(events)
}
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
val isCompactMode = profileConfig.agendaCompactMode
b.agendaDefaultView.init(
events,
dateStart,
dateEnd,
Locale.getDefault(),
object : CalendarPickerController {
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)
when (event) {
is AgendaEvent -> EventDetailsDialog(activity, event.event)
is LessonChangesEvent -> LessonChangeDialog(activity, app.profileId, date)
is TeacherAbsenceEvent -> TeacherAbsenceDialog(
activity,
app.profileId,
date
)
is AgendaEventGroup -> DayDialog(activity, app.profileId, date, eventTypeId = event.typeId)
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) {
selectedDate = Date.fromCalendar(calendar)
// Mark as read scrolled date
if (selectedDate.value in unreadDates) {
setAsRead(calendar)
activity.launch(Dispatchers.Default) {
app.db.eventDao().setSeenByDate(app.profileId, selectedDate, true)
}
unreadDates.remove(selectedDate.value)
}
}
},
AgendaEventRenderer(app.eventManager, isCompactMode),
AgendaEventGroupRenderer(),
LessonChangesEventRenderer(),
TeacherAbsenceEventRenderer()
)
listView.setOnScrollListener(this)
isInitialized = true
b.progressBar.isVisible = false
}
private fun updateView() {
manager.events.clear()
manager.loadEvents(events, BaseCalendarEvent())
adapter?.updateEvents(manager.events)
//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<CalendarEvent>,
eventList: List<EventFull>
) {
events.removeAll { it is AgendaEvent || it is AgendaEventGroup }
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,
typeId = event.type,
typeName = event.typeName ?: "-",
typeColor = event.typeColor ?: event.eventColor,
count = list.size,
showBadge = list.any { !it.seen }
))
}
}
}
private fun addLessonChanges(events: MutableList<CalendarEvent>) {
val lessons = app.db.timetableDao().getChangesNow(app.profileId)
val grouped = lessons.groupBy {
it.displayDate
}
events += grouped.mapNotNull { (date, changes) ->
LessonChangesEvent(
app.profileId,
date = date ?: return@mapNotNull null,
count = changes.size,
showBadge = changes.any { !it.seen }
)
}
}
private fun addTeacherAbsence(events: MutableList<CalendarEvent>) {
val teacherAbsence = app.db.teacherAbsenceDao().getAllNow(app.profileId)
val countMap = SparseIntArray()
for (absence in teacherAbsence) {
while (absence.dateFrom <= absence.dateTo) {
countMap[absence.dateFrom.value] += 1
absence.dateFrom.stepForward(0, 0, 1)
}
}
countMap.forEach { dateInt, count ->
events += TeacherAbsenceEvent(
app.profileId,
date = Date.fromValue(dateInt),
count = count
)
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-4-9.
*/
package pl.szczodrzynski.edziennik.ui.modules.agenda
import com.github.tibolte.agendacalendarview.models.CalendarEvent
import com.github.tibolte.agendacalendarview.models.IDayItem
import com.github.tibolte.agendacalendarview.models.IWeekItem
import java.util.*
open class BaseEvent(
private val id: Long,
private val time: Calendar,
private val color: Int,
private var showBadge: Boolean,
var showItemBadge: Boolean = showBadge
) : CalendarEvent {
override fun copy() = BaseEvent(id, time, color, showBadge)
private lateinit var date: Calendar
override fun getInstanceDay() = date
override fun setInstanceDay(value: Calendar) {
date = value
}
private lateinit var dayReference: IDayItem
override fun getDayReference() = dayReference
override fun setDayReference(value: IDayItem) {
dayReference = value
}
private lateinit var weekReference: IWeekItem
override fun getWeekReference() = weekReference
override fun setWeekReference(value: IWeekItem) {
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
override fun getTitle() = ""
override fun getDescription() = ""
override fun getLocation() = ""
override fun getColor() = color
override fun getTextColor() = 0
override fun isPlaceholder() = false
override fun isAllDay() = false
override fun setId(value: Long) = Unit
override fun setStartTime(value: Calendar) = Unit
override fun setEndTime(value: Calendar) = Unit
override fun setTitle(value: String) = Unit
override fun setDescription(value: String) = Unit
override fun setLocation(value: String) = Unit
override fun setTextColor(value: Int) = Unit
override fun setPlaceholder(value: Boolean) = Unit
override fun setAllDay(value: Boolean) = Unit
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
*/
package pl.szczodrzynski.edziennik.ui.modules.agenda.event
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent
class AgendaEvent(
val event: EventFull,
showBadge: Boolean = !event.seen
) : BaseEvent(
id = event.id,
time = event.startTimeCalendar,
color = event.eventColor,
showBadge = showBadge
) {
override fun copy() = AgendaEvent(event, showBadge)
}

View File

@ -0,0 +1,25 @@
/*
* 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 typeId: Long,
val typeName: String,
val typeColor: Int,
val count: Int,
showBadge: Boolean
) : BaseEvent(
id = date.value.toLong(),
time = date.asCalendar,
color = typeColor,
showBadge = showBadge
) {
override fun copy() = AgendaEventGroup(profileId, date, typeId, typeName, typeColor, count, showBadge)
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-4-10.
*/
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
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.edziennik.utils.Colors
class AgendaEventGroupRenderer : EventRenderer<AgendaEventGroup>() {
override fun render(view: View, event: AgendaEventGroup) {
val b = AgendaWrappedGroupBinding.bind(view).item
b.card.foreground.setTintColor(event.color)
b.card.background.setTintColor(event.color)
b.name.text = event.typeName
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
}

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