mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2025-06-15 15:10:17 +02:00
Compare commits
45 Commits
v4.13.2-rc
...
develop-v4
Author | SHA1 | Date | |
---|---|---|---|
96da698551 | |||
ee30232530 | |||
988d7cac76 | |||
2f029c096f | |||
908959f7ee | |||
d1ae14a65c | |||
541979dcd6 | |||
f65d01de1b | |||
02a9724587 | |||
2681794676 | |||
42e59ac0db | |||
cac98ee3d4 | |||
aeecc48639 | |||
db444d89f0 | |||
29971777a7 | |||
88cd18b8c6 | |||
30a77f1a99 | |||
6de7ee9cee | |||
d44b85073a | |||
514fbafd00 | |||
c35222cdfd | |||
1e7dbba995 | |||
0b8f3fe94b | |||
0e8b0673ca | |||
cefb0deba8 | |||
90a151c129 | |||
9fd9721ae7 | |||
ceca75ef4b | |||
21c00bbe53 | |||
db00566ebf | |||
07ab1b984f | |||
8177d4aa2d | |||
beff1b6460 | |||
31b569b02e | |||
8bf77817d2 | |||
27b61adf1d | |||
a0244841ad | |||
12c0c6f2ec | |||
aaa3b8626e | |||
48c9e2dfe3 | |||
81d4801d27 | |||
5f8016061d | |||
5007587192 | |||
dfd1083e41 | |||
678baf46e5 |
2
.github/utils/_get_password.py
vendored
2
.github/utils/_get_password.py
vendored
@ -23,8 +23,6 @@ def get_password(
|
|||||||
auth_plugin="mysql_native_password",
|
auth_plugin="mysql_native_password",
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Generating passwords for version {version_name} ({version_code})")
|
|
||||||
|
|
||||||
password = base64.b64encode(secrets.token_bytes(16)).decode()
|
password = base64.b64encode(secrets.token_bytes(16)).decode()
|
||||||
iv = secrets.token_bytes(16)
|
iv = secrets.token_bytes(16)
|
||||||
|
|
||||||
|
4
.github/utils/_utils.py
vendored
4
.github/utils/_utils.py
vendored
@ -102,7 +102,9 @@ def get_commit_log(project_dir: str, format: str, max_lines: int = None) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
log = subprocess.run(
|
log = subprocess.run(
|
||||||
args=f"git log {last_tag}..HEAD --format=%an%x00%at%x00%h%x00%s%x00%D".split(" "),
|
args=f"git log {last_tag}..HEAD --format=%an%x00%at%x00%h%x00%s%x00%D".split(
|
||||||
|
" "
|
||||||
|
),
|
||||||
cwd=project_dir,
|
cwd=project_dir,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
18
.github/utils/bump_nightly.py
vendored
18
.github/utils/bump_nightly.py
vendored
@ -1,11 +1,8 @@
|
|||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from _utils import (
|
from _utils import (
|
||||||
get_commit_log,
|
get_commit_log,
|
||||||
get_project_dir,
|
get_project_dir,
|
||||||
@ -25,17 +22,6 @@ if __name__ == "__main__":
|
|||||||
print("Missing GitHub environment variables.")
|
print("Missing GitHub environment variables.")
|
||||||
exit(-1)
|
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()
|
project_dir = get_project_dir()
|
||||||
|
|
||||||
(version_code, version_name) = read_gradle_version(project_dir)
|
(version_code, version_name) = read_gradle_version(project_dir)
|
||||||
@ -48,8 +34,8 @@ if __name__ == "__main__":
|
|||||||
date -= timedelta(days=1)
|
date -= timedelta(days=1)
|
||||||
version_name += "+nightly." + date.strftime("%Y%m%d")
|
version_name += "+nightly." + date.strftime("%Y%m%d")
|
||||||
|
|
||||||
print("::set-output name=appVersionName::" + version_name)
|
print("appVersionName=" + version_name)
|
||||||
print("::set-output name=appVersionCode::" + str(version_code))
|
print("appVersionCode=" + str(version_code))
|
||||||
|
|
||||||
write_gradle_version(project_dir, version_code, version_name)
|
write_gradle_version(project_dir, version_code, version_name)
|
||||||
|
|
||||||
|
23
.github/utils/check_nightly.py
vendored
Normal file
23
.github/utils/check_nightly.py
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
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("hasNewChanges=false")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
print("hasNewChanges=true")
|
25
.github/utils/extract_changelogs.py
vendored
25
.github/utils/extract_changelogs.py
vendored
@ -12,24 +12,24 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
(version_code, version_name) = read_gradle_version(project_dir)
|
(version_code, version_name) = read_gradle_version(project_dir)
|
||||||
|
|
||||||
print("::set-output name=appVersionName::" + version_name)
|
print("appVersionName=" + version_name)
|
||||||
print("::set-output name=appVersionCode::" + str(version_code))
|
print("appVersionCode=" + str(version_code))
|
||||||
|
|
||||||
dir = f"{project_dir}/app/release/whatsnew-{version_name}/"
|
dir = f"{project_dir}/app/release/whatsnew-{version_name}/"
|
||||||
os.makedirs(dir, exist_ok=True)
|
os.makedirs(dir, exist_ok=True)
|
||||||
|
|
||||||
print("::set-output name=changelogDir::" + dir)
|
print("changelogDir=" + dir)
|
||||||
|
|
||||||
(title, changelog) = get_changelog(project_dir, format="plain")
|
(title, changelog) = get_changelog(project_dir, format="plain")
|
||||||
|
|
||||||
# plain text changelog - Firebase App Distribution
|
# plain text changelog - Firebase App Distribution
|
||||||
with open(dir + "whatsnew-titled.txt", "w", encoding="utf-8") as f:
|
with open(dir + "whatsnew_titled.txt", "w", encoding="utf-8") as f:
|
||||||
f.write(title)
|
f.write(title)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew-titled.txt")
|
print("changelogPlainTitledFile=" + dir + "whatsnew_titled.txt")
|
||||||
|
|
||||||
print("::set-output name=changelogTitle::" + title)
|
print("changelogTitle=" + title)
|
||||||
|
|
||||||
# plain text changelog, max 500 chars - Google Play
|
# plain text changelog, max 500 chars - Google Play
|
||||||
with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f:
|
with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f:
|
||||||
@ -41,32 +41,31 @@ if __name__ == "__main__":
|
|||||||
changelog = changelog.strip()
|
changelog = changelog.strip()
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
|
|
||||||
print("::set-output name=changelogPlainFile::" + dir + "whatsnew-pl-PL")
|
print("changelogPlainFile=" + dir + "whatsnew-pl-PL")
|
||||||
|
|
||||||
# markdown changelog - Discord webhook
|
# markdown changelog - Discord webhook
|
||||||
(_, changelog) = get_changelog(project_dir, format="markdown")
|
(_, changelog) = get_changelog(project_dir, format="markdown")
|
||||||
with open(dir + "whatsnew.md", "w", encoding="utf-8") as f:
|
with open(dir + "whatsnew.md", "w", encoding="utf-8") as f:
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
print("::set-output name=changelogMarkdownFile::" + dir + "whatsnew.md")
|
print("changelogMarkdownFile=" + dir + "whatsnew.md")
|
||||||
|
|
||||||
# html changelog - version info in DB
|
# html changelog - version info in DB
|
||||||
(_, changelog) = get_changelog(project_dir, format="html")
|
(_, changelog) = get_changelog(project_dir, format="html")
|
||||||
with open(dir + "whatsnew.html", "w", encoding="utf-8") as f:
|
with open(dir + "whatsnew.html", "w", encoding="utf-8") as f:
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
print("::set-output name=changelogHtmlFile::" + dir + "whatsnew.html")
|
print("changelogHtmlFile=" + dir + "whatsnew.html")
|
||||||
|
|
||||||
|
|
||||||
changelog = get_commit_log(project_dir, format="plain", max_lines=10)
|
changelog = get_commit_log(project_dir, format="plain", max_lines=10)
|
||||||
with open(dir + "commit_log.txt", "w", encoding="utf-8") as f:
|
with open(dir + "commit_log.txt", "w", encoding="utf-8") as f:
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
print("::set-output name=commitLogPlainFile::" + dir + "commit_log.txt")
|
print("commitLogPlainFile=" + dir + "commit_log.txt")
|
||||||
|
|
||||||
changelog = get_commit_log(project_dir, format="markdown", max_lines=10)
|
changelog = get_commit_log(project_dir, format="markdown", max_lines=10)
|
||||||
with open(dir + "commit_log.md", "w", encoding="utf-8") as f:
|
with open(dir + "commit_log.md", "w", encoding="utf-8") as f:
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
print("::set-output name=commitLogMarkdownFile::" + dir + "commit_log.md")
|
print("commitLogMarkdownFile=" + dir + "commit_log.md")
|
||||||
|
|
||||||
changelog = get_commit_log(project_dir, format="html", max_lines=10)
|
changelog = get_commit_log(project_dir, format="html", max_lines=10)
|
||||||
with open(dir + "commit_log.html", "w", encoding="utf-8") as f:
|
with open(dir + "commit_log.html", "w", encoding="utf-8") as f:
|
||||||
f.write(changelog)
|
f.write(changelog)
|
||||||
print("::set-output name=commitLogHtmlFile::" + dir + "commit_log.html")
|
print("commitLogHtmlFile=" + dir + "commit_log.html")
|
||||||
|
@ -13,7 +13,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
files = glob.glob(f"{project_dir}/app/release/*.*")
|
files = glob.glob(f"{project_dir}/app/release/*.*")
|
||||||
for file in files:
|
for file in files:
|
||||||
file_relative = file.replace(os.getenv("GITHUB_WORKSPACE") + "/", "")
|
file_relative = file.replace(project_dir + "/", "")
|
||||||
if "-aligned.apk" in file:
|
if "-aligned.apk" in file:
|
||||||
os.unlink(file)
|
os.unlink(file)
|
||||||
elif "-signed.apk" in file:
|
elif "-signed.apk" in file:
|
||||||
@ -22,5 +22,5 @@ if __name__ == "__main__":
|
|||||||
os.unlink(new_file)
|
os.unlink(new_file)
|
||||||
os.rename(file, new_file)
|
os.rename(file, new_file)
|
||||||
elif ".apk" in file or ".aab" in file:
|
elif ".apk" in file or ".aab" in file:
|
||||||
print("::set-output name=signedReleaseFile::" + file)
|
print("signedReleaseFile=" + file)
|
||||||
print("::set-output name=signedReleaseFileRelative::" + file_relative)
|
print("signedReleaseFileRelative=" + file_relative)
|
19
.github/utils/save_version.py
vendored
19
.github/utils/save_version.py
vendored
@ -64,7 +64,14 @@ def save_version(
|
|||||||
if build_type in ["nightly", "daily"]:
|
if build_type in ["nightly", "daily"]:
|
||||||
download_url = apk_server_nightly + apk_name if apk_name else None
|
download_url = apk_server_nightly + apk_name if apk_name else None
|
||||||
else:
|
else:
|
||||||
download_url = apk_server_release + apk_name if apk_name else None
|
# download_url = apk_server_release + apk_name if apk_name else None
|
||||||
|
download_url = (
|
||||||
|
f"https://github.com/szkolny-eu/szkolny-android/releases/download/v{version_name}/{apk_name}"
|
||||||
|
if apk_name
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if download_url:
|
||||||
|
print("downloadUrl=" + download_url)
|
||||||
|
|
||||||
cols = [
|
cols = [
|
||||||
"versionCode",
|
"versionCode",
|
||||||
@ -119,4 +126,12 @@ if __name__ == "__main__":
|
|||||||
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
|
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
|
||||||
APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY")
|
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)
|
save_version(
|
||||||
|
project_dir,
|
||||||
|
DB_HOST,
|
||||||
|
DB_USER,
|
||||||
|
DB_PASS,
|
||||||
|
DB_NAME,
|
||||||
|
APK_SERVER_RELEASE,
|
||||||
|
APK_SERVER_NIGHTLY,
|
||||||
|
)
|
||||||
|
6
.github/utils/sign.py
vendored
6
.github/utils/sign.py
vendored
@ -31,8 +31,6 @@ def sign(
|
|||||||
SIGNING_FORMAT = "$param1.{}.$param2"
|
SIGNING_FORMAT = "$param1.{}.$param2"
|
||||||
CPP_FORMAT = "/*{}*/\nstatic toys AES_IV[16] = {{\n\t{} }};"
|
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_hex = " ".join(["{:02x}".format(x) for x in iv])
|
||||||
iv_cpp = ", ".join(["0x{:02x}".format(x) for x in iv])
|
iv_cpp = ", ".join(["0x{:02x}".format(x) for x in iv])
|
||||||
|
|
||||||
@ -71,8 +69,8 @@ if __name__ == "__main__":
|
|||||||
version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME
|
version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME
|
||||||
)
|
)
|
||||||
|
|
||||||
print("::set-output name=appVersionName::" + version_name)
|
print("appVersionName=" + version_name)
|
||||||
print("::set-output name=appVersionCode::" + str(version_code))
|
print("appVersionCode=" + str(version_code))
|
||||||
|
|
||||||
sign(
|
sign(
|
||||||
project_dir,
|
project_dir,
|
||||||
|
33
.github/utils/webhook_discord.py
vendored
33
.github/utils/webhook_discord.py
vendored
@ -11,8 +11,7 @@ from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_v
|
|||||||
def post_webhook(
|
def post_webhook(
|
||||||
project_dir: str,
|
project_dir: str,
|
||||||
apk_file: str,
|
apk_file: str,
|
||||||
apk_server_release: str,
|
download_url: str,
|
||||||
apk_server_nightly: str,
|
|
||||||
webhook_release: str,
|
webhook_release: str,
|
||||||
webhook_testing: str,
|
webhook_testing: str,
|
||||||
):
|
):
|
||||||
@ -25,12 +24,6 @@ def post_webhook(
|
|||||||
testing = ["dev", "beta", "nightly", "daily"]
|
testing = ["dev", "beta", "nightly", "daily"]
|
||||||
testing = build_type in testing
|
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:
|
if testing:
|
||||||
build_date = int(os.stat(apk_file).st_mtime)
|
build_date = int(os.stat(apk_file).st_mtime)
|
||||||
if build_date:
|
if build_date:
|
||||||
@ -48,13 +41,17 @@ def post_webhook(
|
|||||||
requests.post(url=webhook_testing, json=webhook)
|
requests.post(url=webhook_testing, json=webhook)
|
||||||
else:
|
else:
|
||||||
changelog = get_changelog(project_dir, format="markdown")
|
changelog = get_changelog(project_dir, format="markdown")
|
||||||
webhook = get_webhook_release(changelog, download_url)
|
webhook = get_webhook_release(version_name, changelog, download_url)
|
||||||
requests.post(url=webhook_release, json=webhook)
|
requests.post(url=webhook_release, json=webhook)
|
||||||
|
|
||||||
|
|
||||||
def get_webhook_release(changelog: str, download_url: str):
|
def get_webhook_release(version_name: str, changelog: str, download_url: str):
|
||||||
(title, content) = changelog
|
(title, content) = changelog
|
||||||
return {"content": f"__**{title}**__\n{content}\n{download_url}"}
|
return {
|
||||||
|
"content": (
|
||||||
|
f"__**{title}**__\n{content}\n[Szkolny.eu {version_name}]({download_url})"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_webhook_testing(
|
def get_webhook_testing(
|
||||||
@ -73,9 +70,11 @@ def get_webhook_testing(
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"name": f"Wersja `{version_name}`",
|
"name": f"Wersja `{version_name}`",
|
||||||
"value": f"[Pobierz .APK]({download_url})"
|
"value": (
|
||||||
if download_url
|
f"[Pobierz .APK]({download_url})"
|
||||||
else "*Pobieranie niedostępne*",
|
if download_url
|
||||||
|
else "*Pobieranie niedostępne*"
|
||||||
|
),
|
||||||
"inline": False,
|
"inline": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -103,16 +102,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
APK_FILE = os.getenv("APK_FILE")
|
APK_FILE = os.getenv("APK_FILE")
|
||||||
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
|
DOWNLOAD_URL = os.getenv("DOWNLOAD_URL")
|
||||||
APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY")
|
|
||||||
WEBHOOK_RELEASE = os.getenv("WEBHOOK_RELEASE")
|
WEBHOOK_RELEASE = os.getenv("WEBHOOK_RELEASE")
|
||||||
WEBHOOK_TESTING = os.getenv("WEBHOOK_TESTING")
|
WEBHOOK_TESTING = os.getenv("WEBHOOK_TESTING")
|
||||||
|
|
||||||
post_webhook(
|
post_webhook(
|
||||||
project_dir,
|
project_dir,
|
||||||
APK_FILE,
|
APK_FILE,
|
||||||
APK_SERVER_RELEASE,
|
DOWNLOAD_URL,
|
||||||
APK_SERVER_NIGHTLY,
|
|
||||||
WEBHOOK_RELEASE,
|
WEBHOOK_RELEASE,
|
||||||
WEBHOOK_TESTING,
|
WEBHOOK_TESTING,
|
||||||
)
|
)
|
||||||
|
195
.github/workflows/_build.yml
vendored
Normal file
195
.github/workflows/_build.yml
vendored
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
name: "[reusable] Szkolny.eu Build"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
nightly:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
build-apk:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
build-aab:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
release-ssh:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
release-github:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
release-firebase:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
release-google-play:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
release-discord:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
secrets:
|
||||||
|
APK_SERVER_NIGHTLY:
|
||||||
|
APK_SERVER_RELEASE:
|
||||||
|
DB_HOST:
|
||||||
|
DB_NAME:
|
||||||
|
DB_PASS:
|
||||||
|
DB_USER:
|
||||||
|
FIREBASE_APP_ID:
|
||||||
|
FIREBASE_GROUPS_NIGHTLY:
|
||||||
|
FIREBASE_GROUPS_RELEASE:
|
||||||
|
FIREBASE_SERVICE_ACCOUNT_JSON:
|
||||||
|
KEY_ALIAS_PASSWORD:
|
||||||
|
KEY_ALIAS:
|
||||||
|
KEY_STORE_PASSWORD:
|
||||||
|
KEY_STORE:
|
||||||
|
PLAY_RELEASE_TRACK:
|
||||||
|
PLAY_SERVICE_ACCOUNT_JSON:
|
||||||
|
SSH_IP:
|
||||||
|
SSH_KEY:
|
||||||
|
SSH_PATH_NIGHTLY:
|
||||||
|
SSH_PATH_RELEASE:
|
||||||
|
SSH_USERNAME:
|
||||||
|
WEBHOOK_RELEASE:
|
||||||
|
WEBHOOK_TESTING:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
clean: false
|
||||||
|
- name: Setup JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "temurin"
|
||||||
|
java-version: "17"
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
- name: Install Python packages
|
||||||
|
uses: BSFishy/pip-action@v1
|
||||||
|
with:
|
||||||
|
packages: |
|
||||||
|
python-dotenv
|
||||||
|
pycryptodome
|
||||||
|
mysql-connector-python
|
||||||
|
requests
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
|
- name: Bump nightly version
|
||||||
|
if: ${{ inputs.nightly }}
|
||||||
|
run: python $GITHUB_WORKSPACE/.github/utils/bump_nightly.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
|
||||||
|
- name: Write signing passwords and keystore
|
||||||
|
env:
|
||||||
|
DB_HOST: ${{ secrets.DB_HOST }}
|
||||||
|
DB_USER: ${{ secrets.DB_USER }}
|
||||||
|
DB_PASS: ${{ secrets.DB_PASS }}
|
||||||
|
DB_NAME: ${{ secrets.DB_NAME }}
|
||||||
|
KEY_STORE: ${{ secrets.KEY_STORE }}
|
||||||
|
run: |
|
||||||
|
python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit >> $GITHUB_OUTPUT
|
||||||
|
echo $KEY_STORE | base64 --decode > keystore.jks
|
||||||
|
- name: Clean build artifacts
|
||||||
|
run: |
|
||||||
|
rm -rf app/release/*
|
||||||
|
rm -rf app/build/outputs/apk/*
|
||||||
|
rm -rf app/build/outputs/bundle/*
|
||||||
|
|
||||||
|
- name: Build app with Gradle
|
||||||
|
if: ${{ inputs.build-apk || inputs.build-aab }}
|
||||||
|
run: |
|
||||||
|
chmod +x ./gradlew
|
||||||
|
./gradlew \
|
||||||
|
${{ inputs.build-apk && 'assembleOfficialRelease' || '' }} \
|
||||||
|
${{ inputs.build-aab && 'bundlePlayRelease' || '' }} \
|
||||||
|
-P android.injected.signing.store.file=${{ github.workspace }}/keystore.jks \
|
||||||
|
-P android.injected.signing.store.password=${{ secrets.KEY_STORE_PASSWORD }} \
|
||||||
|
-P android.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \
|
||||||
|
-P android.injected.signing.key.password=${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload release to server
|
||||||
|
if: ${{ inputs.release-ssh }}
|
||||||
|
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: app/release/
|
||||||
|
TARGET: ${{ inputs.nightly && secrets.SSH_PATH_NIGHTLY || secrets.SSH_PATH_RELEASE }}
|
||||||
|
|
||||||
|
- name: Find signed artifacts
|
||||||
|
id: artifacts
|
||||||
|
run: python $GITHUB_WORKSPACE/.github/utils/find_artifacts.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
|
||||||
|
- name: Extract release changelogs
|
||||||
|
id: changelog
|
||||||
|
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
|
||||||
|
- name: Save version to database
|
||||||
|
id: save
|
||||||
|
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 >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Release on GitHub
|
||||||
|
if: ${{ inputs.release-github }}
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
name: ${{ steps.changelog.outputs.changelogTitle }}
|
||||||
|
body_path: ${{ steps.changelog.outputs.changelogMarkdownFile }}
|
||||||
|
files: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Distribute to App Distribution
|
||||||
|
if: ${{ inputs.release-firebase }}
|
||||||
|
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||||
|
with:
|
||||||
|
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||||
|
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
|
||||||
|
file: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||||
|
groups: ${{ inputs.nightly && secrets.FIREBASE_GROUPS_NIGHTLY || secrets.FIREBASE_GROUPS_RELEASE }}
|
||||||
|
releaseNotesFile: ${{ inputs.nightly && steps.changelog.outputs.commitLogPlainFile || steps.changelog.outputs.changelogPlainTitledFile }}
|
||||||
|
|
||||||
|
- name: Publish AAB to Google Play
|
||||||
|
if: ${{ inputs.release-google-play }}
|
||||||
|
uses: r0adkll/upload-google-play@v1
|
||||||
|
with:
|
||||||
|
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
packageName: pl.szczodrzynski.edziennik
|
||||||
|
releaseFiles: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||||
|
releaseName: ${{ steps.changelog.outputs.appVersionName }}
|
||||||
|
track: ${{ secrets.PLAY_RELEASE_TRACK }}
|
||||||
|
whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }}
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
- name: Post Discord webhook
|
||||||
|
if: ${{ inputs.release-discord }}
|
||||||
|
env:
|
||||||
|
APK_FILE: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||||
|
DOWNLOAD_URL: ${{ steps.save.outputs.downloadUrl }}
|
||||||
|
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
|
||||||
|
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
|
||||||
|
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload workflow artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: ${{ steps.changelog.outputs.appVersionName }}
|
||||||
|
path: |
|
||||||
|
app/release/whatsnew*/
|
||||||
|
app/release/*.apk
|
||||||
|
app/release/*.aab
|
||||||
|
app/release/*.json
|
||||||
|
app/release/*.txt
|
154
.github/workflows/build-nightly-apk.yml
vendored
154
.github/workflows/build-nightly-apk.yml
vendored
@ -1,154 +0,0 @@
|
|||||||
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: 'temurin'
|
|
||||||
java-version: '11'
|
|
||||||
- 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
|
|
||||||
uses: gradle/gradle-build-action@v2
|
|
||||||
with:
|
|
||||||
arguments: 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
|
|
131
.github/workflows/build-release-aab-play.yml
vendored
131
.github/workflows/build-release-aab-play.yml
vendored
@ -1,131 +0,0 @@
|
|||||||
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: 'temurin'
|
|
||||||
java-version: '11'
|
|
||||||
- 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
|
|
||||||
uses: gradle/gradle-build-action@v2
|
|
||||||
with:
|
|
||||||
arguments: 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
|
|
154
.github/workflows/build-release-apk.yml
vendored
154
.github/workflows/build-release-apk.yml
vendored
@ -1,154 +0,0 @@
|
|||||||
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: 'temurin'
|
|
||||||
java-version: '11'
|
|
||||||
- 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
|
|
||||||
uses: gradle/gradle-build-action@v2
|
|
||||||
with:
|
|
||||||
arguments: 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
|
|
13
.github/workflows/push-master.yml
vendored
Normal file
13
.github/workflows/push-master.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
name: Push (master)
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["master"]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build for Google Play (AAB)
|
||||||
|
uses: szkolny-eu/szkolny-android/.github/workflows/_build.yml@develop
|
||||||
|
with:
|
||||||
|
build-aab: true
|
||||||
|
release-ssh: true
|
||||||
|
release-google-play: true
|
||||||
|
secrets: inherit
|
15
.github/workflows/release.yml
vendored
Normal file
15
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*.*"]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build release (APK)
|
||||||
|
uses: szkolny-eu/szkolny-android/.github/workflows/_build.yml@develop
|
||||||
|
with:
|
||||||
|
build-apk: true
|
||||||
|
release-ssh: true
|
||||||
|
release-github: true
|
||||||
|
release-firebase: true
|
||||||
|
release-discord: true
|
||||||
|
secrets: inherit
|
42
.github/workflows/schedule-dispatch.yml
vendored
Normal file
42
.github/workflows/schedule-dispatch.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: Schedule/dispatch
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# 23:30 UTC, 0:30 or 1:30 CET/CEST
|
||||||
|
- cron: "30 23 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check new changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
hasNewChanges: ${{ steps.nightly.outputs.hasNewChanges }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
clean: false
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
- name: Install packages
|
||||||
|
uses: BSFishy/pip-action@v1
|
||||||
|
with:
|
||||||
|
packages: |
|
||||||
|
requests
|
||||||
|
- name: Check new changes
|
||||||
|
id: nightly
|
||||||
|
run: python $GITHUB_WORKSPACE/.github/utils/check_nightly.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build nightly release (APK)
|
||||||
|
needs:
|
||||||
|
- check
|
||||||
|
if: ${{ needs.check.outputs.hasNewChanges == 'true' }}
|
||||||
|
uses: szkolny-eu/szkolny-android/.github/workflows/_build.yml@develop
|
||||||
|
with:
|
||||||
|
nightly: true
|
||||||
|
build-apk: true
|
||||||
|
release-ssh: true
|
||||||
|
release-firebase: true
|
||||||
|
release-discord: true
|
||||||
|
secrets: inherit
|
@ -104,7 +104,6 @@ android {
|
|||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path "src/main/cpp/CMakeLists.txt"
|
path "src/main/cpp/CMakeLists.txt"
|
||||||
version "3.10.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
@ -113,8 +112,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.whenTaskAdded { task ->
|
tasks.whenTaskAdded { task ->
|
||||||
if (!task.name.endsWith("Release") && !task.name.endsWith("ReleaseWithR8"))
|
if (!(task.name == "assembleUnofficialRelease" || task.name == "assembleOfficialRelease" || task.name == "signPlayReleaseBundle"))
|
||||||
return
|
return
|
||||||
|
|
||||||
def renameTaskName = "rename${task.name.capitalize()}"
|
def renameTaskName = "rename${task.name.capitalize()}"
|
||||||
|
|
||||||
def flavor = ""
|
def flavor = ""
|
||||||
@ -124,17 +124,22 @@ tasks.whenTaskAdded { task ->
|
|||||||
flavor = task.name.substring("assemble".length(), task.name.indexOf("Release")).uncapitalize()
|
flavor = task.name.substring("assemble".length(), task.name.indexOf("Release")).uncapitalize()
|
||||||
if (task.name.startsWith("minify"))
|
if (task.name.startsWith("minify"))
|
||||||
flavor = task.name.substring("minify".length(), task.name.indexOf("Release")).uncapitalize()
|
flavor = task.name.substring("minify".length(), task.name.indexOf("Release")).uncapitalize()
|
||||||
|
if (task.name.startsWith("sign"))
|
||||||
|
flavor = task.name.substring("sign".length(), task.name.indexOf("Release")).uncapitalize()
|
||||||
|
|
||||||
if (flavor != "") {
|
if (flavor != "") {
|
||||||
tasks.create(renameTaskName, Copy) {
|
tasks.register(renameTaskName, Copy) {
|
||||||
|
dependsOn(task.name)
|
||||||
|
duplicatesStrategy DuplicatesStrategy.FAIL
|
||||||
from file("${projectDir}/${flavor}/release/"),
|
from file("${projectDir}/${flavor}/release/"),
|
||||||
file("${buildDir}/outputs/mapping/${flavor}Release/"),
|
file("${projectDir}/build/outputs/apk/${flavor}/release/"),
|
||||||
file("${buildDir}/outputs/apk/${flavor}/release/"),
|
file("${projectDir}/build/outputs/mapping/${flavor}Release/"),
|
||||||
file("${buildDir}/outputs/bundle/${flavor}Release/")
|
file("${projectDir}/build/outputs/bundle/${flavor}Release/")
|
||||||
include "*.aab", "*.apk", "mapping.txt", "output-metadata.json"
|
include "*-release.aab", "*-release.apk", "mapping.txt", "output-metadata.json"
|
||||||
destinationDir file("${projectDir}/release/")
|
destinationDir file("${projectDir}/release/")
|
||||||
rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1'
|
rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1'
|
||||||
}
|
}
|
||||||
|
|
||||||
task.finalizedBy(renameTaskName)
|
task.finalizedBy(renameTaskName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,6 +161,7 @@ dependencies {
|
|||||||
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
|
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
implementation "androidx.room:room-runtime:2.4.3"
|
implementation "androidx.room:room-runtime:2.4.3"
|
||||||
|
implementation "androidx.room:room-ktx:2.4.3"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||||
kapt "androidx.room:room-compiler:2.4.3"
|
kapt "androidx.room:room-compiler:2.4.3"
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
<!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 -->
|
<!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 -->
|
||||||
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" />
|
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" />
|
||||||
|
|
||||||
@ -84,7 +85,7 @@
|
|||||||
android:resource="@xml/widget_timetable_info" />
|
android:resource="@xml/widget_timetable_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<service android:name=".ui.widgets.timetable.WidgetTimetableService"
|
<service android:name=".ui.widgets.timetable.WidgetTimetableService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" android:foregroundServiceType="dataSync" />
|
||||||
<activity android:name=".ui.widgets.LessonDialogActivity"
|
<activity android:name=".ui.widgets.LessonDialogActivity"
|
||||||
android:label=""
|
android:label=""
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
@ -105,7 +106,7 @@
|
|||||||
android:resource="@xml/widget_notifications_info" />
|
android:resource="@xml/widget_notifications_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
<service android:name=".ui.widgets.notifications.WidgetNotificationsService"
|
<service android:name=".ui.widgets.notifications.WidgetNotificationsService"
|
||||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
android:permission="android.permission.BIND_REMOTEVIEWS" android:foregroundServiceType="dataSync" />
|
||||||
<!-- LUCKY NUMBER -->
|
<!-- LUCKY NUMBER -->
|
||||||
<receiver android:name=".ui.widgets.luckynumber.WidgetLuckyNumberProvider"
|
<receiver android:name=".ui.widgets.luckynumber.WidgetLuckyNumberProvider"
|
||||||
android:label="@string/widget_lucky_number_title"
|
android:label="@string/widget_lucky_number_title"
|
||||||
@ -160,7 +161,11 @@
|
|||||||
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
|
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/AppTheme.Light" />
|
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
|
||||||
|
<activity android:name=".ui.login.recaptcha.RecaptchaActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
|
||||||
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
|
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
|
||||||
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
|
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
|
||||||
|
|
||||||
@ -198,15 +203,15 @@
|
|||||||
____) | __/ | \ V /| | (_| __/\__ \
|
____) | __/ | \ V /| | (_| __/\__ \
|
||||||
|_____/ \___|_| \_/ |_|\___\___||___/
|
|_____/ \___|_| \_/ |_|\___\___||___/
|
||||||
-->
|
-->
|
||||||
<service android:name=".data.api.ApiService" />
|
<service android:name=".data.api.ApiService" android:foregroundServiceType="dataSync" />
|
||||||
<service android:name=".data.firebase.MyFirebaseService"
|
<service android:name=".data.firebase.MyFirebaseService"
|
||||||
android:exported="false">
|
android:exported="false" android:foregroundServiceType="dataSync">
|
||||||
<intent-filter android:priority="10000000">
|
<intent-filter android:priority="10000000">
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
|
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name=".sync.UpdateDownloaderService" />
|
<service android:name=".sync.UpdateDownloaderService" android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
_____ _ _
|
_____ _ _
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
<h3>Wersja 4.13.2-rc.4, 2022-11-25</h3>
|
<h3>Wersja 4.14.2, 2025-02-25</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Poprawiono synchronizację w Mobidzienniku bez adresu e-mail.</li>
|
<li>USOS: <b>dodano obsługę ocen</b>.</li>
|
||||||
<li>Poprawiono błąd synchronizacji w Vulcanie.</li>
|
<li>USOS: obliczanie średniej za studia oraz punktów ECTS.</li>
|
||||||
|
<li>USOS: poprawiono brak planu zajęć po rozpoczęciu roku.</li>
|
||||||
|
<li>Wyłączono archiwizator profili.</li>
|
||||||
|
<li>Naprawiono zatrzymanie aplikacji na Androidzie 14.</li>
|
||||||
|
<li>Udostępniono opcję wyłączania śniegu również w lutym.</li>
|
||||||
|
<li>Poprawiono błędy synchronizacji. @KrystianQur</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
Dzięki za korzystanie ze Szkolnego!<br>
|
Dzięki za korzystanie ze Szkolnego!<br>
|
||||||
<i>© [Kuba Szczodrzyński](@kuba2k2) 2022</i>
|
<i>© [Kuba Szczodrzyński](@kuba2k2) 2025</i>
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.4.1)
|
cmake_minimum_required(VERSION 3.4.1)
|
||||||
|
|
||||||
|
project(szkolny-signing)
|
||||||
|
|
||||||
# Creates and names a library, sets it as either STATIC
|
# Creates and names a library, sets it as either STATIC
|
||||||
# or SHARED, and provides the relative paths to its source code.
|
# or SHARED, and provides the relative paths to its source code.
|
||||||
# You can define multiple libraries, and CMake builds them for you.
|
# You can define multiple libraries, and CMake builds them for you.
|
||||||
@ -41,4 +43,4 @@ target_link_libraries( # Specifies the target library.
|
|||||||
|
|
||||||
# Links the target library to the log library
|
# Links the target library to the log library
|
||||||
# included in the NDK.
|
# included in the NDK.
|
||||||
${log-lib} )
|
${log-lib} )
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
/*secret password - removed for source code publication*/
|
/*secret password - removed for source code publication*/
|
||||||
static toys AES_IV[16] = {
|
static toys AES_IV[16] = {
|
||||||
0xc5, 0x67, 0x62, 0x67, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
|
0x3e, 0x4b, 0xe4, 0xc4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
|
||||||
|
|
||||||
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);
|
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);
|
||||||
|
|
||||||
|
@ -235,11 +235,20 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Signing.getCert(this)
|
Signing.getCert(this)
|
||||||
|
Utils.initializeStorageDir(this)
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
config.migrate(this@App)
|
config.migrate(this@App)
|
||||||
|
|
||||||
|
if (config.appVersionCore < BuildConfig.VERSION_CODE) {
|
||||||
|
// force syncing all endpoints on update
|
||||||
|
db.endpointTimerDao().clear()
|
||||||
|
config.sync.lastAppSync = 0L
|
||||||
|
config.hash = "invalid"
|
||||||
|
config.appVersionCore = BuildConfig.VERSION_CODE
|
||||||
|
}
|
||||||
|
|
||||||
SSLProviderInstaller.install(applicationContext, this@App::buildHttp)
|
SSLProviderInstaller.install(applicationContext, this@App::buildHttp)
|
||||||
|
|
||||||
if (config.devModePassword != null)
|
if (config.devModePassword != null)
|
||||||
@ -422,6 +431,12 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
|||||||
try {
|
try {
|
||||||
App.data = AppData.get(profile.loginStoreType)
|
App.data = AppData.get(profile.loginStoreType)
|
||||||
d("App", "Loaded AppData: ${App.data}")
|
d("App", "Loaded AppData: ${App.data}")
|
||||||
|
// apply newly-added config overrides, if not changed by the user yet
|
||||||
|
for ((key, value) in App.data.configOverrides) {
|
||||||
|
val config = App.profile.config
|
||||||
|
if (!config.has(key))
|
||||||
|
config.set(key, value)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("App", "Cannot load AppData", e)
|
Log.e("App", "Cannot load AppData", e)
|
||||||
Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show()
|
Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show()
|
||||||
|
@ -15,6 +15,8 @@ import android.view.Gravity
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
@ -322,7 +324,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
|
|
||||||
// IT'S WINTER MY DUDES
|
// IT'S WINTER MY DUDES
|
||||||
val today = Date.getToday()
|
val today = Date.getToday()
|
||||||
if ((today.month % 11 == 1) && app.config.ui.snowfall) {
|
if ((today.month / 3 % 4 == 0) && app.config.ui.snowfall) {
|
||||||
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
|
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
|
||||||
} else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
|
} else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
|
||||||
val eggfall = layoutInflater.inflate(
|
val eggfall = layoutInflater.inflate(
|
||||||
@ -829,7 +831,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
d(TAG, "Activity resumed")
|
d(TAG, "Activity resumed")
|
||||||
val filter = IntentFilter()
|
val filter = IntentFilter()
|
||||||
filter.addAction(Intent.ACTION_MAIN)
|
filter.addAction(Intent.ACTION_MAIN)
|
||||||
registerReceiver(intentReceiver, filter)
|
ActivityCompat.registerReceiver(
|
||||||
|
this,
|
||||||
|
intentReceiver,
|
||||||
|
filter,
|
||||||
|
ContextCompat.RECEIVER_EXPORTED,
|
||||||
|
)
|
||||||
EventBus.getDefault().register(this)
|
EventBus.getDefault().register(this)
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
@ -59,10 +59,11 @@ data class AppData(
|
|||||||
val lessonHeight: Int,
|
val lessonHeight: Int,
|
||||||
val enableMarkAsReadAnnouncements: Boolean,
|
val enableMarkAsReadAnnouncements: Boolean,
|
||||||
val enableNoticePoints: Boolean,
|
val enableNoticePoints: Boolean,
|
||||||
|
val eventManualShowSubjectDropdown: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class EventType(
|
data class EventType(
|
||||||
val id: Int,
|
val id: Long,
|
||||||
val color: String,
|
val color: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
@ -43,4 +43,6 @@ abstract class BaseConfig(
|
|||||||
db.configDao().add(ConfigEntry(profileId ?: -1, key, value))
|
db.configDao().add(ConfigEntry(profileId ?: -1, key, value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun has(key: String) = values.containsKey(key)
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ class Config(db: AppDb) : BaseConfig(db) {
|
|||||||
var appInstalledTime by config<Long>(0L)
|
var appInstalledTime by config<Long>(0L)
|
||||||
var appRateSnackbarTime by config<Long>(0L)
|
var appRateSnackbarTime by config<Long>(0L)
|
||||||
var appVersion by config<Int>(BuildConfig.VERSION_CODE)
|
var appVersion by config<Int>(BuildConfig.VERSION_CODE)
|
||||||
|
var appVersionCore by config<Int>(0)
|
||||||
var validation by config<String?>(null, "buildValidation")
|
var validation by config<String?>(null, "buildValidation")
|
||||||
|
|
||||||
var archiverEnabled by config<Boolean>(true)
|
var archiverEnabled by config<Boolean>(true)
|
||||||
|
@ -15,7 +15,7 @@ class ProfileConfig(
|
|||||||
entries: List<ConfigEntry>?,
|
entries: List<ConfigEntry>?,
|
||||||
) : BaseConfig(db, profileId, entries) {
|
) : BaseConfig(db, profileId, entries) {
|
||||||
companion object {
|
companion object {
|
||||||
const val DATA_VERSION = 4
|
const val DATA_VERSION = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
val grades by lazy { ProfileConfigGrades(this) }
|
val grades by lazy { ProfileConfigGrades(this) }
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.config
|
package pl.szczodrzynski.edziennik.config
|
||||||
|
|
||||||
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
|
||||||
|
|
||||||
@ -15,8 +16,11 @@ class ProfileConfigGrades(base: ProfileConfig) {
|
|||||||
var dontCountEnabled by base.config<Boolean>(false)
|
var dontCountEnabled by base.config<Boolean>(false)
|
||||||
var dontCountGrades by base.config<List<String>> { listOf() }
|
var dontCountGrades by base.config<List<String>> { listOf() }
|
||||||
var hideImproved by base.config<Boolean>(false)
|
var hideImproved by base.config<Boolean>(false)
|
||||||
|
var hideNoGrade by base.config<Boolean>(false)
|
||||||
var hideSticksFromOld by base.config<Boolean>(false)
|
var hideSticksFromOld by base.config<Boolean>(false)
|
||||||
var minusValue by base.config<Float?>(null)
|
var minusValue by base.config<Float?>(null)
|
||||||
var plusValue by base.config<Float?>(null)
|
var plusValue by base.config<Float?>(null)
|
||||||
var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES)
|
var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES)
|
||||||
|
var universityAverageMode by base.config<Int>(UNIVERSITY_AVERAGE_MODE_ECTS)
|
||||||
|
var countEctsInProgress by base.config<Boolean>(false)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class ProfileConfigUI(base: ProfileConfig) {
|
|||||||
var agendaGroupByType by base.config<Boolean>(false)
|
var agendaGroupByType by base.config<Boolean>(false)
|
||||||
var agendaLessonChanges by base.config<Boolean>(true)
|
var agendaLessonChanges by base.config<Boolean>(true)
|
||||||
var agendaTeacherAbsence by base.config<Boolean>(true)
|
var agendaTeacherAbsence by base.config<Boolean>(true)
|
||||||
|
var agendaSubjectImportant by base.config<Boolean>(false)
|
||||||
var agendaElearningMark by base.config<Boolean>(false)
|
var agendaElearningMark by base.config<Boolean>(false)
|
||||||
var agendaElearningGroup by base.config<Boolean>(true)
|
var agendaElearningGroup by base.config<Boolean>(true)
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_AL
|
|||||||
class ProfileConfigMigration(config: ProfileConfig) {
|
class ProfileConfigMigration(config: ProfileConfig) {
|
||||||
init { config.apply {
|
init { config.apply {
|
||||||
|
|
||||||
|
val profile = db.profileDao().getByIdNow(profileId ?: -1)
|
||||||
|
|
||||||
if (dataVersion < 2) {
|
if (dataVersion < 2) {
|
||||||
sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE
|
sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE
|
||||||
|
|
||||||
@ -37,11 +39,23 @@ class ProfileConfigMigration(config: ProfileConfig) {
|
|||||||
// switch to new event types (USOS)
|
// switch to new event types (USOS)
|
||||||
dataVersion = 4
|
dataVersion = 4
|
||||||
|
|
||||||
val profile = db.profileDao().getByIdNow(profileId ?: -1)
|
|
||||||
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
|
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
|
||||||
db.eventTypeDao().clear(profileId ?: -1)
|
db.eventTypeDao().clear(profileId ?: -1)
|
||||||
db.eventTypeDao().addDefaultTypes(profile)
|
db.eventTypeDao().addDefaultTypes(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataVersion < 5) {
|
||||||
|
// update USOS event types and the appropriate events (2022-12-25)
|
||||||
|
dataVersion = 5
|
||||||
|
|
||||||
|
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
|
||||||
|
db.eventTypeDao().getAllWithDefaults(profile)
|
||||||
|
// wejściówka (4) -> kartkówka (3)
|
||||||
|
db.eventDao().getRawNow("UPDATE events SET eventType = 3 WHERE profileId = $profileId AND eventType = 4;")
|
||||||
|
// zadanie (6) -> zadanie domowe (-1)
|
||||||
|
db.eventDao().getRawNow("UPDATE events SET eventType = -1 WHERE profileId = $profileId AND eventType = 6;")
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,10 @@ 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 SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
|
||||||
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
|
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
|
||||||
const val LIBRUS_REDIRECT_URL = "app://librus"
|
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_AUTHORIZE_URL = "https://portal.librus.pl/konto-librus/redirect/dru"
|
||||||
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"
|
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action"
|
||||||
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
|
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
|
||||||
|
const val LIBRUS_HEADER = "pl.librus.synergiaDru2"
|
||||||
|
|
||||||
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
|
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
|
||||||
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
|
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
|
||||||
@ -59,9 +60,6 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
|
|||||||
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
|
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
|
||||||
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
|
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
|
||||||
|
|
||||||
const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
|
|
||||||
const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
|
|
||||||
|
|
||||||
|
|
||||||
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
|
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT
|
||||||
|
|
||||||
|
@ -148,6 +148,7 @@ const val ERROR_LIBRUS_MESSAGES_NOT_FOUND = 186
|
|||||||
const val ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST = 187
|
const val ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST = 187
|
||||||
const val ERROR_LIBRUS_MESSAGES_ATTACHMENT_NOT_FOUND = 188
|
const val ERROR_LIBRUS_MESSAGES_ATTACHMENT_NOT_FOUND = 188
|
||||||
const val ERROR_LOGIN_LIBRUS_MESSAGES_TIMEOUT = 189
|
const val ERROR_LOGIN_LIBRUS_MESSAGES_TIMEOUT = 189
|
||||||
|
const val ERROR_LIBRUS_API_TEACHER_FREE_DAYS_NOT_PUBLIC = 190
|
||||||
|
|
||||||
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201
|
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201
|
||||||
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202
|
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202
|
||||||
|
@ -24,6 +24,25 @@ object Regexes {
|
|||||||
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
|
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val HTML_INPUT_HIDDEN by lazy {
|
||||||
|
"""<input .*?type="hidden".+?>""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_INPUT_NAME by lazy {
|
||||||
|
"""name="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_INPUT_VALUE by lazy {
|
||||||
|
"""value="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_CSRF_TOKEN by lazy {
|
||||||
|
"""name="csrf-token" content="([A-z0-9=+/\-_]+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_FORM_ACTION by lazy {
|
||||||
|
"""<form .*?action="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
val HTML_RECAPTCHA_KEY by lazy {
|
||||||
|
"""data-sitekey="(.+?)"""".toRegex()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
|
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
|
||||||
|
@ -9,6 +9,7 @@ import org.greenrobot.eventbus.EventBus
|
|||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_ARCHIVED
|
import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_ARCHIVED
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.demo.Demo
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus
|
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
|
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
|
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
|
||||||
@ -113,6 +114,11 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (profile?.empty == true) {
|
||||||
|
// force app sync on first login
|
||||||
|
app.config.sync.lastAppSync = 0L
|
||||||
|
}
|
||||||
|
|
||||||
edziennikInterface = when (loginStore.type) {
|
edziennikInterface = when (loginStore.type) {
|
||||||
LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback)
|
LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback)
|
||||||
LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)
|
LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)
|
||||||
@ -120,6 +126,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
|
|||||||
LoginType.PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
|
LoginType.PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
|
||||||
LoginType.TEMPLATE -> Template(app, profile, loginStore, taskCallback)
|
LoginType.TEMPLATE -> Template(app, profile, loginStore, taskCallback)
|
||||||
LoginType.USOS -> Usos(app, profile, loginStore, taskCallback)
|
LoginType.USOS -> Usos(app, profile, loginStore, taskCallback)
|
||||||
|
LoginType.DEMO -> Demo(app, profile, loginStore, taskCallback)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (edziennikInterface == null) {
|
if (edziennikInterface == null) {
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2024-7-8.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.data.api.edziennik.demo
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import pl.szczodrzynski.edziennik.App
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
|
|
||||||
|
class Demo(
|
||||||
|
val app: App,
|
||||||
|
val profile: Profile?,
|
||||||
|
val loginStore: LoginStore,
|
||||||
|
val callback: EdziennikCallback,
|
||||||
|
) : EdziennikInterface {
|
||||||
|
|
||||||
|
private fun completed() {
|
||||||
|
callback.onCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sync(
|
||||||
|
featureTypes: Set<FeatureType>?,
|
||||||
|
onlyEndpoints: Set<Int>?,
|
||||||
|
arguments: JsonObject?,
|
||||||
|
) = completed()
|
||||||
|
|
||||||
|
override fun getMessage(message: MessageFull) =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun sendMessage(recipients: Set<Teacher>, subject: String, text: String) =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun markAllAnnouncementsAsRead() =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun getAnnouncement(announcement: AnnouncementFull) =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun getRecipientList() =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun getEvent(eventFull: EventFull) =
|
||||||
|
completed()
|
||||||
|
|
||||||
|
override fun firstLogin() {
|
||||||
|
val profile = Profile(
|
||||||
|
id = loginStore.id,
|
||||||
|
loginStoreId = loginStore.id,
|
||||||
|
loginStoreType = LoginType.DEMO,
|
||||||
|
name = "Jan Szkolny",
|
||||||
|
subname = "Szkolny.eu",
|
||||||
|
studentNameLong = "Jan Szkolny",
|
||||||
|
studentNameShort = "Jan S.",
|
||||||
|
accountName = null,
|
||||||
|
)
|
||||||
|
profile.apply {
|
||||||
|
empty = false
|
||||||
|
syncEnabled = false
|
||||||
|
registration = Profile.REGISTRATION_DISABLED
|
||||||
|
studentClassName = "1A"
|
||||||
|
userCode = "nologin:1234"
|
||||||
|
dateYearEnd.month = 8
|
||||||
|
}
|
||||||
|
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(profile), loginStore))
|
||||||
|
completed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {}
|
||||||
|
}
|
@ -29,6 +29,9 @@ import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
|
|||||||
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
|
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
|
import pl.szczodrzynski.edziennik.ext.DAY
|
||||||
|
import pl.szczodrzynski.edziennik.ext.HOUR
|
||||||
|
import pl.szczodrzynski.edziennik.ext.WEEK
|
||||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||||
|
|
||||||
class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
|
class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
|
||||||
@ -235,6 +238,11 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
|
|||||||
data.app.config.sync.tokenLibrusList + data.profileId
|
data.app.config.sync.tokenLibrusList + data.profileId
|
||||||
data()
|
data()
|
||||||
}
|
}
|
||||||
|
ERROR_LIBRUS_API_TEACHER_FREE_DAYS_NOT_PUBLIC -> {
|
||||||
|
d(TAG, "Student not have access to Teacher Free Days resource")
|
||||||
|
data.setSyncNext(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS, 1 * WEEK, FeatureType.AGENDA)
|
||||||
|
data()
|
||||||
|
}
|
||||||
else -> callback.onError(apiError)
|
else -> callback.onError(apiError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,7 @@ open class LibrusApi(open val data: DataLibrus, open val lastSync: Long?) {
|
|||||||
"NoticeboardProblem" -> ERROR_LIBRUS_API_NOTICEBOARD_PROBLEM
|
"NoticeboardProblem" -> ERROR_LIBRUS_API_NOTICEBOARD_PROBLEM
|
||||||
"DeviceRegistered" -> ERROR_LIBRUS_API_DEVICE_REGISTERED
|
"DeviceRegistered" -> ERROR_LIBRUS_API_DEVICE_REGISTERED
|
||||||
"Maintenance" -> ERROR_LIBRUS_API_MAINTENANCE
|
"Maintenance" -> ERROR_LIBRUS_API_MAINTENANCE
|
||||||
|
"TeacherFreeDaysIsNotActive" -> ERROR_LIBRUS_API_TEACHER_FREE_DAYS_NOT_PUBLIC
|
||||||
else -> ERROR_LIBRUS_API_OTHER
|
else -> ERROR_LIBRUS_API_OTHER
|
||||||
}.let { errorCode ->
|
}.let { errorCode ->
|
||||||
if (errorCode !in ignoreErrors) {
|
if (errorCode !in ignoreErrors) {
|
||||||
|
@ -29,7 +29,7 @@ class LibrusApiTeacherFreeDays(override val data: DataLibrus,
|
|||||||
data.db.teacherAbsenceTypeDao().getAllNow(profileId).toSparseArray(data.teacherAbsenceTypes) { it.id }
|
data.db.teacherAbsenceTypeDao().getAllNow(profileId).toSparseArray(data.teacherAbsenceTypes) { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
apiGet(TAG, "TeacherFreeDays") { json ->
|
apiGet(TAG, "Calendars/TeacherFreeDays") { json ->
|
||||||
val teacherAbsences = json.getJsonArray("TeacherFreeDays")?.asJsonObjectList()
|
val teacherAbsences = json.getJsonArray("TeacherFreeDays")?.asJsonObjectList()
|
||||||
|
|
||||||
teacherAbsences?.forEach { teacherAbsence ->
|
teacherAbsences?.forEach { teacherAbsence ->
|
||||||
|
@ -24,6 +24,9 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
private const val TAG = "LoginLibrusPortal"
|
private const val TAG = "LoginLibrusPortal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loop failsafe
|
||||||
|
private var loginPerformed = false
|
||||||
|
|
||||||
init { run {
|
init { run {
|
||||||
if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) {
|
if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) {
|
||||||
data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE))
|
data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE))
|
||||||
@ -33,6 +36,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
|
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
|
||||||
return@run
|
return@run
|
||||||
}
|
}
|
||||||
|
loginPerformed = false
|
||||||
|
|
||||||
// succeed having a non-expired access token and a refresh token
|
// succeed having a non-expired access token and a refresh token
|
||||||
if (data.isPortalLoginValid()) {
|
if (data.isPortalLoginValid()) {
|
||||||
@ -58,18 +62,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private fun authorize(url: String?) {
|
private fun authorize(url: String, referer: String? = null) {
|
||||||
d(TAG, "Request: Librus/Login/Portal - $url")
|
d(TAG, "Request: Librus/Login/Portal - $url")
|
||||||
|
|
||||||
Request.builder()
|
Request.builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.userAgent(LIBRUS_USER_AGENT)
|
.userAgent(LIBRUS_USER_AGENT)
|
||||||
|
.also {
|
||||||
|
if (referer != null)
|
||||||
|
it.addHeader("Referer", referer)
|
||||||
|
}
|
||||||
|
.addHeader("X-Requested-With", LIBRUS_HEADER)
|
||||||
.withClient(data.app.httpLazy)
|
.withClient(data.app.httpLazy)
|
||||||
.callback(object : TextCallbackHandler() {
|
.callback(object : TextCallbackHandler() {
|
||||||
override fun onSuccess(text: String, response: Response) {
|
override fun onSuccess(text: String, response: Response) {
|
||||||
val location = response.headers().get("Location")
|
val location = response.headers().get("Location")
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
|
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
|
||||||
when {
|
when {
|
||||||
authMatcher.find() -> {
|
authMatcher.find() -> {
|
||||||
accessToken(authMatcher.group(1), null)
|
accessToken(authMatcher.group(1), null)
|
||||||
@ -83,16 +92,31 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
authorize(location)
|
authorize(location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return
|
||||||
val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text)
|
}
|
||||||
if (csrfMatcher.find()) {
|
|
||||||
login(csrfMatcher.group(1) ?: "")
|
if (checkError(text, response))
|
||||||
} else {
|
return
|
||||||
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING)
|
|
||||||
.withResponse(response)
|
var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL
|
||||||
.withApiResponse(text))
|
val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: ""
|
||||||
|
|
||||||
|
for (match in Regexes.HTML_FORM_ACTION.findAll(text)) {
|
||||||
|
val form = match.value.lowercase()
|
||||||
|
if ("login" in form && "post" in form) {
|
||||||
|
loginUrl = match[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val params = mutableMapOf<String, String>()
|
||||||
|
for (match in Regexes.HTML_INPUT_HIDDEN.findAll(text)) {
|
||||||
|
val input = match.value
|
||||||
|
val name = Regexes.HTML_INPUT_NAME.find(input)?.get(1) ?: continue
|
||||||
|
val value = Regexes.HTML_INPUT_VALUE.find(input)?.get(1) ?: continue
|
||||||
|
params[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
login(url = loginUrl, referer = url, csrfToken, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(response: Response, throwable: Throwable) {
|
override fun onFailure(response: Response, throwable: Throwable) {
|
||||||
@ -105,8 +129,54 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(csrfToken: String) {
|
private fun checkError(text: String, response: Response): Boolean {
|
||||||
d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}")
|
when {
|
||||||
|
text.contains("librus_account_settings_main") -> return false
|
||||||
|
text.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
|
||||||
|
text.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
||||||
|
text.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
||||||
|
else -> null // no error for now
|
||||||
|
}?.let { errorCode ->
|
||||||
|
data.error(ApiError(TAG, errorCode)
|
||||||
|
.withApiResponse(text)
|
||||||
|
.withResponse(response))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("robotem" in text || "g-recaptcha" in text || "captchaValidate" in text) {
|
||||||
|
val siteKey = Regexes.HTML_RECAPTCHA_KEY.find(text)?.get(1)
|
||||||
|
if (siteKey == null) {
|
||||||
|
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)
|
||||||
|
.withApiResponse(text)
|
||||||
|
.withResponse(response))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
data.requireUserAction(
|
||||||
|
type = UserActionRequiredEvent.Type.RECAPTCHA,
|
||||||
|
params = Bundle(
|
||||||
|
"siteKey" to siteKey,
|
||||||
|
"referer" to response.request().url().toString(),
|
||||||
|
"userAgent" to LIBRUS_USER_AGENT,
|
||||||
|
),
|
||||||
|
errorText = R.string.notification_user_action_required_captcha_librus,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun login(
|
||||||
|
url: String,
|
||||||
|
referer: String,
|
||||||
|
csrfToken: String?,
|
||||||
|
params: Map<String, String>,
|
||||||
|
) {
|
||||||
|
if (loginPerformed) {
|
||||||
|
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d(TAG, "Request: Librus/Login/Portal - $url")
|
||||||
|
|
||||||
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
|
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
|
||||||
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
|
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
|
||||||
@ -116,67 +186,46 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
|
|||||||
Request.builder()
|
Request.builder()
|
||||||
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
|
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
|
||||||
.userAgent(LIBRUS_USER_AGENT)
|
.userAgent(LIBRUS_USER_AGENT)
|
||||||
|
.addHeader("X-Requested-With", LIBRUS_HEADER)
|
||||||
|
.addHeader("Referer", referer)
|
||||||
|
.withClient(data.app.httpLazy)
|
||||||
.addParameter("email", data.portalEmail)
|
.addParameter("email", data.portalEmail)
|
||||||
.addParameter("password", data.portalPassword)
|
.addParameter("password", data.portalPassword)
|
||||||
.also {
|
.also {
|
||||||
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
|
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
|
||||||
it.addParameter("g-recaptcha-response", recaptchaCode)
|
it.addParameter("g-recaptcha-response", recaptchaCode)
|
||||||
|
if (csrfToken != null)
|
||||||
|
it.addHeader("X-CSRF-TOKEN", csrfToken)
|
||||||
|
for ((key, value) in params) {
|
||||||
|
it.addParameter(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.addHeader("X-CSRF-TOKEN", csrfToken)
|
.contentType(MediaTypeUtils.APPLICATION_FORM)
|
||||||
.allowErrorCode(HTTP_BAD_REQUEST)
|
|
||||||
.allowErrorCode(HTTP_FORBIDDEN)
|
|
||||||
.contentType(MediaTypeUtils.APPLICATION_JSON)
|
|
||||||
.post()
|
.post()
|
||||||
.callback(object : JsonCallbackHandler() {
|
.callback(object : TextCallbackHandler() {
|
||||||
override fun onSuccess(json: JsonObject?, response: Response) {
|
override fun onSuccess(text: String?, response: Response) {
|
||||||
|
loginPerformed = true
|
||||||
val location = response.headers()?.get("Location")
|
val location = response.headers()?.get("Location")
|
||||||
if (location == "$LIBRUS_REDIRECT_URL?command=close") {
|
if (location == "$LIBRUS_REDIRECT_URL?command=close") {
|
||||||
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
|
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
|
||||||
.withApiResponse(json)
|
.withApiResponse(text)
|
||||||
.withResponse(response))
|
.withResponse(response))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (text == null) {
|
||||||
if (json == null) {
|
|
||||||
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
|
|
||||||
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
|
|
||||||
.withResponse(response))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
|
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
|
||||||
.withResponse(response))
|
.withResponse(response))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val error = if (response.code() == 200) null else
|
|
||||||
json.getJsonArray("errors")?.getString(0)
|
|
||||||
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
|
|
||||||
|
|
||||||
if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) {
|
authorize(
|
||||||
data.requireUserAction(
|
url = location
|
||||||
type = UserActionRequiredEvent.Type.RECAPTCHA,
|
?: if (data.fakeLogin)
|
||||||
params = Bundle(
|
FAKE_LIBRUS_AUTHORIZE
|
||||||
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
|
else
|
||||||
"referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER,
|
LIBRUS_AUTHORIZE_URL,
|
||||||
),
|
referer = referer,
|
||||||
errorText = R.string.notification_user_action_required_captcha_librus,
|
)
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
error?.let { code ->
|
|
||||||
when {
|
|
||||||
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
|
|
||||||
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
|
||||||
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
|
|
||||||
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
|
|
||||||
}.let { errorCode ->
|
|
||||||
data.error(ApiError(TAG, errorCode)
|
|
||||||
.withApiResponse(json)
|
|
||||||
.withResponse(response))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(response: Response, throwable: Throwable) {
|
override fun onFailure(response: Response, throwable: Throwable) {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.data.api.edziennik.usos
|
package pl.szczodrzynski.edziennik.data.api.edziennik.usos
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.Data
|
import pl.szczodrzynski.edziennik.data.api.models.Data
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||||
@ -73,4 +74,9 @@ class DataUsos(
|
|||||||
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
|
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
|
||||||
set(value) { profile["studentId"] = value; mStudentId = value }
|
set(value) { profile["studentId"] = value; mStudentId = value }
|
||||||
private var mStudentId: Int? = null
|
private var mStudentId: Int? = null
|
||||||
|
|
||||||
|
var termNames: Map<String, String> = mapOf()
|
||||||
|
get() { mTermNames = mTermNames ?: profile?.getStudentData("termNames", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mTermNames ?: mapOf() }
|
||||||
|
set(value) { profile["termNames"] = app.gson.toJson(value); mTermNames = value }
|
||||||
|
private var mTermNames: Map<String, String>? = null
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,12 @@ import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
|
|||||||
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
|
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
|
||||||
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
|
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
|
||||||
|
|
||||||
const val ENDPOINT_USOS_API_USER = 7000
|
const val ENDPOINT_USOS_API_USER = 7000
|
||||||
const val ENDPOINT_USOS_API_TERMS = 7010
|
const val ENDPOINT_USOS_API_TERMS = 7010
|
||||||
const val ENDPOINT_USOS_API_COURSES = 7020
|
const val ENDPOINT_USOS_API_COURSES = 7020
|
||||||
const val ENDPOINT_USOS_API_TIMETABLE = 7030
|
const val ENDPOINT_USOS_API_TIMETABLE = 7030
|
||||||
|
const val ENDPOINT_USOS_API_ECTS_POINTS = 7040
|
||||||
|
const val ENDPOINT_USOS_API_EXAM_REPORTS = 7050
|
||||||
|
|
||||||
val UsosFeatures = listOf(
|
val UsosFeatures = listOf(
|
||||||
/*
|
/*
|
||||||
@ -39,4 +41,12 @@ val UsosFeatures = listOf(
|
|||||||
Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf(
|
Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf(
|
||||||
ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API,
|
ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API,
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Grades
|
||||||
|
*/
|
||||||
|
Feature(LoginType.USOS, FeatureType.GRADES, listOf(
|
||||||
|
ENDPOINT_USOS_API_ECTS_POINTS to LoginMethod.USOS_API,
|
||||||
|
ENDPOINT_USOS_API_EXAM_REPORTS to LoginMethod.USOS_API,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,8 @@ import pl.szczodrzynski.edziennik.R
|
|||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
|
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.*
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.*
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiEctsPoints
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiExamReports
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser
|
||||||
@ -58,6 +60,14 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
|
|||||||
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
|
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
|
||||||
UsosApiTimetable(data, lastSync, onSuccess)
|
UsosApiTimetable(data, lastSync, onSuccess)
|
||||||
}
|
}
|
||||||
|
ENDPOINT_USOS_API_ECTS_POINTS -> {
|
||||||
|
data.startProgress(R.string.edziennik_progress_endpoint_grade_categories)
|
||||||
|
UsosApiEctsPoints(data, lastSync, onSuccess)
|
||||||
|
}
|
||||||
|
ENDPOINT_USOS_API_EXAM_REPORTS -> {
|
||||||
|
data.startProgress(R.string.edziennik_progress_endpoint_grades)
|
||||||
|
UsosApiExamReports(data, lastSync, onSuccess)
|
||||||
|
}
|
||||||
else -> onSuccess(endpointId)
|
else -> onSuccess(endpointId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
|
|||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Team
|
import pl.szczodrzynski.edziennik.data.db.entity.Team
|
||||||
import pl.szczodrzynski.edziennik.ext.*
|
import pl.szczodrzynski.edziennik.ext.*
|
||||||
|
|
||||||
@ -25,17 +26,20 @@ class UsosApiCourses(
|
|||||||
apiRequest<JsonObject>(
|
apiRequest<JsonObject>(
|
||||||
tag = TAG,
|
tag = TAG,
|
||||||
service = "courses/user",
|
service = "courses/user",
|
||||||
|
params = mapOf(
|
||||||
|
"active_terms_only" to false,
|
||||||
|
),
|
||||||
fields = listOf(
|
fields = listOf(
|
||||||
// "terms" to listOf("id", "name", "start_date", "end_date"),
|
// "terms" to listOf("id", "name", "start_date", "end_date"),
|
||||||
"course_editions" to listOf(
|
"course_editions" to listOf(
|
||||||
"course_id",
|
"course_id",
|
||||||
"course_name",
|
"course_name",
|
||||||
// "term_id",
|
|
||||||
"user_groups" to listOf(
|
"user_groups" to listOf(
|
||||||
"course_unit_id",
|
"course_unit_id",
|
||||||
"group_number",
|
"group_number",
|
||||||
// "class_type",
|
"class_type",
|
||||||
"class_type_id",
|
"class_type_id",
|
||||||
|
"term_id",
|
||||||
"lecturers",
|
"lecturers",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -63,22 +67,38 @@ class UsosApiCourses(
|
|||||||
for (courseEdition in courseEditions) {
|
for (courseEdition in courseEditions) {
|
||||||
val courseId = courseEdition.getString("course_id") ?: continue
|
val courseId = courseEdition.getString("course_id") ?: continue
|
||||||
val courseName = courseEdition.getLangString("course_name") ?: continue
|
val courseName = courseEdition.getLangString("course_name") ?: continue
|
||||||
val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
|
val userGroups =
|
||||||
|
courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
|
||||||
for (userGroup in userGroups) {
|
for (userGroup in userGroups) {
|
||||||
val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
|
val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
|
||||||
val groupNumber = userGroup.getInt("group_number") ?: continue
|
val groupNumber = userGroup.getInt("group_number") ?: continue
|
||||||
// val classType = userGroup.getLangString("class_type") ?: continue
|
val classType = userGroup.getLangString("class_type") ?: continue
|
||||||
val classTypeId = userGroup.getString("class_type_id") ?: continue
|
val classTypeId = userGroup.getString("class_type_id") ?: continue
|
||||||
|
val termId = userGroup.getString("term_id") ?: continue
|
||||||
val lecturers = userGroup.getLecturerIds("lecturers")
|
val lecturers = userGroup.getLecturerIds("lecturers")
|
||||||
|
|
||||||
data.teamList.put(courseUnitId, Team(
|
data.teamList.put(
|
||||||
profileId,
|
courseUnitId, Team(
|
||||||
courseUnitId,
|
profileId,
|
||||||
"${profile?.studentClassName} $classTypeId$groupNumber - $courseName",
|
courseUnitId,
|
||||||
2,
|
"${profile?.studentClassName} $courseName ($classTypeId$groupNumber)",
|
||||||
"${data.schoolId}:${courseId} $classTypeId$groupNumber",
|
2,
|
||||||
lecturers.firstOrNull() ?: -1L,
|
"${data.schoolId}:${termId}:${courseId} $classTypeId$groupNumber",
|
||||||
))
|
lecturers.firstOrNull() ?: -1L,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val gradeCategory = data.gradeCategories[courseUnitId]
|
||||||
|
data.gradeCategories.put(
|
||||||
|
courseUnitId, GradeCategory(
|
||||||
|
profileId,
|
||||||
|
courseUnitId,
|
||||||
|
gradeCategory?.weight ?: -1.0f,
|
||||||
|
0,
|
||||||
|
courseId,
|
||||||
|
).addColumn(classType)
|
||||||
|
)
|
||||||
|
|
||||||
hasValidTeam = true
|
hasValidTeam = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2025-1-31.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_ECTS_POINTS
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
|
||||||
|
import pl.szczodrzynski.edziennik.ext.DAY
|
||||||
|
import pl.szczodrzynski.edziennik.ext.filter
|
||||||
|
|
||||||
|
class UsosApiEctsPoints(
|
||||||
|
override val data: DataUsos,
|
||||||
|
override val lastSync: Long?,
|
||||||
|
val onSuccess: (endpointId: Int) -> Unit,
|
||||||
|
) : UsosApi(data, lastSync) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "UsosApiEctsPoints"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
apiRequest<JsonObject>(
|
||||||
|
tag = TAG,
|
||||||
|
service = "courses/user_ects_points",
|
||||||
|
responseType = ResponseType.OBJECT,
|
||||||
|
) { json, response ->
|
||||||
|
if (!processResponse(json)) {
|
||||||
|
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
|
||||||
|
return@apiRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
data.setSyncNext(ENDPOINT_USOS_API_ECTS_POINTS, 2 * DAY)
|
||||||
|
onSuccess(ENDPOINT_USOS_API_ECTS_POINTS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processResponse(json: JsonObject): Boolean {
|
||||||
|
for ((_, coursePointsEl) in json.entrySet()) {
|
||||||
|
if (!coursePointsEl.isJsonObject)
|
||||||
|
continue
|
||||||
|
for ((courseId, pointsEl) in coursePointsEl.asJsonObject.entrySet()) {
|
||||||
|
if (!pointsEl.isJsonPrimitive)
|
||||||
|
continue
|
||||||
|
val gradeCategories = data.gradeCategories
|
||||||
|
.filter { it.text == courseId }
|
||||||
|
gradeCategories.forEach {
|
||||||
|
it.weight = pointsEl.asString.toFloatOrNull() ?: -1.0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2025-1-31.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_EXAM_REPORTS
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
|
||||||
|
import pl.szczodrzynski.edziennik.ext.getBoolean
|
||||||
|
import pl.szczodrzynski.edziennik.ext.getInt
|
||||||
|
import pl.szczodrzynski.edziennik.ext.getJsonArray
|
||||||
|
import pl.szczodrzynski.edziennik.ext.getJsonObject
|
||||||
|
import pl.szczodrzynski.edziennik.ext.getLong
|
||||||
|
import pl.szczodrzynski.edziennik.ext.getString
|
||||||
|
import pl.szczodrzynski.edziennik.ext.join
|
||||||
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
|
|
||||||
|
class UsosApiExamReports(
|
||||||
|
override val data: DataUsos,
|
||||||
|
override val lastSync: Long?,
|
||||||
|
val onSuccess: (endpointId: Int) -> Unit,
|
||||||
|
) : UsosApi(data, lastSync) {
|
||||||
|
companion object {
|
||||||
|
const val TAG = "UsosApiExamReports"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val missingTermNames = mutableSetOf<String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
apiRequest<JsonObject>(
|
||||||
|
tag = TAG,
|
||||||
|
service = "examrep/user2",
|
||||||
|
fields = listOf(
|
||||||
|
"id",
|
||||||
|
"type_description",
|
||||||
|
"course_unit" to listOf("id", "course_name"),
|
||||||
|
"sessions" to listOf(
|
||||||
|
"number",
|
||||||
|
"description",
|
||||||
|
"issuer_grades" to listOf(
|
||||||
|
"value_symbol",
|
||||||
|
// "value_description",
|
||||||
|
"passes",
|
||||||
|
"counts_into_average",
|
||||||
|
"date_modified",
|
||||||
|
"modification_author",
|
||||||
|
"comment",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
responseType = ResponseType.OBJECT,
|
||||||
|
) { json, response ->
|
||||||
|
if (!processResponse(json)) {
|
||||||
|
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
|
||||||
|
return@apiRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
data.toRemove.add(DataRemoveModel.Grades.all())
|
||||||
|
data.setSyncNext(ENDPOINT_USOS_API_EXAM_REPORTS, SYNC_ALWAYS)
|
||||||
|
|
||||||
|
if (missingTermNames.isEmpty())
|
||||||
|
onSuccess(ENDPOINT_USOS_API_EXAM_REPORTS)
|
||||||
|
else
|
||||||
|
UsosApiTerms(data, lastSync, onSuccess, missingTermNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processResponse(json: JsonObject): Boolean {
|
||||||
|
for ((termId, courseEditionEl) in json.entrySet()) {
|
||||||
|
if (!courseEditionEl.isJsonObject)
|
||||||
|
continue
|
||||||
|
for ((courseId, examReportsEl) in courseEditionEl.asJsonObject.entrySet()) {
|
||||||
|
if (!examReportsEl.isJsonArray)
|
||||||
|
continue
|
||||||
|
for (examReportEl in examReportsEl.asJsonArray) {
|
||||||
|
if (!examReportEl.isJsonObject)
|
||||||
|
continue
|
||||||
|
val examReport = examReportEl.asJsonObject
|
||||||
|
processExamReport(termId, courseId, examReport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processExamReport(termId: String, courseId: String, examReport: JsonObject) {
|
||||||
|
val examId = examReport.getString("id")?.toIntOrNull()
|
||||||
|
?: return
|
||||||
|
val typeDescription = examReport.getLangString("type_description")
|
||||||
|
val courseUnit = examReport.getJsonObject("course_unit")
|
||||||
|
?: return
|
||||||
|
val courseUnitId = courseUnit.getString("id")?.toLongOrNull()
|
||||||
|
?: return
|
||||||
|
val courseName = courseUnit.getLangString("course_name")
|
||||||
|
?: return
|
||||||
|
val sessions = examReport.getJsonArray("sessions")
|
||||||
|
?: return
|
||||||
|
|
||||||
|
val gradeCategory = data.gradeCategories[courseUnitId]
|
||||||
|
val classType = gradeCategory?.columns?.get(0)
|
||||||
|
|
||||||
|
val subject = data.getSubject(
|
||||||
|
id = null,
|
||||||
|
name = courseName,
|
||||||
|
shortName = courseId,
|
||||||
|
)
|
||||||
|
|
||||||
|
var hasGrade = false
|
||||||
|
|
||||||
|
for (sessionEl in sessions) {
|
||||||
|
if (!sessionEl.isJsonObject)
|
||||||
|
continue
|
||||||
|
val session = sessionEl.asJsonObject
|
||||||
|
|
||||||
|
val sessionNumber = session.getInt("number") ?: continue
|
||||||
|
val sessionDescription = session.getLangString("description")
|
||||||
|
val issuerGrade = session.getJsonObject("issuer_grades")
|
||||||
|
|
||||||
|
val valueSymbol = issuerGrade.getString("value_symbol") ?: continue
|
||||||
|
val passes = issuerGrade.getBoolean("passes")
|
||||||
|
val countsIntoAverage = issuerGrade.getString("counts_into_average") ?: "T"
|
||||||
|
val dateModified = issuerGrade.getString("date_modified")
|
||||||
|
val modificationAuthorId = issuerGrade.getJsonObject("modification_author")
|
||||||
|
?.getLong("id") ?: -1L
|
||||||
|
val comment = issuerGrade.getString("comment")
|
||||||
|
|
||||||
|
val value = valueSymbol.toFloatOrNull() ?: 0.0f
|
||||||
|
|
||||||
|
if (termId !in data.termNames) {
|
||||||
|
missingTermNames.add(termId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val gradeObject = Grade(
|
||||||
|
profileId = profileId,
|
||||||
|
id = examId * 10L + sessionNumber,
|
||||||
|
name = valueSymbol,
|
||||||
|
type = TYPE_NORMAL,
|
||||||
|
value = value,
|
||||||
|
weight = if (countsIntoAverage == "T") gradeCategory?.weight ?: 0.0f else 0.0f,
|
||||||
|
color = (if (passes == true) 0xFF465FB3 else 0xFFB71C1C).toInt(),
|
||||||
|
category = typeDescription,
|
||||||
|
description = listOfNotNull(classType, sessionDescription, comment).join(" - "),
|
||||||
|
comment = termId,
|
||||||
|
semester = 1,
|
||||||
|
teacherId = modificationAuthorId,
|
||||||
|
subjectId = subject.id,
|
||||||
|
addedDate = Date.fromIso(dateModified),
|
||||||
|
)
|
||||||
|
hasGrade = true
|
||||||
|
|
||||||
|
if (sessionNumber > 1) {
|
||||||
|
val origId = examId * 10L + sessionNumber - 1
|
||||||
|
val grades = data.gradeList.filter { it.id == origId }
|
||||||
|
val improvedGrade = grades.firstOrNull()
|
||||||
|
improvedGrade?.parentId = gradeObject.id
|
||||||
|
improvedGrade?.weight = 0.0f
|
||||||
|
gradeObject.isImprovement = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data.gradeList.add(gradeObject)
|
||||||
|
data.metadataList.add(
|
||||||
|
Metadata(
|
||||||
|
profileId,
|
||||||
|
MetadataType.GRADE,
|
||||||
|
gradeObject.id,
|
||||||
|
profile?.empty ?: false,
|
||||||
|
profile?.empty ?: false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasGrade) {
|
||||||
|
// add an "empty" grade for the exam
|
||||||
|
val gradeObject = Grade(
|
||||||
|
profileId = profileId,
|
||||||
|
id = examId * 10L,
|
||||||
|
name = "...",
|
||||||
|
type = TYPE_NO_GRADE,
|
||||||
|
value = 0.0f,
|
||||||
|
weight = 0.0f,
|
||||||
|
color = 0xFFBABABD.toInt(),
|
||||||
|
category = typeDescription,
|
||||||
|
description = classType,
|
||||||
|
comment = termId,
|
||||||
|
semester = 1,
|
||||||
|
teacherId = -1L,
|
||||||
|
subjectId = subject.id,
|
||||||
|
addedDate = 0,
|
||||||
|
)
|
||||||
|
data.gradeList.add(gradeObject)
|
||||||
|
data.metadataList.add(
|
||||||
|
Metadata(
|
||||||
|
profileId,
|
||||||
|
MetadataType.GRADE,
|
||||||
|
gradeObject.id,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
|
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
|
||||||
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
|
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
|
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
|
||||||
@ -16,43 +17,81 @@ class UsosApiTerms(
|
|||||||
override val data: DataUsos,
|
override val data: DataUsos,
|
||||||
override val lastSync: Long?,
|
override val lastSync: Long?,
|
||||||
val onSuccess: (endpointId: Int) -> Unit,
|
val onSuccess: (endpointId: Int) -> Unit,
|
||||||
|
names: Set<String>? = null,
|
||||||
) : UsosApi(data, lastSync) {
|
) : UsosApi(data, lastSync) {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "UsosApiTerms"
|
const val TAG = "UsosApiTerms"
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
apiRequest<JsonArray>(
|
if (names != null) {
|
||||||
tag = TAG,
|
apiRequest<JsonObject>(
|
||||||
service = "terms/search",
|
tag = TAG,
|
||||||
params = mapOf(
|
service = "terms/terms",
|
||||||
"query" to Date.getToday().year.toString(),
|
params = mapOf("term_ids" to names.joinToString("|")),
|
||||||
),
|
responseType = ResponseType.OBJECT,
|
||||||
responseType = ResponseType.ARRAY,
|
) { json, response ->
|
||||||
) { json, response ->
|
if (!processResponse(json.entrySet().map { it.value.asJsonObject })) {
|
||||||
if (!processResponse(json)) {
|
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
|
||||||
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
|
return@apiRequest
|
||||||
return@apiRequest
|
}
|
||||||
}
|
|
||||||
|
|
||||||
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY)
|
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY)
|
||||||
onSuccess(ENDPOINT_USOS_API_TERMS)
|
onSuccess(ENDPOINT_USOS_API_TERMS)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiRequest<JsonArray>(
|
||||||
|
tag = TAG,
|
||||||
|
service = "terms/search",
|
||||||
|
params = mapOf("query" to Date.getToday().year.toString()),
|
||||||
|
responseType = ResponseType.ARRAY,
|
||||||
|
) { json, response ->
|
||||||
|
if (!processResponse(json.asJsonObjectList())) {
|
||||||
|
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
|
||||||
|
return@apiRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY)
|
||||||
|
onSuccess(ENDPOINT_USOS_API_TERMS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processResponse(json: JsonArray): Boolean {
|
private fun processResponse(terms: List<JsonObject>): Boolean {
|
||||||
|
val profile = profile ?: return false
|
||||||
|
val termNames = data.termNames.toMutableMap()
|
||||||
val today = Date.getToday()
|
val today = Date.getToday()
|
||||||
for (term in json.asJsonObjectList()) {
|
for (term in terms) {
|
||||||
|
val id = term.getString("id")
|
||||||
|
val name = term.getLangString("name")
|
||||||
|
val orderKey = term.getInt("order_key")
|
||||||
|
if (id != null && name != null)
|
||||||
|
termNames[id] = "$orderKey$$name"
|
||||||
|
|
||||||
if (!term.getBoolean("is_active", false))
|
if (!term.getBoolean("is_active", false))
|
||||||
continue
|
continue
|
||||||
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue
|
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue
|
||||||
val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue
|
val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue
|
||||||
if (today in startDate..finishDate) {
|
if (today !in startDate..finishDate)
|
||||||
profile?.studentSchoolYearStart = startDate.year
|
continue
|
||||||
profile?.dateSemester1Start = startDate
|
|
||||||
profile?.dateSemester2Start = finishDate
|
if (startDate.month >= 8)
|
||||||
}
|
profile.dateSemester1Start = startDate
|
||||||
|
else
|
||||||
|
profile.dateSemester2Start = startDate
|
||||||
|
|
||||||
|
if (finishDate.month >= 8)
|
||||||
|
profile.dateYearEnd = finishDate
|
||||||
|
else
|
||||||
|
profile.dateSemester2Start = finishDate
|
||||||
}
|
}
|
||||||
|
// update school year start
|
||||||
|
profile.studentSchoolYearStart = profile.dateSemester1Start.year
|
||||||
|
// update year end date if there is a new year
|
||||||
|
if (profile.dateYearEnd <= profile.dateSemester1Start)
|
||||||
|
profile.dateYearEnd =
|
||||||
|
profile.dateSemester1Start.clone().setYear(profile.dateSemester1Start.year + 1)
|
||||||
|
data.termNames = termNames
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,8 @@ class UsosApiUser(
|
|||||||
"last_name",
|
"last_name",
|
||||||
"student_number",
|
"student_number",
|
||||||
"student_programmes" to listOf(
|
"student_programmes" to listOf(
|
||||||
"programme" to listOf("id"),
|
"id",
|
||||||
|
"programme" to listOf("id", "description"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -40,9 +41,11 @@ class UsosApiUser(
|
|||||||
) { json, response ->
|
) { json, response ->
|
||||||
val programmes = json.getJsonArray("student_programmes")
|
val programmes = json.getJsonArray("student_programmes")
|
||||||
if (programmes.isNullOrEmpty()) {
|
if (programmes.isNullOrEmpty()) {
|
||||||
data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
|
data.error(
|
||||||
.withApiResponse(json)
|
ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
|
||||||
.withResponse(response))
|
.withApiResponse(json)
|
||||||
|
.withResponse(response)
|
||||||
|
)
|
||||||
return@apiRequest
|
return@apiRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +53,19 @@ class UsosApiUser(
|
|||||||
val lastName = json.getString("last_name")
|
val lastName = json.getString("last_name")
|
||||||
val studentName = buildFullName(firstName, lastName)
|
val studentName = buildFullName(firstName, lastName)
|
||||||
|
|
||||||
|
val studentProgrammeId = programmes.getJsonObject(0)
|
||||||
|
.getString("id")
|
||||||
|
val programmeId = programmes.getJsonObject(0)
|
||||||
|
.getJsonObject("programme")
|
||||||
|
.getString("id")
|
||||||
|
|
||||||
data.studentId = json.getInt("id") ?: data.studentId
|
data.studentId = json.getInt("id") ?: data.studentId
|
||||||
profile?.studentNameLong = studentName
|
profile?.studentNameLong = studentName
|
||||||
profile?.studentNameShort = studentName.getShortName()
|
profile?.studentNameShort = studentName.getShortName()
|
||||||
profile?.studentNumber = json.getInt("student_number", -1)
|
profile?.studentNumber = json.getInt("student_number", -1)
|
||||||
profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
|
profile?.studentClassName = programmeId
|
||||||
|
|
||||||
profile?.studentClassName?.let {
|
val team = programmeId?.let {
|
||||||
data.getTeam(
|
data.getTeam(
|
||||||
id = null,
|
id = null,
|
||||||
name = it,
|
name = it,
|
||||||
@ -64,6 +73,7 @@ class UsosApiUser(
|
|||||||
isTeamClass = true,
|
isTeamClass = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
team?.code = "${data.schoolId}:${studentProgrammeId}:${programmeId}"
|
||||||
|
|
||||||
data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY)
|
data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY)
|
||||||
onSuccess(ENDPOINT_USOS_API_USER)
|
onSuccess(ENDPOINT_USOS_API_USER)
|
||||||
|
@ -206,7 +206,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
|
|||||||
teams.filter { it.profileId == profile.id }.map { it.code }
|
teams.filter { it.profileId == profile.id }.map { it.code }
|
||||||
)
|
)
|
||||||
val hash = user.toString().md5()
|
val hash = user.toString().md5()
|
||||||
if (hash == profile.config.hash)
|
if (hash == profile.config.hash && app.config.hash != "invalid")
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
return@mapNotNull user to profile.config
|
return@mapNotNull user to profile.config
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,12 @@ class SignatureInterceptor(val app: App) : Interceptor {
|
|||||||
return chain.proceed(
|
return chain.proceed(
|
||||||
request.newBuilder()
|
request.newBuilder()
|
||||||
.header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY)
|
.header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY)
|
||||||
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
|
|
||||||
.header("X-Timestamp", timestamp.toString())
|
|
||||||
.header("X-Signature", sign(timestamp, body, url))
|
|
||||||
.header("X-AppBuild", BuildConfig.BUILD_TYPE)
|
.header("X-AppBuild", BuildConfig.BUILD_TYPE)
|
||||||
.header("X-AppFlavor", BuildConfig.FLAVOR)
|
.header("X-AppFlavor", BuildConfig.FLAVOR)
|
||||||
|
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
|
||||||
|
.header("X-DeviceId", app.deviceId)
|
||||||
|
.header("X-Signature", sign(timestamp, body, url))
|
||||||
|
.header("X-Timestamp", timestamp.toString())
|
||||||
.build())
|
.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,6 @@ object Signing {
|
|||||||
|
|
||||||
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
|
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
|
||||||
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
|
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
|
||||||
return "$param1.MTIzNDU2Nzg5MDLWJ/cfbN===.$param2".sha256()
|
return "$param1.MTIzNDU2Nzg5MDn0HBlu/h===.$param2".sha256()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,7 @@ interface EndpointTimerDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM endpointTimers WHERE profileId = :profileId")
|
@Query("DELETE FROM endpointTimers WHERE profileId = :profileId")
|
||||||
fun clear(profileId: Int)
|
fun clear(profileId: Int)
|
||||||
|
|
||||||
|
@Query("DELETE FROM endpointTimers")
|
||||||
|
fun clear()
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,9 @@ abstract class EventTypeDao {
|
|||||||
@Query("DELETE FROM eventTypes WHERE profileId = :profileId")
|
@Query("DELETE FROM eventTypes WHERE profileId = :profileId")
|
||||||
abstract fun clear(profileId: Int)
|
abstract fun clear(profileId: Int)
|
||||||
|
|
||||||
|
@Query("DELETE FROM eventTypes WHERE profileId = :profileId AND eventTypeSource = :source")
|
||||||
|
abstract fun clearBySource(profileId: Int, source: Int)
|
||||||
|
|
||||||
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId")
|
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId")
|
||||||
abstract fun getByIdNow(profileId: Int, typeId: Long): EventType?
|
abstract fun getByIdNow(profileId: Int, typeId: Long): EventType?
|
||||||
|
|
||||||
@ -43,7 +46,7 @@ abstract class EventTypeDao {
|
|||||||
val typeList = data.eventTypes.map {
|
val typeList = data.eventTypes.map {
|
||||||
EventType(
|
EventType(
|
||||||
profileId = profile.id,
|
profileId = profile.id,
|
||||||
id = it.id.toLong(),
|
id = it.id,
|
||||||
name = it.name,
|
name = it.name,
|
||||||
color = Color.parseColor(it.color),
|
color = Color.parseColor(it.color),
|
||||||
order = order++,
|
order = order++,
|
||||||
@ -53,4 +56,21 @@ abstract class EventTypeDao {
|
|||||||
addAll(typeList)
|
addAll(typeList)
|
||||||
return typeList
|
return typeList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllWithDefaults(profile: Profile): List<EventType> {
|
||||||
|
val eventTypes = getAllNow(profile.id)
|
||||||
|
|
||||||
|
val defaultIdsExpected = AppData.get(profile.loginStoreType).eventTypes
|
||||||
|
.map { it.id }
|
||||||
|
val defaultIdsFound = eventTypes.filter { it.source == SOURCE_DEFAULT }
|
||||||
|
.sortedBy { it.order }
|
||||||
|
.map { it.id }
|
||||||
|
|
||||||
|
if (defaultIdsExpected == defaultIdsFound)
|
||||||
|
return eventTypes
|
||||||
|
|
||||||
|
clearBySource(profile.id, SOURCE_DEFAULT)
|
||||||
|
addDefaultTypes(profile)
|
||||||
|
return eventTypes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,9 @@ interface ProfileDao {
|
|||||||
@Query("SELECT * FROM profiles WHERE profileId = :profileId")
|
@Query("SELECT * FROM profiles WHERE profileId = :profileId")
|
||||||
fun getByIdNow(profileId: Int): Profile?
|
fun getByIdNow(profileId: Int): Profile?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM profiles WHERE profileId = :profileId")
|
||||||
|
suspend fun getByIdSuspend(profileId: Int): Profile?
|
||||||
|
|
||||||
@get:Query("SELECT * FROM profiles WHERE profileId >= 0 ORDER BY profileId")
|
@get:Query("SELECT * FROM profiles WHERE profileId >= 0 ORDER BY profileId")
|
||||||
val all: LiveData<List<Profile>>
|
val all: LiveData<List<Profile>>
|
||||||
|
|
||||||
|
@ -5,31 +5,6 @@ package pl.szczodrzynski.edziennik.data.db.entity
|
|||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
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(
|
@Entity(
|
||||||
tableName = "eventTypes",
|
tableName = "eventTypes",
|
||||||
@ -55,35 +30,5 @@ class EventType(
|
|||||||
const val SOURCE_REGISTER = 1
|
const val SOURCE_REGISTER = 1
|
||||||
const val SOURCE_CUSTOM = 2
|
const val SOURCE_CUSTOM = 2
|
||||||
const val SOURCE_SHARED = 3
|
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ open class Grade(
|
|||||||
const val TYPE_DESCRIPTIVE = 30
|
const val TYPE_DESCRIPTIVE = 30
|
||||||
const val TYPE_DESCRIPTIVE_TEXT = 31
|
const val TYPE_DESCRIPTIVE_TEXT = 31
|
||||||
const val TYPE_TEXT = 40
|
const val TYPE_TEXT = 40
|
||||||
|
const val TYPE_NO_GRADE = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColumnInfo(name = "gradeValueMax")
|
@ColumnInfo(name = "gradeValueMax")
|
||||||
|
@ -17,4 +17,5 @@ enum class LoginMode(
|
|||||||
VULCAN_HEBE(LoginType.VULCAN, id = 402),
|
VULCAN_HEBE(LoginType.VULCAN, id = 402),
|
||||||
PODLASIE_API(LoginType.PODLASIE, id = 600),
|
PODLASIE_API(LoginType.PODLASIE, id = 600),
|
||||||
USOS_OAUTH(LoginType.USOS, id = 700),
|
USOS_OAUTH(LoginType.USOS, id = 700),
|
||||||
|
DEMO(LoginType.DEMO, id = 800),
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ enum class LoginType(
|
|||||||
VULCAN(id = 4, features = FEATURES_VULCAN),
|
VULCAN(id = 4, features = FEATURES_VULCAN),
|
||||||
PODLASIE(id = 6, features = FEATURES_PODLASIE),
|
PODLASIE(id = 6, features = FEATURES_PODLASIE),
|
||||||
USOS(id = 7, features = FEATURES_USOS, schoolType = SchoolType.UNIVERSITY),
|
USOS(id = 7, features = FEATURES_USOS, schoolType = SchoolType.UNIVERSITY),
|
||||||
DEMO(id = 20, features = setOf()),
|
DEMO(id = 8, features = setOf()),
|
||||||
TEMPLATE(id = 21, features = setOf()),
|
TEMPLATE(id = 21, features = setOf()),
|
||||||
|
|
||||||
// the graveyard
|
// the graveyard
|
||||||
|
@ -93,6 +93,7 @@ internal val FEATURES_PODLASIE = setOf(
|
|||||||
internal val FEATURES_USOS = setOf(
|
internal val FEATURES_USOS = setOf(
|
||||||
TIMETABLE,
|
TIMETABLE,
|
||||||
AGENDA,
|
AGENDA,
|
||||||
|
GRADES,
|
||||||
|
|
||||||
STUDENT_INFO,
|
STUDENT_INFO,
|
||||||
STUDENT_NUMBER,
|
STUDENT_NUMBER,
|
||||||
|
@ -73,7 +73,8 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
|
|||||||
is Number -> addProperty(key, value)
|
is Number -> addProperty(key, value)
|
||||||
is Boolean -> addProperty(key, value)
|
is Boolean -> addProperty(key, value)
|
||||||
is Enum<*> -> addProperty(key, value.toInt())
|
is Enum<*> -> addProperty(key, value.toInt())
|
||||||
else -> add(key, property.toJsonElement())
|
null -> add(key, null)
|
||||||
|
else -> add(key, value.toJsonElement())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,12 @@ fun pendingIntentFlag(): Int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pendingIntentMutable(): Int {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
return PendingIntent.FLAG_MUTABLE
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
fun Int?.takeValue() = if (this == -1) null else this
|
fun Int?.takeValue() = if (this == -1) null else this
|
||||||
fun Int?.takePositive() = if (this == -1 || this == 0) null else this
|
fun Int?.takePositive() = if (this == -1 || this == 0) null else this
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.R
|
|||||||
import pl.szczodrzynski.edziennik.config.AppData
|
import pl.szczodrzynski.edziennik.config.AppData
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||||
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
|
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
|
||||||
import pl.szczodrzynski.edziennik.utils.ProfileImageHolder
|
import pl.szczodrzynski.edziennik.utils.ProfileImageHolder
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.navlib.ImageHolder
|
import pl.szczodrzynski.navlib.ImageHolder
|
||||||
@ -71,6 +72,7 @@ fun Profile.getAppData() =
|
|||||||
if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType)
|
if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType)
|
||||||
|
|
||||||
fun Profile.shouldArchive(): Boolean {
|
fun Profile.shouldArchive(): Boolean {
|
||||||
|
return false
|
||||||
// vulcan hotfix
|
// vulcan hotfix
|
||||||
if (dateYearEnd.month > 6) {
|
if (dateYearEnd.month > 6) {
|
||||||
dateYearEnd.month = 6
|
dateYearEnd.month = 6
|
||||||
|
@ -15,6 +15,7 @@ import android.text.style.CharacterStyle
|
|||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
import android.text.style.StrikethroughSpan
|
import android.text.style.StrikethroughSpan
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
|
import android.text.style.UnderlineSpan
|
||||||
import androidx.annotation.PluralsRes
|
import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||||
@ -160,6 +161,11 @@ fun CharSequence?.asBoldSpannable(): Spannable {
|
|||||||
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
fun CharSequence?.asUnderlineSpannable(): Spannable {
|
||||||
|
val spannable = SpannableString(this)
|
||||||
|
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
return spannable
|
||||||
|
}
|
||||||
fun CharSequence.asSpannable(
|
fun CharSequence.asSpannable(
|
||||||
vararg spans: CharacterStyle,
|
vararg spans: CharacterStyle,
|
||||||
substring: CharSequence? = null,
|
substring: CharSequence? = null,
|
||||||
|
@ -8,6 +8,7 @@ import android.content.Context
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@ -161,3 +162,12 @@ val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.removeFromParent() {
|
||||||
|
(parent as? ViewGroup)?.removeView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.appendView(child: View) {
|
||||||
|
val parent = parent as? ViewGroup ?: return
|
||||||
|
val index = parent.indexOfChild(this)
|
||||||
|
parent.addView(child, index + 1)
|
||||||
|
}
|
||||||
|
@ -5,8 +5,20 @@
|
|||||||
package pl.szczodrzynski.edziennik.network.cookie
|
package pl.szczodrzynski.edziennik.network.cookie
|
||||||
|
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
class DumbCookie(var cookie: Cookie) {
|
class DumbCookie(var cookie: Cookie) {
|
||||||
|
companion object {
|
||||||
|
fun deserialize(key: String, value: String): DumbCookie? {
|
||||||
|
val (domain, _) = key.split('|', limit = 2)
|
||||||
|
val url = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host(domain)
|
||||||
|
.build()
|
||||||
|
val cookie = Cookie.parse(url, value) ?: return null
|
||||||
|
return DumbCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(domain: String, name: String, value: String, expiresAt: Long? = null) : this(
|
constructor(domain: String, name: String, value: String, expiresAt: Long? = null) : this(
|
||||||
Cookie.Builder()
|
Cookie.Builder()
|
||||||
@ -21,7 +33,10 @@ class DumbCookie(var cookie: Cookie) {
|
|||||||
cookie = Cookie.Builder()
|
cookie = Cookie.Builder()
|
||||||
.name(cookie.name())
|
.name(cookie.name())
|
||||||
.value(cookie.value())
|
.value(cookie.value())
|
||||||
.expiresAt(cookie.expiresAt())
|
.also {
|
||||||
|
if (cookie.persistent())
|
||||||
|
it.expiresAt(cookie.expiresAt())
|
||||||
|
}
|
||||||
.domain(cookie.domain())
|
.domain(cookie.domain())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@ -45,4 +60,7 @@ class DumbCookie(var cookie: Cookie) {
|
|||||||
hash = 31 * hash + cookie.domain().hashCode()
|
hash = 31 * hash + cookie.domain().hashCode()
|
||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun serializeKey() = cookie.domain() + "|" + cookie.name()
|
||||||
|
fun serialize() = serializeKey() to cookie.toString()
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package pl.szczodrzynski.edziennik.network.cookie
|
package pl.szczodrzynski.edziennik.network.cookie
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
import okhttp3.Cookie
|
import okhttp3.Cookie
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@ -26,22 +27,48 @@ class DumbCookieJar(
|
|||||||
) : CookieJar {
|
) : CookieJar {
|
||||||
|
|
||||||
private val prefs = context.getSharedPreferences("cookies", Context.MODE_PRIVATE)
|
private val prefs = context.getSharedPreferences("cookies", Context.MODE_PRIVATE)
|
||||||
val sessionCookies = mutableSetOf<DumbCookie>()
|
private val sessionCookies = mutableSetOf<DumbCookie>()
|
||||||
private val savedCookies = mutableSetOf<DumbCookie>()
|
|
||||||
|
init {
|
||||||
|
val toRemove = mutableListOf<String>()
|
||||||
|
prefs.all.forEach { (key, value) ->
|
||||||
|
if (value !is String)
|
||||||
|
return@forEach
|
||||||
|
val dc = DumbCookie.deserialize(key, value) ?: return@forEach
|
||||||
|
if (dc.cookie.expiresAt() > System.currentTimeMillis())
|
||||||
|
sessionCookies.add(dc)
|
||||||
|
else
|
||||||
|
toRemove.add(key)
|
||||||
|
}
|
||||||
|
prefs.edit {
|
||||||
|
for (key in toRemove) {
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun save(dc: DumbCookie) {
|
private fun save(dc: DumbCookie) {
|
||||||
sessionCookies.remove(dc)
|
sessionCookies.remove(dc)
|
||||||
sessionCookies.add(dc)
|
sessionCookies.add(dc)
|
||||||
if (dc.cookie.persistent() || persistAll) {
|
if (dc.cookie.persistent() || persistAll) {
|
||||||
savedCookies.remove(dc)
|
prefs.edit {
|
||||||
savedCookies.add(dc)
|
val (key, value) = dc.serialize()
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun delete(vararg toRemove: DumbCookie) {
|
private fun delete(vararg toRemove: DumbCookie) {
|
||||||
sessionCookies.removeAll(toRemove)
|
sessionCookies.removeAll(toRemove.toSet())
|
||||||
savedCookies.removeAll(toRemove)
|
prefs.edit {
|
||||||
|
for (dc in toRemove) {
|
||||||
|
val key = dc.serializeKey()
|
||||||
|
if (prefs.contains(key))
|
||||||
|
remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl?, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
|
||||||
for (cookie in cookies) {
|
for (cookie in cookies) {
|
||||||
val dc = DumbCookie(cookie)
|
val dc = DumbCookie(cookie)
|
||||||
save(dc)
|
save(dc)
|
||||||
@ -54,6 +81,10 @@ class DumbCookieJar(
|
|||||||
}.map { it.cookie }
|
}.map { it.cookie }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllDomains(): List<Cookie> {
|
||||||
|
return sessionCookies.map { it.cookie }
|
||||||
|
}
|
||||||
|
|
||||||
fun get(domain: String, name: String): String? {
|
fun get(domain: String, name: String): String? {
|
||||||
return sessionCookies.firstOrNull {
|
return sessionCookies.firstOrNull {
|
||||||
it.domainMatches(domain) && it.cookie.name() == name
|
it.domainMatches(domain) && it.cookie.name() == name
|
||||||
@ -84,7 +115,7 @@ class DumbCookieJar(
|
|||||||
fun getAll(domain: String): Map<String, String> {
|
fun getAll(domain: String): Map<String, String> {
|
||||||
return sessionCookies.filter {
|
return sessionCookies.filter {
|
||||||
it.domainMatches(domain)
|
it.domainMatches(domain)
|
||||||
}.map { it.cookie.name() to it.cookie.value() }.toMap()
|
}.associate { it.cookie.name() to it.cookie.value() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(domain: String, name: String) {
|
fun remove(domain: String, name: String) {
|
||||||
@ -100,4 +131,11 @@ class DumbCookieJar(
|
|||||||
}
|
}
|
||||||
delete(*toRemove.toTypedArray())
|
delete(*toRemove.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearAllDomains() {
|
||||||
|
sessionCookies.clear()
|
||||||
|
prefs.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,13 +142,7 @@ class AgendaFragment : Fragment(), CoroutineScope {
|
|||||||
|
|
||||||
private suspend fun checkEventTypes() {
|
private suspend fun checkEventTypes() {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val eventTypes = app.db.eventTypeDao().getAllNow(app.profileId).map {
|
app.db.eventTypeDao().getAllWithDefaults(app.profile)
|
||||||
it.id
|
|
||||||
}
|
|
||||||
val defaultEventTypes = EventType.getTypeColorMap().keys
|
|
||||||
if (!eventTypes.containsAll(defaultEventTypes)) {
|
|
||||||
app.db.eventTypeDao().addDefaultTypes(app.profile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import android.widget.TextView
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
||||||
import com.mikepenz.iconics.view.IconicsTextView
|
import com.mikepenz.iconics.view.IconicsTextView
|
||||||
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding
|
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding
|
||||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding
|
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding
|
||||||
@ -53,16 +54,24 @@ class AgendaEventRenderer(
|
|||||||
else
|
else
|
||||||
event.time!!.stringHM
|
event.time!!.stringHM
|
||||||
|
|
||||||
|
val agendaSubjectImportant = App.profile.config.ui.agendaSubjectImportant
|
||||||
val eventSubtitle = listOfNotNull(
|
val eventSubtitle = listOfNotNull(
|
||||||
timeText,
|
timeText,
|
||||||
event.subjectLongName,
|
event.subjectLongName.takeIf { !agendaSubjectImportant },
|
||||||
|
event.typeName.takeIf { agendaSubjectImportant },
|
||||||
event.teacherName,
|
event.teacherName,
|
||||||
event.teamName
|
event.teamName
|
||||||
).join(", ")
|
).join(", ")
|
||||||
|
|
||||||
card.foreground.setTintColor(event.eventColor)
|
card.foreground.setTintColor(event.eventColor)
|
||||||
card.background.setTintColor(event.eventColor)
|
card.background.setTintColor(event.eventColor)
|
||||||
manager.setEventTopic(title, event, doneIconColor = textColor)
|
manager.setEventTopic(
|
||||||
|
title = title,
|
||||||
|
event = event,
|
||||||
|
doneIconColor = textColor,
|
||||||
|
showType = !agendaSubjectImportant,
|
||||||
|
showSubject = agendaSubjectImportant,
|
||||||
|
)
|
||||||
title.setTextColor(textColor)
|
title.setTextColor(textColor)
|
||||||
subtitle?.text = eventSubtitle
|
subtitle?.text = eventSubtitle
|
||||||
subtitle?.setTextColor(textColor)
|
subtitle?.setTextColor(textColor)
|
||||||
|
@ -66,9 +66,7 @@ class AttendanceBar : View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DrawAllocation", "CanvasSize")
|
@SuppressLint("DrawAllocation", "CanvasSize")
|
||||||
override fun onDraw(canvas: Canvas?) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
canvas ?: return
|
|
||||||
|
|
||||||
val sum = attendancesList.sumOf { it.count }
|
val sum = attendancesList.sumOf { it.count }
|
||||||
if (sum == 0) {
|
if (sum == 0) {
|
||||||
return
|
return
|
||||||
|
@ -217,6 +217,7 @@ enum class NavTarget(
|
|||||||
location = NavTargetLocation.BOTTOM_SHEET,
|
location = NavTargetLocation.BOTTOM_SHEET,
|
||||||
nameRes = R.string.menu_debug,
|
nameRes = R.string.menu_debug,
|
||||||
icon = CommunityMaterial.Icon.cmd_android_debug_bridge,
|
icon = CommunityMaterial.Icon.cmd_android_debug_bridge,
|
||||||
|
devModeOnly = true,
|
||||||
),
|
),
|
||||||
GRADES_EDITOR(
|
GRADES_EDITOR(
|
||||||
id = 501,
|
id = 501,
|
||||||
|
@ -25,6 +25,7 @@ class RecaptchaDialog(
|
|||||||
private val autoRetry: Boolean = true,
|
private val autoRetry: Boolean = true,
|
||||||
private val onSuccess: (recaptchaCode: String) -> Unit,
|
private val onSuccess: (recaptchaCode: String) -> Unit,
|
||||||
private val onFailure: (() -> Unit)? = null,
|
private val onFailure: (() -> Unit)? = null,
|
||||||
|
private val onServerError: (() -> Unit)? = null,
|
||||||
onShowListener: ((tag: String) -> Unit)? = null,
|
onShowListener: ((tag: String) -> Unit)? = null,
|
||||||
onDismissListener: ((tag: String) -> Unit)? = null,
|
onDismissListener: ((tag: String) -> Unit)? = null,
|
||||||
) : BindingDialog<RecaptchaDialogBinding>(activity, onShowListener, onDismissListener) {
|
) : BindingDialog<RecaptchaDialogBinding>(activity, onShowListener, onDismissListener) {
|
||||||
@ -44,7 +45,11 @@ class RecaptchaDialog(
|
|||||||
|
|
||||||
override suspend fun onBeforeShow(): Boolean {
|
override suspend fun onBeforeShow(): Boolean {
|
||||||
val (title, text, bitmap) = withContext(Dispatchers.Default) {
|
val (title, text, bitmap) = withContext(Dispatchers.Default) {
|
||||||
val html = loadCaptchaHtml() ?: return@withContext null
|
val html = loadCaptchaHtml()
|
||||||
|
if (html == null) {
|
||||||
|
onServerError?.invoke()
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
return@withContext loadCaptchaData(html)
|
return@withContext loadCaptchaData(html)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
onFailure?.invoke()
|
onFailure?.invoke()
|
||||||
|
@ -19,6 +19,7 @@ class RecaptchaPromptDialog(
|
|||||||
private val referer: String,
|
private val referer: String,
|
||||||
private val onSuccess: (recaptchaCode: String) -> Unit,
|
private val onSuccess: (recaptchaCode: String) -> Unit,
|
||||||
private val onCancel: (() -> Unit)?,
|
private val onCancel: (() -> Unit)?,
|
||||||
|
private val onServerError: (() -> Unit)? = null,
|
||||||
onShowListener: ((tag: String) -> Unit)? = null,
|
onShowListener: ((tag: String) -> Unit)? = null,
|
||||||
onDismissListener: ((tag: String) -> Unit)? = null,
|
onDismissListener: ((tag: String) -> Unit)? = null,
|
||||||
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
|
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
|
||||||
@ -62,7 +63,8 @@ class RecaptchaPromptDialog(
|
|||||||
b.checkbox.background = checkboxBackground
|
b.checkbox.background = checkboxBackground
|
||||||
b.checkbox.foreground = checkboxForeground
|
b.checkbox.foreground = checkboxForeground
|
||||||
b.progress.visibility = View.GONE
|
b.progress.visibility = View.GONE
|
||||||
}
|
},
|
||||||
|
onServerError = onServerError,
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import kotlinx.coroutines.launch
|
|||||||
import pl.szczodrzynski.edziennik.*
|
import pl.szczodrzynski.edziennik.*
|
||||||
import pl.szczodrzynski.edziennik.config.Config
|
import pl.szczodrzynski.edziennik.config.Config
|
||||||
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
|
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT
|
||||||
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
|
||||||
import pl.szczodrzynski.edziennik.ext.*
|
import pl.szczodrzynski.edziennik.ext.*
|
||||||
import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment
|
import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment
|
||||||
@ -65,6 +66,7 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
|
|||||||
b.clearEndpointTimers.isVisible = false
|
b.clearEndpointTimers.isVisible = false
|
||||||
b.rodo.isVisible = false
|
b.rodo.isVisible = false
|
||||||
b.removeHomework.isVisible = false
|
b.removeHomework.isVisible = false
|
||||||
|
b.resetEventTypes.isVisible = false
|
||||||
b.unarchive.isVisible = false
|
b.unarchive.isVisible = false
|
||||||
b.profile.isVisible = false
|
b.profile.isVisible = false
|
||||||
}
|
}
|
||||||
@ -100,6 +102,11 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
|
|||||||
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
|
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.resetEventTypes.onClick {
|
||||||
|
app.db.eventTypeDao().clearBySource(App.profileId, SOURCE_DEFAULT)
|
||||||
|
app.db.eventTypeDao().getAllWithDefaults(App.profile)
|
||||||
|
}
|
||||||
|
|
||||||
b.chucker.isChecked = App.enableChucker
|
b.chucker.isChecked = App.enableChucker
|
||||||
b.chucker.onChange { _, isChecked ->
|
b.chucker.onChange { _, isChecked ->
|
||||||
app.config.enableChucker = isChecked
|
app.config.enableChucker = isChecked
|
||||||
@ -172,28 +179,39 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
|
|||||||
return@setOnChangeListener true
|
return@setOnChangeListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.clearCookies.onClick {
|
||||||
|
app.cookieJar.clearAllDomains()
|
||||||
|
}
|
||||||
|
|
||||||
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
|
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
|
||||||
startCoroutineTimer(500L, 300L) {
|
startCoroutineTimer(500L, 300L) {
|
||||||
val text = app.cookieJar.sessionCookies
|
val text = app.cookieJar.getAllDomains()
|
||||||
.map { it.cookie }
|
.sortedBy { it.domain() }
|
||||||
.sortedBy { it.domain() }
|
.groupBy { it.domain() }
|
||||||
.groupBy { it.domain() }
|
.map { pair ->
|
||||||
.map {
|
listOf(
|
||||||
listOf(
|
pair.key.asBoldSpannable(),
|
||||||
it.key.asBoldSpannable(),
|
":\n",
|
||||||
":\n",
|
pair.value
|
||||||
it.value
|
.sortedBy { it.name() }
|
||||||
.sortedBy { it.name() }
|
.map { cookie ->
|
||||||
.map {
|
listOf(
|
||||||
listOf(
|
" ",
|
||||||
" ",
|
if (cookie.persistent())
|
||||||
it.name(),
|
cookie.name()
|
||||||
"=",
|
.asUnderlineSpannable()
|
||||||
it.value().decode().take(40).asItalicSpannable().asColoredSpannable(colorSecondary)
|
else
|
||||||
).concat("")
|
cookie.name(),
|
||||||
}.concat("\n")
|
"=",
|
||||||
).concat("")
|
cookie.value()
|
||||||
}.concat("\n\n")
|
.decode()
|
||||||
|
.take(40)
|
||||||
|
.asItalicSpannable()
|
||||||
|
.asColoredSpannable(colorSecondary),
|
||||||
|
).concat("")
|
||||||
|
}.concat("\n")
|
||||||
|
).concat("")
|
||||||
|
}.concat("\n\n")
|
||||||
b.cookies.text = text
|
b.cookies.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_M
|
|||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC
|
||||||
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS
|
||||||
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_SIMPLE
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG
|
||||||
@ -47,6 +49,8 @@ class GradesConfigDialog(
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override suspend fun loadConfig() {
|
override suspend fun loadConfig() {
|
||||||
|
b.isUniversity = app.gradesManager.isUniversity
|
||||||
|
|
||||||
b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null
|
b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null
|
||||||
b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked
|
b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked
|
||||||
b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null
|
b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null
|
||||||
@ -76,10 +80,18 @@ class GradesConfigDialog(
|
|||||||
else -> null
|
else -> null
|
||||||
}?.isChecked = true
|
}?.isChecked = true
|
||||||
|
|
||||||
|
when (app.profile.config.grades.universityAverageMode) {
|
||||||
|
UNIVERSITY_AVERAGE_MODE_ECTS -> b.gradeUniversityAverageMode1
|
||||||
|
UNIVERSITY_AVERAGE_MODE_SIMPLE -> b.gradeUniversityAverageMode0
|
||||||
|
else -> null
|
||||||
|
}?.isChecked = true
|
||||||
|
|
||||||
b.dontCountGrades.isChecked =
|
b.dontCountGrades.isChecked =
|
||||||
app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty()
|
app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty()
|
||||||
b.hideImproved.isChecked = app.profile.config.grades.hideImproved
|
b.hideImproved.isChecked = app.profile.config.grades.hideImproved
|
||||||
|
b.hideNoGrade.isChecked = app.profile.config.grades.hideNoGrade
|
||||||
b.averageWithoutWeight.isChecked = app.profile.config.grades.averageWithoutWeight
|
b.averageWithoutWeight.isChecked = app.profile.config.grades.averageWithoutWeight
|
||||||
|
b.countEctsInProgress.isChecked = app.profile.config.grades.countEctsInProgress
|
||||||
|
|
||||||
if (app.profile.config.grades.dontCountGrades.isEmpty()) {
|
if (app.profile.config.grades.dontCountGrades.isEmpty()) {
|
||||||
b.dontCountGradesText.setText("nb, 0, bz, bd")
|
b.dontCountGradesText.setText("nb, 0, bz, bd")
|
||||||
@ -149,10 +161,21 @@ class GradesConfigDialog(
|
|||||||
app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_SEM
|
app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_SEM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.gradeUniversityAverageMode1.setOnSelectedListener {
|
||||||
|
app.profile.config.grades.universityAverageMode = UNIVERSITY_AVERAGE_MODE_ECTS
|
||||||
|
}
|
||||||
|
b.gradeUniversityAverageMode0.setOnSelectedListener {
|
||||||
|
app.profile.config.grades.universityAverageMode = UNIVERSITY_AVERAGE_MODE_SIMPLE
|
||||||
|
}
|
||||||
|
|
||||||
b.hideImproved.onChange { _, isChecked -> app.profile.config.grades.hideImproved = isChecked }
|
b.hideImproved.onChange { _, isChecked -> app.profile.config.grades.hideImproved = isChecked }
|
||||||
|
b.hideNoGrade.onChange { _, isChecked -> app.profile.config.grades.hideNoGrade = isChecked }
|
||||||
b.averageWithoutWeight.onChange { _, isChecked ->
|
b.averageWithoutWeight.onChange { _, isChecked ->
|
||||||
app.profile.config.grades.averageWithoutWeight = isChecked
|
app.profile.config.grades.averageWithoutWeight = isChecked
|
||||||
}
|
}
|
||||||
|
b.countEctsInProgress.onChange { _, isChecked ->
|
||||||
|
app.profile.config.grades.countEctsInProgress = isChecked
|
||||||
|
}
|
||||||
|
|
||||||
b.averageWithoutWeightHelp.onClick {
|
b.averageWithoutWeightHelp.onClick {
|
||||||
MaterialAlertDialogBuilder(activity)
|
MaterialAlertDialogBuilder(activity)
|
||||||
|
@ -113,9 +113,20 @@ class EventDetailsDialog(
|
|||||||
|
|
||||||
b.typeColor.background?.setTintColor(event.eventColor)
|
b.typeColor.background?.setTintColor(event.eventColor)
|
||||||
|
|
||||||
b.details = mutableListOf(
|
val agendaSubjectImportant = event.subjectLongName != null
|
||||||
|
&& App.config[event.profileId].ui.agendaSubjectImportant
|
||||||
|
|
||||||
|
b.name = if (agendaSubjectImportant)
|
||||||
|
event.subjectLongName
|
||||||
|
else
|
||||||
|
event.typeName
|
||||||
|
|
||||||
|
b.details = listOfNotNull(
|
||||||
|
if (agendaSubjectImportant)
|
||||||
|
event.typeName
|
||||||
|
else
|
||||||
event.subjectLongName,
|
event.subjectLongName,
|
||||||
event.teamName?.asColoredSpannable(colorSecondary)
|
event.teamName?.asColoredSpannable(colorSecondary)
|
||||||
).concat(bullet)
|
).concat(bullet)
|
||||||
|
|
||||||
b.addedBy.setText(
|
b.addedBy.setText(
|
||||||
|
@ -24,6 +24,7 @@ class EventListAdapter(
|
|||||||
val showDate: Boolean = false,
|
val showDate: Boolean = false,
|
||||||
val showColor: Boolean = true,
|
val showColor: Boolean = true,
|
||||||
val showType: Boolean = true,
|
val showType: Boolean = true,
|
||||||
|
val showTypeColor: Boolean = showType,
|
||||||
val showTime: Boolean = true,
|
val showTime: Boolean = true,
|
||||||
val showSubject: Boolean = true,
|
val showSubject: Boolean = true,
|
||||||
val markAsSeen: Boolean = true,
|
val markAsSeen: Boolean = true,
|
||||||
|
@ -19,7 +19,9 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.config.AppData
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
|
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
|
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
|
||||||
@ -35,9 +37,11 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
|||||||
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
||||||
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
|
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
|
||||||
import pl.szczodrzynski.edziennik.ext.JsonObject
|
import pl.szczodrzynski.edziennik.ext.JsonObject
|
||||||
|
import pl.szczodrzynski.edziennik.ext.appendView
|
||||||
import pl.szczodrzynski.edziennik.ext.getStudentData
|
import pl.szczodrzynski.edziennik.ext.getStudentData
|
||||||
import pl.szczodrzynski.edziennik.ext.onChange
|
import pl.szczodrzynski.edziennik.ext.onChange
|
||||||
import pl.szczodrzynski.edziennik.ext.onClick
|
import pl.szczodrzynski.edziennik.ext.onClick
|
||||||
|
import pl.szczodrzynski.edziennik.ext.removeFromParent
|
||||||
import pl.szczodrzynski.edziennik.ext.setText
|
import pl.szczodrzynski.edziennik.ext.setText
|
||||||
import pl.szczodrzynski.edziennik.ext.setTintColor
|
import pl.szczodrzynski.edziennik.ext.setTintColor
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
|
||||||
@ -117,6 +121,15 @@ class EventManualDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onShow() {
|
override suspend fun onShow() {
|
||||||
|
val data = withContext(Dispatchers.IO) {
|
||||||
|
val profile = app.db.profileDao().getByIdSuspend(profileId) ?: return@withContext null
|
||||||
|
AppData.get(profile.loginStoreType)
|
||||||
|
}
|
||||||
|
if (data?.uiConfig?.eventManualShowSubjectDropdown == true) {
|
||||||
|
b.subjectDropdownLayout.removeFromParent()
|
||||||
|
b.timeDropdownLayout.appendView(b.subjectDropdownLayout)
|
||||||
|
}
|
||||||
|
|
||||||
b.showMore.onClick { // TODO iconics is broken
|
b.showMore.onClick { // TODO iconics is broken
|
||||||
it.apply {
|
it.apply {
|
||||||
refreshDrawableState()
|
refreshDrawableState()
|
||||||
|
@ -113,7 +113,7 @@ class EventViewHolder(
|
|||||||
b.attachmentIcon.isVisible = item.hasAttachments
|
b.attachmentIcon.isVisible = item.hasAttachments
|
||||||
|
|
||||||
b.typeColor.background?.setTintColor(item.eventColor)
|
b.typeColor.background?.setTintColor(item.eventColor)
|
||||||
b.typeColor.isVisible = adapter.showType && adapter.showColor
|
b.typeColor.isVisible = adapter.showTypeColor
|
||||||
|
|
||||||
b.editButton.isVisible = !adapter.simpleMode
|
b.editButton.isVisible = !adapter.simpleMode
|
||||||
&& item.addedManually
|
&& item.addedManually
|
||||||
|
@ -16,6 +16,7 @@ import kotlinx.coroutines.Job
|
|||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Grade
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
||||||
import pl.szczodrzynski.edziennik.ext.onClick
|
import pl.szczodrzynski.edziennik.ext.onClick
|
||||||
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
|
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
|
||||||
@ -134,6 +135,7 @@ class GradesAdapter(
|
|||||||
if (model.state == STATE_CLOSED) {
|
if (model.state == STATE_CLOSED) {
|
||||||
|
|
||||||
val subItems = when {
|
val subItems = when {
|
||||||
|
model is GradesSubject && manager.isUniversity -> listOf()
|
||||||
model is GradesSemester && model.grades.isEmpty() ->
|
model is GradesSemester && model.grades.isEmpty() ->
|
||||||
listOf(GradesEmpty())
|
listOf(GradesEmpty())
|
||||||
model is GradesSemester && manager.hideImproved ->
|
model is GradesSemester && manager.hideImproved ->
|
||||||
@ -147,10 +149,12 @@ class GradesAdapter(
|
|||||||
if (notifyAdapter) notifyItemInserted(position)
|
if (notifyAdapter) notifyItemInserted(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
position++
|
|
||||||
model.state = STATE_OPENED
|
model.state = STATE_OPENED
|
||||||
items.addAll(position, subItems.filterNotNull())
|
if (subItems.isNotEmpty()) {
|
||||||
if (notifyAdapter) notifyItemRangeInserted(position, subItems.size)
|
position++
|
||||||
|
items.addAll(position, subItems.filterNotNull())
|
||||||
|
if (notifyAdapter) notifyItemRangeInserted(position, subItems.size)
|
||||||
|
}
|
||||||
|
|
||||||
if (model is GradesSubject) {
|
if (model is GradesSubject) {
|
||||||
// auto expand first semester
|
// auto expand first semester
|
||||||
@ -232,10 +236,13 @@ class GradesAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item !is GradeFull || onGradeClick != null)
|
if (item !is GradeFull || (onGradeClick != null && item.type != TYPE_NO_GRADE)) {
|
||||||
holder.itemView.setOnClickListener(onClickListener)
|
holder.itemView.setOnClickListener(onClickListener)
|
||||||
else
|
holder.itemView.isEnabled = true
|
||||||
|
} else {
|
||||||
holder.itemView.setOnClickListener(null)
|
holder.itemView.setOnClickListener(null)
|
||||||
|
holder.itemView.isEnabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyItemChanged(model: Any) {
|
fun notifyItemChanged(model: Any) {
|
||||||
|
@ -18,6 +18,7 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import pl.szczodrzynski.edziennik.*
|
import pl.szczodrzynski.edziennik.*
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Grade
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE
|
||||||
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
|
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
||||||
import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding
|
||||||
@ -28,7 +29,10 @@ import pl.szczodrzynski.edziennik.ui.grades.models.GradesAverages
|
|||||||
import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester
|
import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester
|
||||||
import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats
|
import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats
|
||||||
import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject
|
import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject
|
||||||
|
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
|
||||||
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
|
||||||
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS
|
||||||
|
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_SIMPLE
|
||||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@ -85,6 +89,35 @@ class GradesListFragment : Fragment(), CoroutineScope {
|
|||||||
else -> grades
|
else -> grades
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (manager.isUniversity) {
|
||||||
|
val termIds = grades.map { it.comment }.toSet().toMutableList()
|
||||||
|
val termNames: MutableMap<String, String> = mutableMapOf()
|
||||||
|
// deserialize to a map of termId to (orderKey, termName)
|
||||||
|
val terms = app.profile.getStudentData("termNames", null)
|
||||||
|
?.let { app.gson.fromJson(it, termNames::class.java) }
|
||||||
|
?.mapValues { (_, value) -> value.split('$', limit = 2) }
|
||||||
|
?.mapValues { (_, value) -> Pair(value[0].toIntOrNull() ?: 0, value[1]) }
|
||||||
|
?: mapOf()
|
||||||
|
// sort by order key
|
||||||
|
termIds.sortByDescending { termId -> terms[termId]?.first ?: 0 }
|
||||||
|
// populate the dropdown
|
||||||
|
b.semesterLayout.isVisible = true
|
||||||
|
b.semesterDropdown.items = termIds.mapIndexed { id, termId ->
|
||||||
|
TextInputDropDown.Item(
|
||||||
|
id.toLong(),
|
||||||
|
terms[termId]?.second ?: termId ?: "-",
|
||||||
|
tag = termId,
|
||||||
|
)
|
||||||
|
}.toMutableList()
|
||||||
|
b.semesterDropdown.select(index = 0)
|
||||||
|
b.semesterDropdown.setOnChangeListener { item ->
|
||||||
|
b.semesterDropdown.select(item)
|
||||||
|
adapter.items = processGrades(items)
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
return@setOnChangeListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load & configure the adapter
|
// load & configure the adapter
|
||||||
adapter.items = withContext(Dispatchers.Default) { processGrades(items) }
|
adapter.items = withContext(Dispatchers.Default) { processGrades(items) }
|
||||||
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
|
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
|
||||||
@ -188,13 +221,23 @@ class GradesListFragment : Fragment(), CoroutineScope {
|
|||||||
var semesterNumber = 0
|
var semesterNumber = 0
|
||||||
var subject = GradesSubject(subjectId, "")
|
var subject = GradesSubject(subjectId, "")
|
||||||
var semester = GradesSemester(0, 1)
|
var semester = GradesSemester(0, 1)
|
||||||
|
val isUniversity = manager.isUniversity
|
||||||
|
val filterTermId = b.semesterDropdown.selected?.tag
|
||||||
|
|
||||||
val hideImproved = manager.hideImproved
|
val hideNoGrade = app.profile.config.grades.hideNoGrade
|
||||||
|
val countEctsInProgress = app.profile.config.grades.countEctsInProgress
|
||||||
|
val universityAverageMode = app.profile.config.grades.universityAverageMode
|
||||||
|
|
||||||
// grades returned by the query are ordered
|
// grades returned by the query are ordered
|
||||||
// by the subject ID, so it's easier and probably
|
// by the subject ID, so it's easier and probably
|
||||||
// a bit faster to build all the models
|
// a bit faster to build all the models
|
||||||
for (grade in grades) {
|
for (grade in grades) {
|
||||||
|
if (isUniversity && filterTermId != null && grade.comment != filterTermId)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (hideNoGrade && grade.type == TYPE_NO_GRADE)
|
||||||
|
continue
|
||||||
|
|
||||||
/*if (grade.parentId != null && grade.parentId != -1L)
|
/*if (grade.parentId != null && grade.parentId != -1L)
|
||||||
continue // the grade is hidden as a new, improved one is available*/
|
continue // the grade is hidden as a new, improved one is available*/
|
||||||
if (grade.subjectId != subjectId) {
|
if (grade.subjectId != subjectId) {
|
||||||
@ -258,8 +301,63 @@ class GradesListFragment : Fragment(), CoroutineScope {
|
|||||||
subject.lastAddedDate = max(subject.lastAddedDate, grade.addedDate)
|
subject.lastAddedDate = max(subject.lastAddedDate, grade.addedDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (manager.orderBy) {
|
||||||
|
GradesManager.ORDER_BY_DATE_DESC -> items.sortByDescending { it.lastAddedDate }
|
||||||
|
GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate }
|
||||||
|
}
|
||||||
|
|
||||||
val stats = GradesStats()
|
val stats = GradesStats()
|
||||||
|
|
||||||
|
if (isUniversity) {
|
||||||
|
val semesterSum = mutableListOf<Float>()
|
||||||
|
val semesterCount = mutableListOf<Float>()
|
||||||
|
val totalSum = mutableListOf<Float>()
|
||||||
|
val totalCount = mutableListOf<Float>()
|
||||||
|
val ectsPoints = mutableMapOf<Pair<Long, String?>, Float>()
|
||||||
|
for (grade in grades) {
|
||||||
|
val pointsPair = grade.subjectId to grade.comment
|
||||||
|
if (grade.type == TYPE_NO_GRADE && !countEctsInProgress)
|
||||||
|
// reset points if there's an exam that isn't passed yet
|
||||||
|
ectsPoints[pointsPair] = 0.0f
|
||||||
|
|
||||||
|
if (grade.value == 0.0f || grade.weight == 0.0f)
|
||||||
|
continue
|
||||||
|
if (universityAverageMode == UNIVERSITY_AVERAGE_MODE_ECTS)
|
||||||
|
totalSum.add(grade.value * grade.weight)
|
||||||
|
else
|
||||||
|
totalSum.add(grade.value)
|
||||||
|
totalCount.add(grade.weight)
|
||||||
|
|
||||||
|
if (grade.value < 3.0)
|
||||||
|
// exam not passed, reset points for this subject
|
||||||
|
ectsPoints[pointsPair] = 0.0f
|
||||||
|
else if (pointsPair !in ectsPoints)
|
||||||
|
// no points for this subject, simply assign
|
||||||
|
ectsPoints[pointsPair] = grade.weight
|
||||||
|
|
||||||
|
if (filterTermId != null && grade.comment != filterTermId)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (universityAverageMode == UNIVERSITY_AVERAGE_MODE_ECTS)
|
||||||
|
semesterSum.add(grade.value * grade.weight)
|
||||||
|
else
|
||||||
|
semesterSum.add(grade.value)
|
||||||
|
semesterCount.add(grade.weight)
|
||||||
|
}
|
||||||
|
when (universityAverageMode) {
|
||||||
|
UNIVERSITY_AVERAGE_MODE_SIMPLE -> {
|
||||||
|
stats.universitySem = semesterSum.sum() / semesterCount.size
|
||||||
|
stats.universityTotal = totalSum.sum() / totalCount.size
|
||||||
|
}
|
||||||
|
UNIVERSITY_AVERAGE_MODE_ECTS -> {
|
||||||
|
stats.universitySem = semesterSum.sum() / semesterCount.sum()
|
||||||
|
stats.universityTotal = totalSum.sum() / totalCount.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stats.universityEcts = ectsPoints.values.sum()
|
||||||
|
return (items + stats).toMutableList()
|
||||||
|
}
|
||||||
|
|
||||||
val sem1Expected = mutableListOf<Float>()
|
val sem1Expected = mutableListOf<Float>()
|
||||||
val sem2Expected = mutableListOf<Float>()
|
val sem2Expected = mutableListOf<Float>()
|
||||||
val yearlyExpected = mutableListOf<Float>()
|
val yearlyExpected = mutableListOf<Float>()
|
||||||
@ -330,11 +428,6 @@ class GradesListFragment : Fragment(), CoroutineScope {
|
|||||||
stats.pointSem2 = sem2Point.averageOrNull()?.toFloat() ?: 0f
|
stats.pointSem2 = sem2Point.averageOrNull()?.toFloat() ?: 0f
|
||||||
stats.pointYearly = yearlyPoint.averageOrNull()?.toFloat() ?: 0f
|
stats.pointYearly = yearlyPoint.averageOrNull()?.toFloat() ?: 0f
|
||||||
|
|
||||||
when (manager.orderBy) {
|
|
||||||
GradesManager.ORDER_BY_DATE_DESC -> items.sortByDescending { it.lastAddedDate }
|
|
||||||
GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate }
|
|
||||||
}
|
|
||||||
|
|
||||||
return (items + stats).toMutableList()
|
return (items + stats).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,4 +22,8 @@ class GradesStats {
|
|||||||
var pointSem1 = 0f
|
var pointSem1 = 0f
|
||||||
var pointSem2 = 0f
|
var pointSem2 = 0f
|
||||||
var pointYearly = 0f
|
var pointYearly = 0f
|
||||||
|
|
||||||
|
var universitySem = 0f
|
||||||
|
var universityTotal = 0f
|
||||||
|
var universityEcts = 0f
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
||||||
import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding
|
import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding
|
||||||
import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter
|
import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter
|
||||||
@ -59,9 +60,12 @@ class GradeViewHolder(
|
|||||||
b.gradeWeight.isVisible = weightText != null
|
b.gradeWeight.isVisible = weightText != null
|
||||||
|
|
||||||
b.gradeTeacherName.text = grade.teacherName
|
b.gradeTeacherName.text = grade.teacherName
|
||||||
b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let {
|
if (grade.addedDate == 0L || grade.type == TYPE_NO_GRADE)
|
||||||
it.getRelativeString(app, 5) ?: it.formattedStringShort
|
b.gradeAddedDate.text = null
|
||||||
}
|
else
|
||||||
|
b.gradeAddedDate.text = Date.fromMillis(grade.addedDate).let {
|
||||||
|
it.getRelativeString(app, 5) ?: it.formattedStringShort
|
||||||
|
}
|
||||||
|
|
||||||
b.unread.isVisible = grade.showAsUnseen
|
b.unread.isVisible = grade.showAsUnseen
|
||||||
if (!grade.seen) {
|
if (!grade.seen) {
|
||||||
|
@ -31,9 +31,35 @@ class StatsViewHolder(
|
|||||||
|
|
||||||
override fun onBind(activity: AppCompatActivity, app: App, item: GradesStats, position: Int, adapter: GradesAdapter) {
|
override fun onBind(activity: AppCompatActivity, app: App, item: GradesStats, position: Int, adapter: GradesAdapter) {
|
||||||
val manager = app.gradesManager
|
val manager = app.gradesManager
|
||||||
|
val isUniversity = manager.isUniversity
|
||||||
val showAverages = mutableListOf<Int>()
|
val showAverages = mutableListOf<Int>()
|
||||||
val showPoint = mutableListOf<Int>()
|
val showPoint = mutableListOf<Int>()
|
||||||
|
|
||||||
|
b.universityTitle.isVisible = isUniversity
|
||||||
|
b.universityLayout.isVisible = isUniversity
|
||||||
|
b.universityDivider.isVisible = isUniversity
|
||||||
|
|
||||||
|
if (isUniversity) {
|
||||||
|
val format = DecimalFormat("#.00")
|
||||||
|
|
||||||
|
b.normalTitle.isVisible = false
|
||||||
|
b.normalLayout.isVisible = false
|
||||||
|
b.normalDivider.isVisible = false
|
||||||
|
b.helpButton.isVisible = false
|
||||||
|
b.pointTitle.isVisible = false
|
||||||
|
b.pointLayout.isVisible = false
|
||||||
|
b.pointDivider.isVisible = false
|
||||||
|
b.noData.isVisible = false
|
||||||
|
b.disclaimer.isVisible = true
|
||||||
|
b.customValueDivider.isVisible = false
|
||||||
|
b.customValueLayout.isVisible = false
|
||||||
|
|
||||||
|
b.universitySemester.text = format.format(item.universitySem)
|
||||||
|
b.universityTotal.text = format.format(item.universityTotal)
|
||||||
|
b.universityEcts.text = format.format(item.universityEcts)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
getSemesterString(app, item.normalSem1, item.normalSem1Proposed, item.normalSem1Final, item.sem1NotAllFinal).let { (average, notice) ->
|
getSemesterString(app, item.normalSem1, item.normalSem1Proposed, item.normalSem1Final, item.sem1NotAllFinal).let { (average, notice) ->
|
||||||
b.normalSemester1Layout.isVisible = average != null
|
b.normalSemester1Layout.isVisible = average != null
|
||||||
b.normalSemester1Notice.isVisible = notice != null
|
b.normalSemester1Notice.isVisible = notice != null
|
||||||
|
@ -62,9 +62,24 @@ class SubjectViewHolder(
|
|||||||
|
|
||||||
val firstSemester = item.semesters.firstOrNull() ?: return
|
val firstSemester = item.semesters.firstOrNull() ?: return
|
||||||
|
|
||||||
b.yearSummary.text = manager.getYearSummaryString(app, item.semesters.map { it.grades.size }.sum(), item.averages)
|
if (manager.isUniversity) {
|
||||||
|
val ectsPoints = item.semesters.firstOrNull()?.grades?.maxOf { it.weight }
|
||||||
|
b.yearSummary.text = if (ectsPoints != null)
|
||||||
|
contextWrapper.getString(
|
||||||
|
R.string.grades_ects_points_format,
|
||||||
|
ectsPoints
|
||||||
|
)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
b.yearSummary.text = manager.getYearSummaryString(
|
||||||
|
app,
|
||||||
|
item.semesters.map { it.grades.size }.sum(),
|
||||||
|
item.averages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (firstSemester.number != item.semester) {
|
if (firstSemester.number != item.semester && !manager.isUniversity) {
|
||||||
b.gradesContainer.addView(TextView(contextWrapper).apply {
|
b.gradesContainer.addView(TextView(contextWrapper).apply {
|
||||||
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
|
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
|
||||||
setText(R.string.grades_preview_other_semester, firstSemester.number)
|
setText(R.string.grades_preview_other_semester, firstSemester.number)
|
||||||
@ -92,16 +107,18 @@ class SubjectViewHolder(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.previewContainer.addView(TextView(contextWrapper).apply {
|
if (!manager.isUniversity) {
|
||||||
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
|
b.previewContainer.addView(TextView(contextWrapper).apply {
|
||||||
text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number)
|
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
|
||||||
//gravity = Gravity.END
|
text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number)
|
||||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
//gravity = Gravity.END
|
||||||
setMargins(0, 0, 8.dp, 0)
|
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||||
}
|
setMargins(0, 0, 8.dp, 0)
|
||||||
maxLines = 1
|
}
|
||||||
ellipsize = TextUtils.TruncateAt.END
|
maxLines = 1
|
||||||
})
|
ellipsize = TextUtils.TruncateAt.END
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// add the topmost semester's grades to preview container (collapsed)
|
// add the topmost semester's grades to preview container (collapsed)
|
||||||
firstSemester.proposedGrade?.let {
|
firstSemester.proposedGrade?.let {
|
||||||
@ -139,7 +156,7 @@ class SubjectViewHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if showing semester 2, add yearly grades to preview container (collapsed)
|
// if showing semester 2, add yearly grades to preview container (collapsed)
|
||||||
if (firstSemester.number == item.semester) {
|
if (firstSemester.number == item.semester && !manager.isUniversity) {
|
||||||
b.previewContainer.addView(TextView(contextWrapper).apply {
|
b.previewContainer.addView(TextView(contextWrapper).apply {
|
||||||
text = manager.getAverageString(app, item.averages, nameSemester = true)
|
text = manager.getAverageString(app, item.averages, nameSemester = true)
|
||||||
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
|
||||||
|
@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.ui.home
|
package pl.szczodrzynski.edziennik.ui.home
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.jetradarmobile.snowfall.SnowfallView
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
@ -20,6 +22,7 @@ import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
|
|||||||
import pl.szczodrzynski.edziennik.ext.timeLeft
|
import pl.szczodrzynski.edziennik.ext.timeLeft
|
||||||
import pl.szczodrzynski.edziennik.ext.timeTill
|
import pl.szczodrzynski.edziennik.ext.timeTill
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.BellSyncTimeChooseDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.BellSyncTimeChooseDialog
|
||||||
|
import pl.szczodrzynski.edziennik.utils.BigNightUtil
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@ -61,6 +64,7 @@ class CounterActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
it.type != Lesson.TYPE_SHIFTED_SOURCE
|
it.type != Lesson.TYPE_SHIFTED_SOURCE
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
lessonList.onEach { it.filterNotes() }
|
||||||
}
|
}
|
||||||
|
|
||||||
b.bellSync.setImageDrawable(
|
b.bellSync.setImageDrawable(
|
||||||
@ -81,6 +85,27 @@ class CounterActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
counterJob = startCoroutineTimer(repeatMillis = 500) {
|
counterJob = startCoroutineTimer(repeatMillis = 500) {
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IT'S WINTER MY DUDES
|
||||||
|
val today = Date.getToday()
|
||||||
|
if ((today.month / 3 % 4 == 0) && 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)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
private fun update() {
|
private fun update() {
|
||||||
@ -101,13 +126,15 @@ class CounterActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
actual != null -> {
|
actual != null -> {
|
||||||
b.lessonName.text = actual.displaySubjectName
|
b.lessonName.text = actual.getNoteSubstituteText(showNotes = true)
|
||||||
|
?: actual.displaySubjectName
|
||||||
|
|
||||||
val left = actual.displayEndTime!! - now
|
val left = actual.displayEndTime!! - now
|
||||||
b.timeLeft.text = timeLeft(left.toInt(), "\n", countInSeconds)
|
b.timeLeft.text = timeLeft(left.toInt(), "\n", countInSeconds)
|
||||||
}
|
}
|
||||||
next != null -> {
|
next != null -> {
|
||||||
b.lessonName.text = next.displaySubjectName
|
b.lessonName.text = next.getNoteSubstituteText(showNotes = true)
|
||||||
|
?: next.displaySubjectName
|
||||||
|
|
||||||
val till = next.displayStartTime!! - now
|
val till = next.displayStartTime!! - now
|
||||||
b.timeLeft.text = timeTill(till.toInt(), "\n", countInSeconds)
|
b.timeLeft.text = timeTill(till.toInt(), "\n", countInSeconds)
|
||||||
|
@ -63,9 +63,10 @@ class HomeEventsCard(
|
|||||||
simpleMode = true,
|
simpleMode = true,
|
||||||
showWeekDay = true,
|
showWeekDay = true,
|
||||||
showDate = true,
|
showDate = true,
|
||||||
showType = true,
|
showType = !profile.config.ui.agendaSubjectImportant,
|
||||||
|
showTypeColor = true,
|
||||||
showTime = false,
|
showTime = false,
|
||||||
showSubject = false,
|
showSubject = profile.config.ui.agendaSubjectImportant,
|
||||||
markAsSeen = false,
|
markAsSeen = false,
|
||||||
onEventClick = {
|
onEventClick = {
|
||||||
EventDetailsDialog(
|
EventDetailsDialog(
|
||||||
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.Job
|
|||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.MainActivity
|
import pl.szczodrzynski.edziennik.MainActivity
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Subject
|
import pl.szczodrzynski.edziennik.data.db.entity.Subject
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
|
||||||
@ -70,7 +71,7 @@ class HomeGradesCard(
|
|||||||
app.db.gradeDao().getAllFromDate(profile.id, sevenDaysAgo).observe(fragment, Observer {
|
app.db.gradeDao().getAllFromDate(profile.id, sevenDaysAgo).observe(fragment, Observer {
|
||||||
grades.apply {
|
grades.apply {
|
||||||
clear()
|
clear()
|
||||||
addAll(it)
|
addAll(it.filter { it.type != TYPE_NO_GRADE })
|
||||||
}
|
}
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
|
@ -232,6 +232,7 @@ class HomeTimetableCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
lessons = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
|
lessons = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
|
||||||
|
lessons.onEach { it.filterNotes() }
|
||||||
|
|
||||||
b.timetableLayout.visibility = View.VISIBLE
|
b.timetableLayout.visibility = View.VISIBLE
|
||||||
b.noTimetableLayout.visibility = View.GONE
|
b.noTimetableLayout.visibility = View.GONE
|
||||||
@ -344,6 +345,7 @@ class HomeTimetableCard(
|
|||||||
|
|
||||||
private val LessonFull?.subjectSpannable: CharSequence
|
private val LessonFull?.subjectSpannable: CharSequence
|
||||||
get() = if (this == null) "?" else when {
|
get() = if (this == null) "?" else when {
|
||||||
|
hasReplacingNotes() -> getNoteSubstituteText(showNotes = true) ?: "?"
|
||||||
isCancelled -> displaySubjectName?.asStrikethroughSpannable() ?: "?"
|
isCancelled -> displaySubjectName?.asStrikethroughSpannable() ?: "?"
|
||||||
isChange -> displaySubjectName?.asItalicSpannable() ?: "?"
|
isChange -> displaySubjectName?.asItalicSpannable() ?: "?"
|
||||||
else -> displaySubjectName ?: "?"
|
else -> displaySubjectName ?: "?"
|
||||||
|
@ -197,7 +197,7 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
|
|||||||
adapter.items.removeAll { it !is LoginInfo.Register }
|
adapter.items.removeAll { it !is LoginInfo.Register }
|
||||||
adapter.items.add(
|
adapter.items.add(
|
||||||
LoginInfo.Register(
|
LoginInfo.Register(
|
||||||
loginType = LoginType.DEMO,
|
loginType = LoginType.TEMPLATE,
|
||||||
registerName = R.string.eggs,
|
registerName = R.string.eggs,
|
||||||
registerLogo = R.drawable.face_1,
|
registerLogo = R.drawable.face_1,
|
||||||
loginModes = listOf(
|
loginModes = listOf(
|
||||||
@ -241,13 +241,13 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
|
|||||||
loginType: LoginInfo.Register,
|
loginType: LoginInfo.Register,
|
||||||
loginMode: LoginInfo.Mode
|
loginMode: LoginInfo.Mode
|
||||||
) {
|
) {
|
||||||
if (loginType.loginType == LoginType.DEMO) {
|
|
||||||
nav.navigate(R.id.loginEggsFragment, null, activity.navOptions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginType.loginType == LoginType.TEMPLATE) {
|
if (loginType.loginType == LoginType.TEMPLATE) {
|
||||||
nav.navigate(R.id.labFragment, null, activity.navOptions)
|
when (loginType.registerName) {
|
||||||
|
R.string.eggs ->
|
||||||
|
nav.navigate(R.id.loginEggsFragment, null, activity.navOptions)
|
||||||
|
R.string.menu_lab ->
|
||||||
|
nav.navigate(R.id.labFragment, null, activity.navOptions)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,6 +328,21 @@ object LoginInfo {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Register(
|
||||||
|
loginType = LoginType.DEMO,
|
||||||
|
registerName = R.string.login_type_demo,
|
||||||
|
registerLogo = R.mipmap.ic_launcher,
|
||||||
|
loginModes = listOf(
|
||||||
|
Mode(
|
||||||
|
loginMode = LoginMode.DEMO,
|
||||||
|
name = R.string.login_mode_demo,
|
||||||
|
icon = R.mipmap.ic_launcher,
|
||||||
|
guideText = R.string.login_mode_demo,
|
||||||
|
credentials = listOf(),
|
||||||
|
errorCodes = mapOf(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2023-3-24.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.ui.login.recaptcha
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT
|
||||||
|
import pl.szczodrzynski.edziennik.utils.Themes
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
class RecaptchaActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RecaptchaActivity"
|
||||||
|
|
||||||
|
private const val CODE = """
|
||||||
|
PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vd3d3Lmdvb2ds
|
||||||
|
ZS5jb20vcmVjYXB0Y2hhL2FwaS5qcz9vbmxvYWQ9cmVhZHkmcmVuZGVyPWV4cGxpY2l0Ij48L3Nj
|
||||||
|
cmlwdD48L2hlYWQ+PGJvZHk+PGJyPjxkaXYgaWQ9ImdyIiBzdHlsZT0icG9zaXRpb246YWJzb2x1
|
||||||
|
dGU7dG9wOjUwJTt0cmFuc2Zvcm06dHJhbnNsYXRlKDAsLTUwJSk7Ij48L2Rpdj48YnI+PHNjcmlw
|
||||||
|
dD5mdW5jdGlvbiByZWFkeSgpe2dyZWNhcHRjaGEucmVuZGVyKCJnciIse3NpdGVrZXk6IlNJVEVL
|
||||||
|
RVkiLHRoZW1lOiJUSEVNRSIsY2FsbGJhY2s6ZnVuY3Rpb24oZSl7d2luZG93LmlmLmNhbGxiYWNr
|
||||||
|
KGUpO30sImV4cGlyZWQtY2FsbGJhY2siOndpbmRvdy5pZi5leHBpcmVkQ2FsbGJhY2ssImVycm9y
|
||||||
|
LWNhbGxiYWNrIjp3aW5kb3cuaWYuZXJyb3JDYWxsYmFja30pO308L3NjcmlwdD48L2JvZHk+PC9o
|
||||||
|
dG1sPg==
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isSuccessful = false
|
||||||
|
private lateinit var jsInterface: CaptchaCallbackInterface
|
||||||
|
|
||||||
|
interface CaptchaCallbackInterface {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun callback(recaptchaResponse: String)
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun expiredCallback()
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun errorCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("AddJavascriptInterface", "SetJavaScriptEnabled")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setTitle(R.string.recaptcha_dialog_title)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val siteKey = intent.getStringExtra("siteKey") ?: return
|
||||||
|
val referer = intent.getStringExtra("referer") ?: return
|
||||||
|
val userAgent = intent.getStringExtra("userAgent") ?: SYSTEM_USER_AGENT
|
||||||
|
|
||||||
|
val htmlContent = Base64.decode(CODE, Base64.DEFAULT)
|
||||||
|
.toString(Charset.defaultCharset())
|
||||||
|
.replace("THEME", if (Themes.isDark) "dark" else "light")
|
||||||
|
.replace("SITEKEY", siteKey)
|
||||||
|
|
||||||
|
jsInterface = object : CaptchaCallbackInterface {
|
||||||
|
@JavascriptInterface
|
||||||
|
override fun callback(recaptchaResponse: String) {
|
||||||
|
isSuccessful = true
|
||||||
|
EventBus.getDefault().post(
|
||||||
|
RecaptchaResult(
|
||||||
|
isError = false,
|
||||||
|
code = recaptchaResponse,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
override fun expiredCallback() {
|
||||||
|
isSuccessful = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
override fun errorCallback() {
|
||||||
|
isSuccessful = false
|
||||||
|
EventBus.getDefault().post(
|
||||||
|
RecaptchaResult(
|
||||||
|
isError = true,
|
||||||
|
code = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val webView = WebView(this).apply {
|
||||||
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
settings.userAgentString = userAgent
|
||||||
|
addJavascriptInterface(jsInterface, "if")
|
||||||
|
loadDataWithBaseURL(
|
||||||
|
referer,
|
||||||
|
htmlContent,
|
||||||
|
"text/html",
|
||||||
|
"UTF-8",
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
// setLayerType(WebView.LAYER_TYPE_SOFTWARE, null)
|
||||||
|
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
}
|
||||||
|
setContentView(webView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (!isSuccessful)
|
||||||
|
EventBus.getDefault().post(
|
||||||
|
RecaptchaResult(
|
||||||
|
isError = false,
|
||||||
|
code = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2023-3-24.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.ui.login.recaptcha
|
||||||
|
|
||||||
|
data class RecaptchaResult(
|
||||||
|
val isError: Boolean,
|
||||||
|
val code: String?,
|
||||||
|
)
|
@ -26,7 +26,7 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
override fun getItems(card: MaterialAboutCard) = listOfNotNull(
|
override fun getItems(card: MaterialAboutCard) = listOfNotNull(
|
||||||
if (Date.getToday().month % 11 == 1) // cool math games
|
if (Date.getToday().month / 3 % 4 == 0) // cool math games
|
||||||
util.createPropertyItem(
|
util.createPropertyItem(
|
||||||
text = R.string.settings_theme_snowfall_text,
|
text = R.string.settings_theme_snowfall_text,
|
||||||
subText = R.string.settings_theme_snowfall_subtext,
|
subText = R.string.settings_theme_snowfall_subtext,
|
||||||
|
@ -109,12 +109,10 @@ class GenerateBlockTimetableDialog(
|
|||||||
.show()
|
.show()
|
||||||
|
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick {
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick {
|
||||||
app.permissionManager.requestStoragePermission(activity, permissionMessage = R.string.permissions_generate_timetable) {
|
when (b.weekSelectionRadioGroup.checkedRadioButtonId) {
|
||||||
when (b.weekSelectionRadioGroup.checkedRadioButtonId) {
|
R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
|
||||||
R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
|
R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd)
|
||||||
R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd)
|
R.id.forSelectedWeekRadio -> selectDate()
|
||||||
R.id.forSelectedWeekRadio -> selectDate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -390,7 +388,7 @@ class GenerateBlockTimetableDialog(
|
|||||||
try {
|
try {
|
||||||
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null
|
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null
|
||||||
resolver.openOutputStream(uri).use {
|
resolver.openOutputStream(uri).use {
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it ?: return@use)
|
||||||
}
|
}
|
||||||
uri
|
uri
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -40,6 +40,7 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
|||||||
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
||||||
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
|
||||||
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
|
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
|
||||||
|
import pl.szczodrzynski.edziennik.databinding.TimetableNoLessonsBinding
|
||||||
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
|
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
|
||||||
import pl.szczodrzynski.edziennik.ext.Intent
|
import pl.szczodrzynski.edziennik.ext.Intent
|
||||||
import pl.szczodrzynski.edziennik.ext.JsonObject
|
import pl.szczodrzynski.edziennik.ext.JsonObject
|
||||||
@ -63,6 +64,7 @@ import pl.szczodrzynski.edziennik.utils.Colors
|
|||||||
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
|
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||||
|
import pl.szczodrzynski.edziennik.utils.models.Week
|
||||||
import pl.szczodrzynski.edziennik.utils.mutableLazy
|
import pl.szczodrzynski.edziennik.utils.mutableLazy
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@ -182,6 +184,20 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
|||||||
b.root.removeAllViews()
|
b.root.removeAllViews()
|
||||||
b.root.addView(view)
|
b.root.addView(view)
|
||||||
viewsRemoved = true
|
viewsRemoved = true
|
||||||
|
|
||||||
|
val b = TimetableNoLessonsBinding.bind(view)
|
||||||
|
val weekStart = date.weekStart.stringY_m_d
|
||||||
|
b.noLessonsSync.onClick {
|
||||||
|
it.isEnabled = false
|
||||||
|
EdziennikTask.syncProfile(
|
||||||
|
profileId = App.profileId,
|
||||||
|
featureTypes = setOf(FeatureType.TIMETABLE),
|
||||||
|
arguments = JsonObject(
|
||||||
|
"weekStart" to weekStart
|
||||||
|
)
|
||||||
|
).enqueue(activity)
|
||||||
|
}
|
||||||
|
b.noLessonsSync.isVisible = date.weekDay !in Week.SATURDAY..Week.SUNDAY
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewpager.widget.ViewPager
|
import androidx.viewpager.widget.ViewPager
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
@ -27,8 +29,11 @@ import kotlinx.coroutines.launch
|
|||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.App
|
||||||
import pl.szczodrzynski.edziennik.MainActivity
|
import pl.szczodrzynski.edziennik.MainActivity
|
||||||
import pl.szczodrzynski.edziennik.R
|
import pl.szczodrzynski.edziennik.R
|
||||||
|
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||||
|
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
|
||||||
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
|
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
|
||||||
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
|
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
|
||||||
|
import pl.szczodrzynski.edziennik.ext.JsonObject
|
||||||
import pl.szczodrzynski.edziennik.ext.getSchoolYearConstrains
|
import pl.szczodrzynski.edziennik.ext.getSchoolYearConstrains
|
||||||
import pl.szczodrzynski.edziennik.ext.getStudentData
|
import pl.szczodrzynski.edziennik.ext.getStudentData
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.settings.TimetableConfigDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.settings.TimetableConfigDialog
|
||||||
@ -87,8 +92,18 @@ class TimetableFragment : Fragment(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_SCROLL_TO_DATE))
|
ActivityCompat.registerReceiver(
|
||||||
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_RELOAD_PAGES))
|
activity,
|
||||||
|
broadcastReceiver,
|
||||||
|
IntentFilter(ACTION_SCROLL_TO_DATE),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
ActivityCompat.registerReceiver(
|
||||||
|
activity,
|
||||||
|
broadcastReceiver,
|
||||||
|
IntentFilter(ACTION_RELOAD_PAGES),
|
||||||
|
ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
@ -178,6 +193,21 @@ class TimetableFragment : Fragment(), CoroutineScope {
|
|||||||
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == selectedDate?.value ?: today }, false)
|
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == selectedDate?.value ?: today }, false)
|
||||||
|
|
||||||
activity.navView.bottomSheet.prependItems(
|
activity.navView.bottomSheet.prependItems(
|
||||||
|
BottomSheetPrimaryItem(true)
|
||||||
|
.withTitle(R.string.menu_timetable_sync)
|
||||||
|
.withIcon(CommunityMaterial.Icon.cmd_calendar_sync_outline)
|
||||||
|
.withOnClickListener {
|
||||||
|
activity.bottomSheet.close()
|
||||||
|
val date = pageSelection ?: Date.getToday()
|
||||||
|
val weekStart = date.weekStart.stringY_m_d
|
||||||
|
EdziennikTask.syncProfile(
|
||||||
|
profileId = App.profileId,
|
||||||
|
featureTypes = setOf(FeatureType.TIMETABLE),
|
||||||
|
arguments = JsonObject(
|
||||||
|
"weekStart" to weekStart
|
||||||
|
)
|
||||||
|
).enqueue(activity)
|
||||||
|
},
|
||||||
BottomSheetPrimaryItem(true)
|
BottomSheetPrimaryItem(true)
|
||||||
.withTitle(R.string.timetable_select_day)
|
.withTitle(R.string.timetable_select_day)
|
||||||
.withIcon(SzkolnyFont.Icon.szf_calendar_today_outline)
|
.withIcon(SzkolnyFont.Icon.szf_calendar_today_outline)
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package pl.szczodrzynski.edziennik.ui.views
|
package pl.szczodrzynski.edziennik.ui.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@ -50,6 +51,10 @@ class AttachmentsView @JvmOverloads constructor(
|
|||||||
val attachmentSizes = arguments.getLongArray("attachmentSizes")
|
val attachmentSizes = arguments.getLongArray("attachmentSizes")
|
||||||
|
|
||||||
val adapter = AttachmentAdapter(context, onAttachmentClick = { item ->
|
val adapter = AttachmentAdapter(context, onAttachmentClick = { item ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
downloadAttachment(item)
|
||||||
|
return@AttachmentAdapter
|
||||||
|
}
|
||||||
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
|
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
|
||||||
downloadAttachment(item)
|
downloadAttachment(item)
|
||||||
}
|
}
|
||||||
@ -57,6 +62,10 @@ class AttachmentsView @JvmOverloads constructor(
|
|||||||
val popupMenu = PopupMenu(chip.context, chip)
|
val popupMenu = PopupMenu(chip.context, chip)
|
||||||
popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again)
|
popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again)
|
||||||
popupMenu.setOnMenuItemClickListener {
|
popupMenu.setOnMenuItemClickListener {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
downloadAttachment(item)
|
||||||
|
return@setOnMenuItemClickListener true
|
||||||
|
}
|
||||||
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
|
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
|
||||||
downloadAttachment(item, forceDownload = true)
|
downloadAttachment(item, forceDownload = true)
|
||||||
}
|
}
|
||||||
|
@ -33,13 +33,8 @@ class EventTypeDropdown : TextInputDropDown {
|
|||||||
suspend fun loadItems() {
|
suspend fun loadItems() {
|
||||||
val types = withContext(Dispatchers.Default) {
|
val types = withContext(Dispatchers.Default) {
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
|
val types = db.eventTypeDao().getAllNow(profileId)
|
||||||
var types = db.eventTypeDao().getAllNow(profileId)
|
.sortedBy { it.order }
|
||||||
|
|
||||||
if (types.none { it.id in -1L..10L }) {
|
|
||||||
val profile = db.profileDao().getByIdNow(profileId) ?: return@withContext listOf()
|
|
||||||
types = db.eventTypeDao().addDefaultTypes(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
list += types.map {
|
list += types.map {
|
||||||
Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply {
|
Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply {
|
||||||
|
@ -23,6 +23,7 @@ import pl.szczodrzynski.edziennik.R
|
|||||||
import pl.szczodrzynski.edziennik.ext.Bundle
|
import pl.szczodrzynski.edziennik.ext.Bundle
|
||||||
import pl.szczodrzynski.edziennik.ext.getJsonObject
|
import pl.szczodrzynski.edziennik.ext.getJsonObject
|
||||||
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
|
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
|
||||||
|
import pl.szczodrzynski.edziennik.ext.pendingIntentMutable
|
||||||
import pl.szczodrzynski.edziennik.ext.putExtras
|
import pl.szczodrzynski.edziennik.ext.putExtras
|
||||||
import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver
|
import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver
|
||||||
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
|
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
|
||||||
@ -50,7 +51,7 @@ class WidgetNotificationsProvider : AppWidgetProvider() {
|
|||||||
val syncIntent = SzkolnyReceiver.getIntent(context, Bundle(
|
val syncIntent = SzkolnyReceiver.getIntent(context, Bundle(
|
||||||
"task" to "SyncRequest"
|
"task" to "SyncRequest"
|
||||||
))
|
))
|
||||||
val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentFlag())
|
val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentMutable())
|
||||||
views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent)
|
views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent)
|
||||||
|
|
||||||
views.setImageViewBitmap(
|
views.setImageViewBitmap(
|
||||||
@ -71,13 +72,13 @@ class WidgetNotificationsProvider : AppWidgetProvider() {
|
|||||||
|
|
||||||
val itemIntent = Intent(context, MainActivity::class.java)
|
val itemIntent = Intent(context, MainActivity::class.java)
|
||||||
itemIntent.action = Intent.ACTION_MAIN
|
itemIntent.action = Intent.ACTION_MAIN
|
||||||
val itemPendingIntent = PendingIntent.getActivity(context, 0, itemIntent, pendingIntentFlag())
|
val itemPendingIntent = PendingIntent.getActivity(context, appWidgetId, itemIntent, pendingIntentMutable())
|
||||||
views.setPendingIntentTemplate(R.id.widgetNotificationsListView, itemPendingIntent)
|
views.setPendingIntentTemplate(R.id.widgetNotificationsListView, itemPendingIntent)
|
||||||
|
|
||||||
val headerIntent = Intent(context, MainActivity::class.java)
|
val headerIntent = Intent(context, MainActivity::class.java)
|
||||||
headerIntent.action = Intent.ACTION_MAIN
|
headerIntent.action = Intent.ACTION_MAIN
|
||||||
headerIntent.putExtras("fragmentId" to NavTarget.NOTIFICATIONS)
|
headerIntent.putExtras("fragmentId" to NavTarget.NOTIFICATIONS)
|
||||||
val headerPendingIntent = PendingIntent.getActivity(context, 0, headerIntent, pendingIntentFlag())
|
val headerPendingIntent = PendingIntent.getActivity(context, appWidgetId, headerIntent, pendingIntentMutable())
|
||||||
views.setOnClickPendingIntent(R.id.widgetNotificationsHeader, headerPendingIntent)
|
views.setOnClickPendingIntent(R.id.widgetNotificationsHeader, headerPendingIntent)
|
||||||
|
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package pl.szczodrzynski.edziennik.ui.widgets.timetable
|
package pl.szczodrzynski.edziennik.ui.widgets.timetable
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
@ -34,6 +35,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSON
|
|||||||
import pl.szczodrzynski.edziennik.ext.filterOutArchived
|
import pl.szczodrzynski.edziennik.ext.filterOutArchived
|
||||||
import pl.szczodrzynski.edziennik.ext.getJsonObject
|
import pl.szczodrzynski.edziennik.ext.getJsonObject
|
||||||
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
|
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
|
||||||
|
import pl.szczodrzynski.edziennik.ext.pendingIntentMutable
|
||||||
import pl.szczodrzynski.edziennik.ext.putExtras
|
import pl.szczodrzynski.edziennik.ext.putExtras
|
||||||
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
|
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
|
||||||
import pl.szczodrzynski.edziennik.ui.widgets.LessonDialogActivity
|
import pl.szczodrzynski.edziennik.ui.widgets.LessonDialogActivity
|
||||||
@ -119,7 +121,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
|
|||||||
0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag())
|
0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag())
|
||||||
views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, refreshPendingIntent)
|
views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, refreshPendingIntent)
|
||||||
|
|
||||||
views.setOnClickPendingIntent(R.id.widgetTimetableSync, getPendingSelfIntent(context, ACTION_SYNC_DATA))
|
views.setViewVisibility(R.id.widgetTimetableSync, View.GONE)
|
||||||
|
|
||||||
views.setImageViewBitmap(
|
views.setImageViewBitmap(
|
||||||
R.id.widgetTimetableRefresh,
|
R.id.widgetTimetableRefresh,
|
||||||
@ -129,14 +131,6 @@ class WidgetTimetableProvider : AppWidgetProvider() {
|
|||||||
}.toBitmap()
|
}.toBitmap()
|
||||||
)
|
)
|
||||||
|
|
||||||
views.setImageViewBitmap(
|
|
||||||
R.id.widgetTimetableSync,
|
|
||||||
IconicsDrawable(context, CommunityMaterial.Icon.cmd_download_outline).apply {
|
|
||||||
colorInt = Color.WHITE
|
|
||||||
sizeDp = if (config.bigStyle) 28 else 20
|
|
||||||
}.toBitmap()
|
|
||||||
)
|
|
||||||
|
|
||||||
prepareAppWidget(app, appWidgetId, views, config, bellSyncDiffMillis)
|
prepareAppWidget(app, appWidgetId, views, config, bellSyncDiffMillis)
|
||||||
|
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
@ -337,8 +331,11 @@ class WidgetTimetableProvider : AppWidgetProvider() {
|
|||||||
scrollPos = pos + 1
|
scrollPos = pos + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove notes from other profiles
|
||||||
|
lesson.filterNotes()
|
||||||
// set the subject and classroom name
|
// set the subject and classroom name
|
||||||
model.subjectName = lesson.displaySubjectName
|
model.subjectName = lesson.getNoteSubstituteText(showNotes = true)
|
||||||
|
?: lesson.displaySubjectName
|
||||||
model.classroomName = lesson.displayClassroom
|
model.classroomName = lesson.displayClassroom
|
||||||
|
|
||||||
// set the bell sync to calculate progress in ListProvider
|
// set the bell sync to calculate progress in ListProvider
|
||||||
@ -399,7 +396,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE)
|
headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE)
|
||||||
val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentFlag())
|
val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, FLAG_UPDATE_CURRENT or pendingIntentMutable())
|
||||||
views.setOnClickPendingIntent(R.id.widgetTimetableHeader, headerPendingIntent)
|
views.setOnClickPendingIntent(R.id.widgetTimetableHeader, headerPendingIntent)
|
||||||
|
|
||||||
timetables!!.put(appWidgetId, models)
|
timetables!!.put(appWidgetId, models)
|
||||||
@ -413,7 +410,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
|
|||||||
// create an intent used to display the lesson details dialog
|
// create an intent used to display the lesson details dialog
|
||||||
val itemIntent = Intent(app, LessonDialogActivity::class.java)
|
val itemIntent = Intent(app, LessonDialogActivity::class.java)
|
||||||
itemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK/* or Intent.FLAG_ACTIVITY_CLEAR_TASK*/)
|
itemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK/* or Intent.FLAG_ACTIVITY_CLEAR_TASK*/)
|
||||||
val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, PendingIntent.FLAG_MUTABLE)
|
val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, FLAG_UPDATE_CURRENT or pendingIntentMutable())
|
||||||
views.setPendingIntentTemplate(R.id.widgetTimetableListView, itemPendingIntent)
|
views.setPendingIntentTemplate(R.id.widgetTimetableListView, itemPendingIntent)
|
||||||
|
|
||||||
if (!unified)
|
if (!unified)
|
||||||
|
@ -774,14 +774,21 @@ public class Utils {
|
|||||||
|
|
||||||
private static File storageDir = null;
|
private static File storageDir = null;
|
||||||
public static File getStorageDir() {
|
public static File getStorageDir() {
|
||||||
if (storageDir != null)
|
|
||||||
return storageDir;
|
|
||||||
storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
|
||||||
storageDir = new File(storageDir, "Szkolny.eu");
|
|
||||||
storageDir.mkdirs();
|
|
||||||
return storageDir;
|
return storageDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void initializeStorageDir(Context context) {
|
||||||
|
if (storageDir != null)
|
||||||
|
return;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
storageDir = context.getExternalFilesDir(null);
|
||||||
|
} else {
|
||||||
|
storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
storageDir = new File(storageDir, "Szkolny.eu");
|
||||||
|
}
|
||||||
|
storageDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
public static void writeStringToFile(File file, String data) throws IOException {
|
public static void writeStringToFile(File file, String data) throws IOException {
|
||||||
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file));
|
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file));
|
||||||
outputStreamWriter.write(data);
|
outputStreamWriter.write(data);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user