forked from github/szkolny
Compare commits
114 Commits
Author | SHA1 | Date | |
---|---|---|---|
18c306b9ea | |||
c6be1a7954 | |||
e8e9f04050 | |||
3700a71c39 | |||
60f0628f5e | |||
80dcd9aa69 | |||
91b685576b | |||
2e3e3dcf3c | |||
118f5e1794 | |||
e902352a4b | |||
2f7fcb6dc3 | |||
21ddb9d706 | |||
efa63452e7 | |||
83f84de019 | |||
b9aca981e5 | |||
5913707519 | |||
dd6a2c0979 | |||
9fdee6e0c7 | |||
b31bf5c1ab | |||
cf4906f2f4 | |||
680a5dfea3 | |||
c1062cd7ed | |||
8edc581f0b | |||
ea9d801d08 | |||
8f72e11d0c | |||
452271e8c0 | |||
7b4effe889 | |||
e2bf48d1b6 | |||
c88056ddb9 | |||
96dbb0a057 | |||
288c80ea26 | |||
5a217aca01 | |||
4bed62aa6f | |||
a4d604e146 | |||
ae4405ef78 | |||
71ca51e813 | |||
1bf07d736f | |||
909899612e | |||
4184fbb2cd | |||
75010c0771 | |||
5562498e84 | |||
c2d0940a80 | |||
baa98f25c5 | |||
26645ee83c | |||
85d74bec1c | |||
fd0fc652a3 | |||
c85dac2e4d | |||
c855f08f9c | |||
a31c68e87a | |||
99021f6b3a | |||
e2b47db3fd | |||
8609956ae7 | |||
e25ca930e0 | |||
47ec1899a1 | |||
1e8fb6a9ae | |||
02eb5b7ee4 | |||
776806caef | |||
755b846b50 | |||
73f3ba17de | |||
07fb1e0e12 | |||
297867cbf3 | |||
db598af28a | |||
ec765c9070 | |||
5eaa754401 | |||
b48b5589f4 | |||
634ef16bc5 | |||
ccf0bdaf05 | |||
4647da7803 | |||
613f271c4e | |||
8b1529f240 | |||
3eb09033bf | |||
12619f6bde | |||
f5ceaa9afe | |||
777ae945e0 | |||
3eae8fb58b | |||
b14ef5cd78 | |||
98bf4f3bdc | |||
2d6cf50ca7 | |||
95baf9fb9c | |||
dd0972b528 | |||
d17f6297d3 | |||
3ae785a45c | |||
dd254d4bec | |||
e04bd75f1f | |||
929ccb53db | |||
72319a4613 | |||
e389e6c073 | |||
cd6951dcbb | |||
02d60754b6 | |||
6884251646 | |||
582e2059d8 | |||
ea2974bfae | |||
b8ff649c96 | |||
8661ecdafb | |||
fe8cbc061d | |||
b4459e1fd4 | |||
fd6553871f | |||
a4ca44e1ce | |||
e124c429d1 | |||
e9a2dae1e4 | |||
8b0f3490e3 | |||
131606a6cf | |||
cacafa205e | |||
9c620de1e7 | |||
3e98fb967b | |||
8db81478f3 | |||
8f9861bac6 | |||
5b35e3500e | |||
fc4c297bef | |||
e7cb699bcf | |||
5301b4efad | |||
bf595dd09c | |||
cb4b168b2a | |||
b2fcbb8289 |
BIN
.github/readme-banner.png
vendored
Normal file
BIN
.github/readme-banner.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
2
.github/utils/.gitignore
vendored
Normal file
2
.github/utils/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.env
|
||||
__pycache__/
|
57
.github/utils/_get_password.py
vendored
Normal file
57
.github/utils/_get_password.py
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
import base64
|
||||
import secrets
|
||||
from hashlib import sha256
|
||||
from typing import Tuple
|
||||
|
||||
import mysql.connector as mysql
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
|
||||
def get_password(
|
||||
version_name: str,
|
||||
version_code: int,
|
||||
db_host: str,
|
||||
db_user: str,
|
||||
db_pass: str,
|
||||
db_name: str,
|
||||
) -> Tuple[str, bytes]:
|
||||
db = mysql.connect(
|
||||
host=db_host,
|
||||
user=db_user,
|
||||
password=db_pass,
|
||||
database=db_name,
|
||||
auth_plugin="mysql_native_password",
|
||||
)
|
||||
|
||||
print(f"Generating passwords for version {version_name} ({version_code})")
|
||||
|
||||
password = base64.b64encode(secrets.token_bytes(16)).decode()
|
||||
iv = secrets.token_bytes(16)
|
||||
|
||||
key = f"{version_name}.{password}.{version_code}"
|
||||
key = sha256(key.encode()).digest()
|
||||
data = "ThisIsOurHardWorkPleaseDoNotCopyOrSteal(c)2019.KubaSz"
|
||||
data = sha256(data.encode()).digest()
|
||||
data = data + (chr(16) * 16).encode()
|
||||
|
||||
aes = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
|
||||
|
||||
app_password = base64.b64encode(aes.encrypt(data)).decode()
|
||||
|
||||
c = db.cursor()
|
||||
c.execute(
|
||||
"INSERT IGNORE INTO _appPasswords (versionCode, appPassword, password, iv) VALUES (%s, %s, %s, %s);",
|
||||
(version_code, app_password, password, iv),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
c = db.cursor()
|
||||
c.execute(
|
||||
"SELECT password, iv FROM _appPasswords WHERE versionCode = %s;",
|
||||
(version_code,),
|
||||
)
|
||||
row = c.fetchone()
|
||||
|
||||
db.close()
|
||||
|
||||
return (row[0], row[1])
|
142
.github/utils/_utils.py
vendored
Normal file
142
.github/utils/_utils.py
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
VERSION_NAME_REGEX = r'versionName: "(.+?)"'
|
||||
VERSION_CODE_REGEX = r"versionCode: ([0-9]+)"
|
||||
VERSION_NAME_FORMAT = 'versionName: "{}"'
|
||||
VERSION_CODE_FORMAT = "versionCode: {}"
|
||||
|
||||
|
||||
def get_project_dir() -> str:
|
||||
project_dir = sys.argv[1]
|
||||
if project_dir[-1:] == "/" or project_dir[-1:] == "\\":
|
||||
project_dir = project_dir[:-1]
|
||||
return project_dir
|
||||
|
||||
|
||||
def read_gradle_version(project_dir: str) -> Tuple[int, str]:
|
||||
GRADLE_PATH = f"{project_dir}/build.gradle"
|
||||
|
||||
with open(GRADLE_PATH, "r") as f:
|
||||
gradle = f.read()
|
||||
|
||||
version_name = re.search(VERSION_NAME_REGEX, gradle).group(1)
|
||||
version_code = int(re.search(VERSION_CODE_REGEX, gradle).group(1))
|
||||
|
||||
return (version_code, version_name)
|
||||
|
||||
|
||||
def write_gradle_version(project_dir: str, version_code: int, version_name: str):
|
||||
GRADLE_PATH = f"{project_dir}/build.gradle"
|
||||
|
||||
with open(GRADLE_PATH, "r") as f:
|
||||
gradle = f.read()
|
||||
|
||||
gradle = re.sub(
|
||||
VERSION_NAME_REGEX, VERSION_NAME_FORMAT.format(version_name), gradle
|
||||
)
|
||||
gradle = re.sub(
|
||||
VERSION_CODE_REGEX, VERSION_CODE_FORMAT.format(version_code), gradle
|
||||
)
|
||||
|
||||
with open(GRADLE_PATH, "w") as f:
|
||||
f.write(gradle)
|
||||
|
||||
|
||||
def build_version_code(version_name: str) -> int:
|
||||
version = version_name.split("+")[0].split("-")
|
||||
version_base = version[0]
|
||||
version_suffix = version[1] if len(version) == 2 else ""
|
||||
|
||||
base_parts = version_base.split(".")
|
||||
major = int(base_parts[0]) or 0
|
||||
minor = int(base_parts[1]) if len(base_parts) > 1 else 0
|
||||
patch = int(base_parts[2]) if len(base_parts) > 2 else 0
|
||||
|
||||
beta = 9
|
||||
rc = 9
|
||||
if "dev" in version_suffix:
|
||||
beta = 0
|
||||
rc = 0
|
||||
elif "beta." in version_suffix:
|
||||
beta = int(version_suffix.split(".")[1])
|
||||
rc = 0
|
||||
elif "rc." in version_suffix:
|
||||
beta = 0
|
||||
rc = int(version_suffix.split(".")[1])
|
||||
|
||||
version_code = beta + rc * 10 + patch * 100 + minor * 10000 + major * 1000000
|
||||
return version_code
|
||||
|
||||
|
||||
def get_changelog(project_dir: str, format: str) -> Tuple[str, str]:
|
||||
with open(
|
||||
f"{project_dir}/app/src/main/assets/pl-changelog.html", "r", encoding="utf-8"
|
||||
) as f:
|
||||
changelog = f.read()
|
||||
|
||||
title = re.search(r"<h3>(.+?)</h3>", changelog).group(1)
|
||||
content = re.search(r"(?s)<ul>(.+)</ul>", changelog).group(1).strip()
|
||||
content = "\n".join(line.strip() for line in content.split("\n"))
|
||||
|
||||
if format != "html":
|
||||
content = content.replace("<li>", "- ")
|
||||
content = content.replace("<br>", "\n")
|
||||
if format == "markdown":
|
||||
content = re.sub(r"<u>(.+?)</u>", "__\\1__", content)
|
||||
content = re.sub(r"<i>(.+?)</i>", "*\\1*", content)
|
||||
content = re.sub(r"<b>(.+?)</b>", "**\\1**", content)
|
||||
content = re.sub(r"</?.+?>", "", content)
|
||||
|
||||
return (title, content)
|
||||
|
||||
|
||||
def get_commit_log(project_dir: str, format: str, max_lines: int = None) -> str:
|
||||
last_tag = (
|
||||
subprocess.check_output("git describe --tags --abbrev=0".split(" "))
|
||||
.decode()
|
||||
.strip()
|
||||
)
|
||||
|
||||
log = subprocess.run(
|
||||
args=f"git log {last_tag}..HEAD --format=%an%x00%at%x00%h%x00%s%x00%D".split(" "),
|
||||
cwd=project_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
log = log.stdout.strip().decode()
|
||||
|
||||
commits = [line.split("\x00") for line in log.split("\n")]
|
||||
if max_lines:
|
||||
commits = commits[:max_lines]
|
||||
|
||||
output = []
|
||||
valid = False
|
||||
|
||||
for commit in commits:
|
||||
if not commit[0]:
|
||||
continue
|
||||
if "origin/" in commit[4]:
|
||||
valid = True
|
||||
if not valid:
|
||||
continue
|
||||
date = datetime.fromtimestamp(float(commit[1]))
|
||||
date = date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if format == "html":
|
||||
output.append(f"<li>{commit[3]} <i> - {commit[0]}</i></li>")
|
||||
elif format == "markdown":
|
||||
output.append(f"[{date}] {commit[0]}\n {commit[3]}")
|
||||
elif format == "markdown_full":
|
||||
output.append(
|
||||
f"_[{date}] {commit[0]}_\n` `__`{commit[2]}`__ **{commit[3]}**"
|
||||
)
|
||||
elif format == "plain":
|
||||
output.append(f"- {commit[3]}")
|
||||
|
||||
if format == "markdown":
|
||||
output.insert(0, "```")
|
||||
output.append("```")
|
||||
|
||||
return "\n".join(output)
|
69
.github/utils/bump_nightly.py
vendored
Normal file
69
.github/utils/bump_nightly.py
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from _utils import (
|
||||
get_commit_log,
|
||||
get_project_dir,
|
||||
read_gradle_version,
|
||||
write_gradle_version,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: bump_nightly.py <project dir>")
|
||||
exit(-1)
|
||||
|
||||
repo = os.getenv("GITHUB_REPOSITORY")
|
||||
sha = os.getenv("GITHUB_SHA")
|
||||
|
||||
if not repo or not sha:
|
||||
print("Missing GitHub environment variables.")
|
||||
exit(-1)
|
||||
|
||||
with requests.get(
|
||||
f"https://api.github.com/repos/{repo}/actions/runs?per_page=5&status=success"
|
||||
) as r:
|
||||
data = json.loads(r.text)
|
||||
runs = [run for run in data["workflow_runs"] if run["head_sha"] == sha]
|
||||
if runs:
|
||||
print("::set-output name=hasNewChanges::false")
|
||||
exit(0)
|
||||
|
||||
print("::set-output name=hasNewChanges::true")
|
||||
|
||||
project_dir = get_project_dir()
|
||||
|
||||
(version_code, version_name) = read_gradle_version(project_dir)
|
||||
version_name = version_name.split("+")[0]
|
||||
|
||||
date = datetime.now()
|
||||
if date.hour > 6:
|
||||
version_name += "+daily." + date.strftime("%Y%m%d-%H%M")
|
||||
else:
|
||||
date -= timedelta(days=1)
|
||||
version_name += "+nightly." + date.strftime("%Y%m%d")
|
||||
|
||||
print("::set-output name=appVersionName::" + version_name)
|
||||
print("::set-output name=appVersionCode::" + str(version_code))
|
||||
|
||||
write_gradle_version(project_dir, version_code, version_name)
|
||||
|
||||
commit_log = get_commit_log(project_dir, format="html", max_lines=10)
|
||||
|
||||
with open(
|
||||
f"{project_dir}/app/src/main/assets/pl-changelog.html", "r", encoding="utf-8"
|
||||
) as f:
|
||||
changelog = f.read()
|
||||
|
||||
changelog = re.sub(r"<h3>(.+?)</h3>", f"<h3>{version_name}</h3>", changelog)
|
||||
changelog = re.sub(r"(?s)<ul>(.+)</ul>", f"<ul>\n{commit_log}\n</ul>", changelog)
|
||||
|
||||
with open(
|
||||
f"{project_dir}/app/src/main/assets/pl-changelog.html", "w", encoding="utf-8"
|
||||
) as f:
|
||||
f.write(changelog)
|
41
.github/utils/bump_version.py
vendored
Normal file
41
.github/utils/bump_version.py
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from _get_password import get_password
|
||||
from _utils import build_version_code, write_gradle_version
|
||||
from sign import sign
|
||||
|
||||
if __name__ == "__main__":
|
||||
version_name = input("Enter version name: ")
|
||||
version_code = build_version_code(version_name)
|
||||
|
||||
print(f"Bumping version to {version_name} ({version_code})")
|
||||
|
||||
project_dir = "../.."
|
||||
|
||||
load_dotenv()
|
||||
DB_HOST = os.getenv("DB_HOST")
|
||||
DB_USER = os.getenv("DB_USER")
|
||||
DB_PASS = os.getenv("DB_PASS")
|
||||
DB_NAME = os.getenv("DB_NAME")
|
||||
|
||||
write_gradle_version(project_dir, version_code, version_name)
|
||||
(password, iv) = get_password(
|
||||
version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME
|
||||
)
|
||||
|
||||
sign(project_dir, version_name, version_code, password, iv, commit=False)
|
||||
|
||||
print("Writing mock passwords")
|
||||
os.chdir(project_dir)
|
||||
os.system(
|
||||
"sed -i -E 's/\/\*([0-9a-f]{2} ?){16}\*\//\/*secret password - removed for source code publication*\//g' app/src/main/cpp/szkolny-signing.cpp"
|
||||
)
|
||||
os.system(
|
||||
"sed -i -E 's/\\t0x.., 0x(.)., 0x.(.), 0x.(.), 0x.., 0x.., 0x.., 0x.(.), 0x.., 0x.(.), 0x(.)., 0x(.)., 0x.., 0x.., 0x.., 0x.(.)/\\t0x\\3\\6, 0x\\7\\4, 0x\\1\\8, 0x\\2\\5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff /g' app/src/main/cpp/szkolny-signing.cpp"
|
||||
)
|
||||
os.system(
|
||||
"sed -i -E 's/param1\..(.).(.).(.).(.)..(.)..(.)..(.)..(.).../param1.MTIzNDU2Nzg5MD\\5\\2\\7\\6\\1\\3\\4\8==/g' app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt"
|
||||
)
|
||||
input("Press any key to finish")
|
72
.github/utils/extract_changelogs.py
vendored
Normal file
72
.github/utils/extract_changelogs.py
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_version
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: extract_changelogs.py <project dir>")
|
||||
exit(-1)
|
||||
|
||||
project_dir = get_project_dir()
|
||||
|
||||
(version_code, version_name) = read_gradle_version(project_dir)
|
||||
|
||||
print("::set-output name=appVersionName::" + version_name)
|
||||
print("::set-output name=appVersionCode::" + str(version_code))
|
||||
|
||||
dir = f"{project_dir}/app/release/whatsnew-{version_name}/"
|
||||
os.makedirs(dir, exist_ok=True)
|
||||
|
||||
print("::set-output name=changelogDir::" + dir)
|
||||
|
||||
(title, changelog) = get_changelog(project_dir, format="plain")
|
||||
|
||||
# plain text changelog - Firebase App Distribution
|
||||
with open(dir + "whatsnew-titled.txt", "w", encoding="utf-8") as f:
|
||||
f.write(title)
|
||||
f.write("\n")
|
||||
f.write(changelog)
|
||||
print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew-titled.txt")
|
||||
|
||||
print("::set-output name=changelogTitle::" + title)
|
||||
|
||||
# plain text changelog, max 500 chars - Google Play
|
||||
with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f:
|
||||
changelog_lines = changelog.split("\n")
|
||||
changelog = ""
|
||||
for line in changelog_lines:
|
||||
if len(changelog) + len(line) < 500:
|
||||
changelog += "\n" + line
|
||||
changelog = changelog.strip()
|
||||
f.write(changelog)
|
||||
|
||||
print("::set-output name=changelogPlainFile::" + dir + "whatsnew-pl-PL")
|
||||
|
||||
# markdown changelog - Discord webhook
|
||||
(_, changelog) = get_changelog(project_dir, format="markdown")
|
||||
with open(dir + "whatsnew.md", "w", encoding="utf-8") as f:
|
||||
f.write(changelog)
|
||||
print("::set-output name=changelogMarkdownFile::" + dir + "whatsnew.md")
|
||||
|
||||
# html changelog - version info in DB
|
||||
(_, changelog) = get_changelog(project_dir, format="html")
|
||||
with open(dir + "whatsnew.html", "w", encoding="utf-8") as f:
|
||||
f.write(changelog)
|
||||
print("::set-output name=changelogHtmlFile::" + dir + "whatsnew.html")
|
||||
|
||||
|
||||
changelog = get_commit_log(project_dir, format="plain", max_lines=10)
|
||||
with open(dir + "commit_log.txt", "w", encoding="utf-8") as f:
|
||||
f.write(changelog)
|
||||
print("::set-output name=commitLogPlainFile::" + dir + "commit_log.txt")
|
||||
|
||||
changelog = get_commit_log(project_dir, format="markdown", max_lines=10)
|
||||
with open(dir + "commit_log.md", "w", encoding="utf-8") as f:
|
||||
f.write(changelog)
|
||||
print("::set-output name=commitLogMarkdownFile::" + dir + "commit_log.md")
|
||||
|
||||
changelog = get_commit_log(project_dir, format="html", max_lines=10)
|
||||
with open(dir + "commit_log.html", "w", encoding="utf-8") as f:
|
||||
f.write(changelog)
|
||||
print("::set-output name=commitLogHtmlFile::" + dir + "commit_log.html")
|
26
.github/utils/rename_artifacts.py
vendored
Normal file
26
.github/utils/rename_artifacts.py
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
from _utils import get_project_dir
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: rename_artifacts.py <project dir>")
|
||||
exit(-1)
|
||||
|
||||
project_dir = get_project_dir()
|
||||
|
||||
files = glob.glob(f"{project_dir}/app/release/*.*")
|
||||
for file in files:
|
||||
file_relative = file.replace(os.getenv("GITHUB_WORKSPACE") + "/", "")
|
||||
if "-aligned.apk" in file:
|
||||
os.unlink(file)
|
||||
elif "-signed.apk" in file:
|
||||
new_file = file.replace("-signed.apk", ".apk")
|
||||
if os.path.isfile(new_file):
|
||||
os.unlink(new_file)
|
||||
os.rename(file, new_file)
|
||||
elif ".apk" in file or ".aab" in file:
|
||||
print("::set-output name=signedReleaseFile::" + file)
|
||||
print("::set-output name=signedReleaseFileRelative::" + file_relative)
|
122
.github/utils/save_version.py
vendored
Normal file
122
.github/utils/save_version.py
vendored
Normal file
@ -0,0 +1,122 @@
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
|
||||
import mysql.connector as mysql
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_version
|
||||
|
||||
|
||||
def save_version(
|
||||
project_dir: str,
|
||||
db_host: str,
|
||||
db_user: str,
|
||||
db_pass: str,
|
||||
db_name: str,
|
||||
apk_server_release: str,
|
||||
apk_server_nightly: str,
|
||||
):
|
||||
db = mysql.connect(
|
||||
host=db_host,
|
||||
user=db_user,
|
||||
password=db_pass,
|
||||
database=db_name,
|
||||
auth_plugin="mysql_native_password",
|
||||
)
|
||||
|
||||
(version_code, version_name) = read_gradle_version(project_dir)
|
||||
(_, changelog) = get_changelog(project_dir, format="html")
|
||||
|
||||
types = ["dev", "beta", "nightly", "daily", "rc", "release"]
|
||||
build_type = [x for x in types if x in version_name]
|
||||
build_type = build_type[0] if build_type else "release"
|
||||
|
||||
if "+nightly." in version_name or "+daily." in version_name:
|
||||
changelog = get_commit_log(project_dir, format="html")
|
||||
build_type = "nightly"
|
||||
elif "-dev" in version_name:
|
||||
build_type = "dev"
|
||||
elif "-beta." in version_name:
|
||||
build_type = "beta"
|
||||
elif "-rc." in version_name:
|
||||
build_type = "rc"
|
||||
|
||||
build_date = int(time())
|
||||
apk_name = None
|
||||
bundle_name_play = None
|
||||
|
||||
files = glob.glob(f"{project_dir}/app/release/*.*")
|
||||
output_apk = f"Edziennik_{version_name}_official.apk"
|
||||
output_aab_play = f"Edziennik_{version_name}_play.aab"
|
||||
for file in files:
|
||||
if output_apk in file:
|
||||
build_date = int(os.stat(file).st_mtime)
|
||||
apk_name = output_apk
|
||||
if output_aab_play in file:
|
||||
build_date = int(os.stat(file).st_mtime)
|
||||
bundle_name_play = output_aab_play
|
||||
|
||||
build_date = datetime.fromtimestamp(build_date).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if build_type in ["nightly", "daily"]:
|
||||
download_url = apk_server_nightly + apk_name if apk_name else None
|
||||
else:
|
||||
download_url = apk_server_release + apk_name if apk_name else None
|
||||
|
||||
cols = [
|
||||
"versionCode",
|
||||
"versionName",
|
||||
"releaseDate",
|
||||
"releaseNotes",
|
||||
"releaseType",
|
||||
"downloadUrl",
|
||||
"apkName",
|
||||
"bundleNamePlay",
|
||||
]
|
||||
updated = {
|
||||
"versionCode": version_code,
|
||||
"downloadUrl": download_url,
|
||||
"apkName": apk_name,
|
||||
"bundleNamePlay": bundle_name_play,
|
||||
}
|
||||
|
||||
values = [
|
||||
version_code,
|
||||
version_name,
|
||||
build_date,
|
||||
changelog,
|
||||
build_type,
|
||||
download_url,
|
||||
apk_name,
|
||||
bundle_name_play,
|
||||
]
|
||||
values.extend(val for val in updated.values() if val)
|
||||
|
||||
updated = ", ".join(f"{col} = %s" for (col, val) in updated.items() if val)
|
||||
|
||||
sql = f"INSERT INTO updates ({', '.join(cols)}) VALUES ({'%s, ' * (len(cols) - 1)}%s) ON DUPLICATE KEY UPDATE {updated};"
|
||||
|
||||
c = db.cursor()
|
||||
c.execute(sql, tuple(values))
|
||||
db.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: save_version.py <project dir>")
|
||||
exit(-1)
|
||||
|
||||
project_dir = get_project_dir()
|
||||
|
||||
load_dotenv()
|
||||
DB_HOST = os.getenv("DB_HOST")
|
||||
DB_USER = os.getenv("DB_USER")
|
||||
DB_PASS = os.getenv("DB_PASS")
|
||||
DB_NAME = os.getenv("DB_NAME")
|
||||
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
|
||||
APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY")
|
||||
|
||||
save_version(project_dir, DB_HOST, DB_USER, DB_PASS, DB_NAME, APK_SERVER_RELEASE, APK_SERVER_NIGHTLY)
|
84
.github/utils/sign.py
vendored
Normal file
84
.github/utils/sign.py
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from _get_password import get_password
|
||||
from _utils import get_project_dir, read_gradle_version
|
||||
|
||||
|
||||
def sign(
|
||||
project_dir: str,
|
||||
version_name: str,
|
||||
version_code: int,
|
||||
password: str,
|
||||
iv: bytes,
|
||||
commit: bool = False,
|
||||
):
|
||||
SIGNING_PATH = f"{project_dir}/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt"
|
||||
CPP_PATH = f"{project_dir}/app/src/main/cpp/szkolny-signing.cpp"
|
||||
|
||||
with open(SIGNING_PATH, "r") as f:
|
||||
signing = f.read()
|
||||
|
||||
with open(CPP_PATH, "r") as f:
|
||||
cpp = f.read()
|
||||
|
||||
SIGNING_REGEX = r"\$param1\..*\.\$param2"
|
||||
CPP_REGEX = r"(?s)/\*.+?toys AES_IV\[16\] = {.+?};"
|
||||
|
||||
SIGNING_FORMAT = "$param1.{}.$param2"
|
||||
CPP_FORMAT = "/*{}*/\nstatic toys AES_IV[16] = {{\n\t{} }};"
|
||||
|
||||
print(f"Writing passwords for version {version_name} ({version_code})")
|
||||
|
||||
iv_hex = " ".join(["{:02x}".format(x) for x in iv])
|
||||
iv_cpp = ", ".join(["0x{:02x}".format(x) for x in iv])
|
||||
|
||||
signing = re.sub(SIGNING_REGEX, SIGNING_FORMAT.format(password), signing)
|
||||
cpp = re.sub(CPP_REGEX, CPP_FORMAT.format(iv_hex, iv_cpp), cpp)
|
||||
|
||||
with open(SIGNING_PATH, "w") as f:
|
||||
f.write(signing)
|
||||
|
||||
with open(CPP_PATH, "w") as f:
|
||||
f.write(cpp)
|
||||
|
||||
if commit:
|
||||
os.chdir(project_dir)
|
||||
os.system("git add .")
|
||||
os.system(
|
||||
f'git commit -m "[{version_name}] Update build.gradle, signing and changelog."'
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: sign.py <project dir> [commit]")
|
||||
exit(-1)
|
||||
|
||||
project_dir = get_project_dir()
|
||||
|
||||
load_dotenv()
|
||||
DB_HOST = os.getenv("DB_HOST")
|
||||
DB_USER = os.getenv("DB_USER")
|
||||
DB_PASS = os.getenv("DB_PASS")
|
||||
DB_NAME = os.getenv("DB_NAME")
|
||||
|
||||
(version_code, version_name) = read_gradle_version(project_dir)
|
||||
(password, iv) = get_password(
|
||||
version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME
|
||||
)
|
||||
|
||||
print("::set-output name=appVersionName::" + version_name)
|
||||
print("::set-output name=appVersionCode::" + str(version_code))
|
||||
|
||||
sign(
|
||||
project_dir,
|
||||
version_name,
|
||||
version_code,
|
||||
password,
|
||||
iv,
|
||||
commit="commit" in sys.argv,
|
||||
)
|
118
.github/utils/webhook_discord.py
vendored
Normal file
118
.github/utils/webhook_discord.py
vendored
Normal file
@ -0,0 +1,118 @@
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_version
|
||||
|
||||
|
||||
def post_webhook(
|
||||
project_dir: str,
|
||||
apk_file: str,
|
||||
apk_server_release: str,
|
||||
apk_server_nightly: str,
|
||||
webhook_release: str,
|
||||
webhook_testing: str,
|
||||
):
|
||||
(_, version_name) = read_gradle_version(project_dir)
|
||||
|
||||
types = ["dev", "beta", "nightly", "daily", "rc", "release"]
|
||||
build_type = [x for x in types if x in version_name]
|
||||
build_type = build_type[0] if build_type else None
|
||||
|
||||
testing = ["dev", "beta", "nightly", "daily"]
|
||||
testing = build_type in testing
|
||||
|
||||
apk_name = os.path.basename(apk_file)
|
||||
if build_type in ["nightly", "daily"]:
|
||||
download_url = apk_server_nightly + apk_name
|
||||
else:
|
||||
download_url = apk_server_release + apk_name
|
||||
|
||||
if testing:
|
||||
build_date = int(os.stat(apk_file).st_mtime)
|
||||
if build_date:
|
||||
build_date = datetime.fromtimestamp(build_date).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# untagged release, get commit log
|
||||
if build_type in ["nightly", "daily"]:
|
||||
changelog = get_commit_log(project_dir, format="markdown", max_lines=5)
|
||||
else:
|
||||
changelog = get_changelog(project_dir, format="markdown")
|
||||
|
||||
webhook = get_webhook_testing(
|
||||
version_name, build_type, changelog, download_url, build_date
|
||||
)
|
||||
requests.post(url=webhook_testing, json=webhook)
|
||||
else:
|
||||
changelog = get_changelog(project_dir, format="markdown")
|
||||
webhook = get_webhook_release(changelog, download_url)
|
||||
requests.post(url=webhook_release, json=webhook)
|
||||
|
||||
|
||||
def get_webhook_release(changelog: str, download_url: str):
|
||||
(title, content) = changelog
|
||||
return {"content": f"__**{title}**__\n{content}\n{download_url}"}
|
||||
|
||||
|
||||
def get_webhook_testing(
|
||||
version_name: str,
|
||||
build_type: str,
|
||||
changelog: str,
|
||||
download_url: str,
|
||||
build_date: str,
|
||||
):
|
||||
return {
|
||||
"embeds": [
|
||||
{
|
||||
"title": f"Nowa wersja {build_type} aplikacji Szkolny.eu",
|
||||
"description": f"Dostępna jest nowa wersja testowa **{build_type}**.",
|
||||
"color": 2201331,
|
||||
"fields": [
|
||||
{
|
||||
"name": f"Wersja `{version_name}`",
|
||||
"value": f"[Pobierz .APK]({download_url})"
|
||||
if download_url
|
||||
else "*Pobieranie niedostępne*",
|
||||
"inline": False,
|
||||
},
|
||||
{
|
||||
"name": "Data kompilacji",
|
||||
"value": build_date or "-",
|
||||
"inline": False,
|
||||
},
|
||||
{
|
||||
"name": "Ostatnie zmiany",
|
||||
"value": changelog or "-",
|
||||
"inline": False,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: webhook_discord.py <project dir>")
|
||||
exit(-1)
|
||||
|
||||
project_dir = get_project_dir()
|
||||
|
||||
load_dotenv()
|
||||
APK_FILE = os.getenv("APK_FILE")
|
||||
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
|
||||
APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY")
|
||||
WEBHOOK_RELEASE = os.getenv("WEBHOOK_RELEASE")
|
||||
WEBHOOK_TESTING = os.getenv("WEBHOOK_TESTING")
|
||||
|
||||
post_webhook(
|
||||
project_dir,
|
||||
APK_FILE,
|
||||
APK_SERVER_RELEASE,
|
||||
APK_SERVER_NIGHTLY,
|
||||
WEBHOOK_RELEASE,
|
||||
WEBHOOK_TESTING,
|
||||
)
|
153
.github/workflows/build-nightly-apk.yml
vendored
Normal file
153
.github/workflows/build-nightly-apk.yml
vendored
Normal file
@ -0,0 +1,153 @@
|
||||
name: Nightly build
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 23:30 UTC, 0:30 or 1:30 CET/CEST
|
||||
- cron: "30 23 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare build environment
|
||||
runs-on: self-hosted
|
||||
outputs:
|
||||
hasNewChanges: ${{ steps.nightly.outputs.hasNewChanges }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- name: Set executable permissions to gradlew
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install packages
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: |
|
||||
python-dotenv
|
||||
pycryptodome
|
||||
mysql-connector-python
|
||||
requests
|
||||
- name: Bump nightly version
|
||||
id: nightly
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/bump_nightly.py $GITHUB_WORKSPACE
|
||||
- name: Write signing passwords
|
||||
if: steps.nightly.outputs.hasNewChanges
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
|
||||
build:
|
||||
name: Build APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- prepare
|
||||
if: ${{ needs.prepare.outputs.hasNewChanges == 'true' }}
|
||||
outputs:
|
||||
androidHome: ${{ env.ANDROID_HOME }}
|
||||
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
|
||||
steps:
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
cache: 'gradle'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
rm -rf app/release/*
|
||||
rm -rf app/build/outputs/apk/*
|
||||
rm -rf app/build/outputs/bundle/*
|
||||
- name: Assemble official release with Gradle
|
||||
run: ./gradlew assembleOfficialRelease
|
||||
sign:
|
||||
name: Sign APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
outputs:
|
||||
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
|
||||
steps:
|
||||
- name: Sign build artifacts
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||
env:
|
||||
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
|
||||
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
- name: Rename signed artifacts
|
||||
id: artifacts
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
|
||||
publish:
|
||||
name: Publish APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- sign
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Extract changelogs
|
||||
id: changelog
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload APK to SFTP
|
||||
uses: easingthemes/ssh-deploy@v2.1.6
|
||||
env:
|
||||
REMOTE_HOST: ${{ secrets.SSH_IP }}
|
||||
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
|
||||
SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }}
|
||||
TARGET: ${{ secrets.SSH_PATH_NIGHTLY }}
|
||||
- name: Save version metadata
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Distribute to App Distribution
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
token: ${{ secrets.FIREBASE_TOKEN }}
|
||||
groups: ${{ secrets.FIREBASE_GROUPS_NIGHTLY }}
|
||||
file: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
releaseNotesFile: ${{ steps.changelog.outputs.commitLogPlainFile }}
|
||||
|
||||
- name: Post Discord webhook
|
||||
env:
|
||||
APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
|
||||
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
if: true
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.appVersionName }}
|
||||
path: |
|
||||
app/release/whatsnew*/
|
||||
app/release/*.apk
|
||||
app/release/*.aab
|
||||
app/release/*.json
|
||||
app/release/*.txt
|
130
.github/workflows/build-release-aab-play.yml
vendored
Normal file
130
.github/workflows/build-release-aab-play.yml
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
name: Release build - Google Play [AAB]
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare build environment
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- name: Set executable permissions to gradlew
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install packages
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: |
|
||||
python-dotenv
|
||||
pycryptodome
|
||||
mysql-connector-python
|
||||
requests
|
||||
- name: Write signing passwords
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
|
||||
build:
|
||||
name: Build App Bundle
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- prepare
|
||||
outputs:
|
||||
androidHome: ${{ env.ANDROID_HOME }}
|
||||
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
|
||||
steps:
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
cache: 'gradle'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
rm -rf app/release/*
|
||||
rm -rf app/build/outputs/apk/*
|
||||
rm -rf app/build/outputs/bundle/*
|
||||
- name: Bundle play release with Gradle
|
||||
run: ./gradlew bundlePlayRelease
|
||||
sign:
|
||||
name: Sign App Bundle
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
outputs:
|
||||
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
|
||||
steps:
|
||||
- name: Sign build artifacts
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||
env:
|
||||
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
|
||||
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
- name: Rename signed artifacts
|
||||
id: artifacts
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
|
||||
publish:
|
||||
name: Publish App Bundle
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- sign
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Extract changelogs
|
||||
id: changelog
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Save version metadata
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Publish AAB to Google Play
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
if: ${{ endsWith(needs.sign.outputs.signedReleaseFile, '.aab') }}
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
packageName: pl.szczodrzynski.edziennik
|
||||
releaseFile: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
releaseName: ${{ steps.changelog.outputs.appVersionName }}
|
||||
track: ${{ secrets.PLAY_RELEASE_TRACK }}
|
||||
whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }}
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.appVersionName }}
|
||||
path: |
|
||||
app/release/whatsnew*/
|
||||
app/release/*.apk
|
||||
app/release/*.aab
|
||||
app/release/*.json
|
||||
app/release/*.txt
|
153
.github/workflows/build-release-apk.yml
vendored
Normal file
153
.github/workflows/build-release-apk.yml
vendored
Normal file
@ -0,0 +1,153 @@
|
||||
name: Release build - official
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare build environment
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- name: Set executable permissions to gradlew
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install packages
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: |
|
||||
python-dotenv
|
||||
pycryptodome
|
||||
mysql-connector-python
|
||||
requests
|
||||
- name: Write signing passwords
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
|
||||
build:
|
||||
name: Build APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- prepare
|
||||
outputs:
|
||||
androidHome: ${{ env.ANDROID_HOME }}
|
||||
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
|
||||
steps:
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
cache: 'gradle'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
rm -rf app/release/*
|
||||
rm -rf app/build/outputs/apk/*
|
||||
rm -rf app/build/outputs/bundle/*
|
||||
- name: Assemble official release with Gradle
|
||||
run: ./gradlew assembleOfficialRelease
|
||||
sign:
|
||||
name: Sign APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
outputs:
|
||||
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
|
||||
steps:
|
||||
- name: Sign build artifacts
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||
env:
|
||||
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
|
||||
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
- name: Rename signed artifacts
|
||||
id: artifacts
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
|
||||
publish:
|
||||
name: Publish APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- sign
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Extract changelogs
|
||||
id: changelog
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload APK to SFTP
|
||||
uses: easingthemes/ssh-deploy@v2.1.6
|
||||
env:
|
||||
REMOTE_HOST: ${{ secrets.SSH_IP }}
|
||||
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
|
||||
SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }}
|
||||
TARGET: ${{ secrets.SSH_PATH_RELEASE }}
|
||||
- name: Save version metadata
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Distribute to App Distribution
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
token: ${{ secrets.FIREBASE_TOKEN }}
|
||||
groups: ${{ secrets.FIREBASE_GROUPS_RELEASE }}
|
||||
file: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
releaseNotesFile: ${{ steps.changelog.outputs.changelogPlainTitledFile }}
|
||||
- name: Release on GitHub
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.changelogTitle }}
|
||||
body_path: ${{ steps.changelog.outputs.changelogMarkdownFile }}
|
||||
files: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Post Discord webhook
|
||||
env:
|
||||
APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
|
||||
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
if: true
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.appVersionName }}
|
||||
path: |
|
||||
app/release/whatsnew*/
|
||||
app/release/*.apk
|
||||
app/release/*.aab
|
||||
app/release/*.json
|
||||
app/release/*.txt
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -265,3 +265,4 @@ fabric.properties
|
||||
# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,gradle,java,kotlin
|
||||
|
||||
signatures/
|
||||
.idea/*.xml
|
||||
|
9
.idea/codeStyles/Project.xml
generated
9
.idea/codeStyles/Project.xml
generated
@ -15,6 +15,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -25,6 +26,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -36,6 +38,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -46,6 +49,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -56,6 +60,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -66,6 +71,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -76,6 +82,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -87,6 +94,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -98,6 +106,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
|
1
.idea/dictionaries/Kuba.xml
generated
1
.idea/dictionaries/Kuba.xml
generated
@ -5,6 +5,7 @@
|
||||
<w>ciasteczko</w>
|
||||
<w>csrf</w>
|
||||
<w>edziennik</w>
|
||||
<w>elearning</w>
|
||||
<w>gson</w>
|
||||
<w>hebe</w>
|
||||
<w>idziennik</w>
|
||||
|
9
.idea/discord.xml
generated
9
.idea/discord.xml
generated
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
</component>
|
||||
<component name="ProjectNotificationSettings">
|
||||
<option name="askShowProject" value="false" />
|
||||
</component>
|
||||
</project>
|
6
.idea/kotlinc.xml
generated
6
.idea/kotlinc.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Kotlin2JvmCompilerArguments">
|
||||
<option name="jvmTarget" value="1.8" />
|
||||
</component>
|
||||
</project>
|
13
.idea/runConfigurations.xml
generated
13
.idea/runConfigurations.xml
generated
@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
36
README.md
36
README.md
@ -1,9 +1,7 @@
|
||||
# Szkolny.eu
|
||||
|
||||
Nieoficjalna aplikacja do obsługi najpopularniejszych dzienników elektronicznych w Polsce.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
[](https://szkolny.eu/discord)
|
||||
[](https://szkolny.eu/)
|
||||
[](https://szkolny.eu/facebook)
|
||||
@ -12,11 +10,15 @@ Nieoficjalna aplikacja do obsługi najpopularniejszych dzienników elektroniczny
|
||||
[](https://github.com/szkolny-eu/szkolny-android/releases/latest)
|
||||

|
||||
|
||||
[](https://github.com/szkolny-eu/szkolny-android/actions/workflows/build-release-apk.yml)
|
||||
[](https://github.com/szkolny-eu/szkolny-android/actions/workflows/build-release-aab-play.yml)
|
||||
[](https://github.com/szkolny-eu/szkolny-android/actions/workflows/build-nightly-apk.yml)
|
||||
|
||||
</div>
|
||||
|
||||
## Ważna informacja
|
||||
|
||||
Jak zapewne już wiecie, we wrześniu 2020r. **firma Librus zabroniła nam** publikowania w sklepie Google Play naszej aplikacji z obsługą dziennika Librus® Synergia. Prowadziliśmy rozmowy, aby **umożliwić Wam wygodny, bezpłatny dostęp do Waszych ocen, wiadomości, zadań domowych**, jednak oczekiwania firmy Librus zdecydowanie przekroczyły wszelkie nasze możliwości finansowe. Mając na uwadze powyższe względy, zdecydowaliśmy się opublikować kod źródłowy aplikacji Szkolny.eu. Liczymy, że dzięki temu aplikacja będzie mogła dalej funkcjonować, być rozwijana, pomagając Wam w czasie zdalnego nauczania i przez kolejne lata nauki.
|
||||
Jak zapewne już wiecie, we wrześniu 2020 r. **firma Librus zabroniła nam** publikowania w sklepie Google Play naszej aplikacji z obsługą dziennika Librus® Synergia. Prowadziliśmy rozmowy, aby **umożliwić Wam wygodny, bezpłatny dostęp do Waszych ocen, wiadomości, zadań domowych**, jednak oczekiwania firmy Librus zdecydowanie przekroczyły wszelkie nasze możliwości finansowe. Mając na uwadze powyższe względy, zdecydowaliśmy się opublikować kod źródłowy aplikacji Szkolny.eu. Liczymy, że dzięki temu aplikacja będzie mogła dalej funkcjonować, być rozwijana, pomagając Wam w czasie zdalnego nauczania i przez kolejne lata nauki.
|
||||
|
||||
__Zachęcamy do [przeczytania całej informacji](https://szkolny.eu/informacja) na naszej stronie.__
|
||||
|
||||
@ -30,17 +32,17 @@ Szkolny.eu jest nieoficjalną aplikacją, umożliwiającą rodzicom i uczniom do
|
||||
|
||||
- plan lekcji, terminarz, oceny, wiadomości, zadania domowe, uwagi, frekwencja
|
||||
- wygodne **widgety** na ekran główny
|
||||
- łatwa komunikacja z nauczycielami - **odbieranie, wyszukiwanie i wysyłanie wiadomości**
|
||||
- łatwa komunikacja z nauczycielami — **odbieranie, wyszukiwanie i wysyłanie wiadomości**
|
||||
- pobieranie **załączników wiadomości i zadań domowych**
|
||||
- **powiadomienia** o nowych informacjach na telefonie lub na komputerze
|
||||
- organizacja zadań domowych i sprawdzianów - łatwe oznaczanie jako wykonane
|
||||
- organizacja zadań domowych i sprawdzianów — łatwe oznaczanie jako wykonane
|
||||
- obliczanie **średniej ocen** ze wszystkich przedmiotów, oceny proponowane i końcowe
|
||||
- Symulator edycji ocen - obliczanie średniej z przedmiotu po zmianie dowolnych jego ocen
|
||||
- Symulator edycji ocen — obliczanie średniej z przedmiotu po zmianie dowolnych jego ocen
|
||||
- **dodawanie własnych wydarzeń** i zadań do terminarza
|
||||
- nowoczesny i intuicyjny interfejs użytkownika
|
||||
- **obsługa wielu profili** uczniów - jeżeli jesteś Rodzicem, możesz skonfigurować wszystkie swoje konta uczniowskie i łatwo między nimi przełączać
|
||||
- **obsługa wielu profili** uczniów — jeżeli jesteś Rodzicem, możesz skonfigurować wszystkie swoje konta uczniowskie i łatwo między nimi przełączać
|
||||
- opcja **automatycznej synchronizacji** z E-dziennikiem
|
||||
- opcja Ciszy nocnej - nigdy więcej budzących Cię dźwięków z telefonu
|
||||
- opcja Ciszy nocnej — nigdy więcej budzących Cię dźwięków z telefonu
|
||||
|
||||
[Zobacz porównanie funkcji z innymi aplikacjami](https://szkolny.eu/funkcje)
|
||||
|
||||
@ -53,7 +55,7 @@ Najnowsze wersje możesz pobrać z Google Play lub bezpośrednio z naszej strony
|
||||
|
||||
### Kompilacja
|
||||
|
||||
Aby uruchomić aplikację "ze źródeł" należy użyć Android Studio w wersji co najmniej 4.2 Beta 6. Wersja `debug` może wtedy zostać zainstalowana np. na emulatorze Androida.
|
||||
Aby uruchomić aplikację „ze źródeł” należy użyć Android Studio w wersji co najmniej 4.2 Beta 6. Wersja `debug` może wtedy zostać zainstalowana np. na emulatorze Androida.
|
||||
|
||||
Aby zbudować wersję produkcyjną, tzn. `release` należy użyć wariantu `mainRelease` oraz podpisać wyjściowy plik .APK sygnaturą w wersji V1 i V2.
|
||||
|
||||
@ -68,15 +70,15 @@ __Jeśli masz jakieś pytania, zapraszamy na [nasz serwer Discord](https://szkol
|
||||
## Licencja
|
||||
|
||||
Szkolny.eu publikowany jest na licencji [GNU GPLv3](LICENSE). W szczególności, deweloper:
|
||||
- może modyfikować oraz usprawniać kod aplikacji
|
||||
- może dystrybuować wersje produkcyjne
|
||||
- musi opublikować wszelkie wprowadzone zmiany, tzn. publiczny fork tego repozytorium
|
||||
- nie może zmieniać licencji ani copyrightu aplikacji
|
||||
- Może modyfikować oraz usprawniać kod aplikacji
|
||||
- Może dystrybuować wersje produkcyjne
|
||||
- Musi opublikować wszelkie wprowadzone zmiany, tzn. publiczny fork tego repozytorium
|
||||
- Nie może zmieniać licencji ani copyrightu aplikacji
|
||||
|
||||
Dodatkowo:
|
||||
- zabronione jest modyfikowanie lub usuwanie kodu odpowiedzialnego za zgodność wersji produkcyjnych z licencją
|
||||
- Zabronione jest modyfikowanie lub usuwanie kodu odpowiedzialnego za zgodność wersji produkcyjnych z licencją.
|
||||
|
||||
- **wersje skompilowane nie mogą być dystrybuowane za pomocą Google Play oraz żadnej platformy, na której istnieje oficjalna wersja aplikacji**
|
||||
- **Wersje skompilowane nie mogą być dystrybuowane za pomocą Google Play oraz żadnej platformy, na której istnieje oficjalna wersja aplikacji**.
|
||||
|
||||
**Autorzy aplikacji nie biorą odpowiedzialności za używanie aplikacji, modyfikowanie oraz dystrybuowanie.**
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
@ -18,8 +19,10 @@ android {
|
||||
versionName release.versionName
|
||||
|
||||
buildConfigField "java.util.Map<String, String>", "GIT_INFO", gitInfoMap
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
|
||||
buildConfigField "String", "VERSION_BASE", "\"${release.versionName}\""
|
||||
manifestPlaceholders = [
|
||||
buildTimestamp: String.valueOf(System.currentTimeMillis())
|
||||
]
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
@ -33,6 +36,9 @@ android {
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled = false
|
||||
manifestPlaceholders = [
|
||||
buildTimestamp: 0
|
||||
]
|
||||
}
|
||||
release {
|
||||
minifyEnabled = true
|
||||
@ -98,7 +104,10 @@ tasks.whenTaskAdded { task ->
|
||||
|
||||
if (flavor != "") {
|
||||
tasks.create(renameTaskName, Copy) {
|
||||
from file("${projectDir}/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/")
|
||||
from file("${projectDir}/${flavor}/release/"),
|
||||
file("${buildDir}/outputs/mapping/${flavor}Release/"),
|
||||
file("${buildDir}/outputs/apk/${flavor}/release/"),
|
||||
file("${buildDir}/outputs/bundle/${flavor}Release/")
|
||||
include "*.aab", "*.apk", "mapping.txt", "output-metadata.json"
|
||||
destinationDir file("${projectDir}/release/")
|
||||
rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1'
|
||||
@ -115,25 +124,25 @@ dependencies {
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||
|
||||
// Android Jetpack
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.appcompat:appcompat:1.3.1"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:2.3.4"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
implementation "androidx.room:room-runtime:2.2.6"
|
||||
implementation "androidx.work:work-runtime-ktx:2.5.0"
|
||||
kapt "androidx.room:room-compiler:2.2.6"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
|
||||
implementation "androidx.core:core-ktx:1.6.0"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.room:room-runtime:2.3.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.6.0"
|
||||
kapt "androidx.room:room-compiler:2.3.0"
|
||||
|
||||
// Google design libs
|
||||
implementation "com.google.android.material:material:1.3.0"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.android:flexbox:2.0.1"
|
||||
|
||||
// Play Services/Firebase
|
||||
implementation "com.google.android.gms:play-services-wearable:17.0.0"
|
||||
implementation "com.google.firebase:firebase-core:18.0.2"
|
||||
implementation "com.google.firebase:firebase-crashlytics:17.4.0"
|
||||
implementation "com.google.android.gms:play-services-wearable:17.1.0"
|
||||
implementation "com.google.firebase:firebase-core:19.0.1"
|
||||
implementation "com.google.firebase:firebase-crashlytics:18.2.1"
|
||||
implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } }
|
||||
|
||||
// OkHttp, Retrofit, Gson, Jsoup
|
||||
@ -147,7 +156,8 @@ dependencies {
|
||||
implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2"
|
||||
|
||||
// Szkolny.eu libraries/forks
|
||||
implementation "eu.szkolny:agendacalendarview:1799f8ef47"
|
||||
implementation "eu.szkolny:android-snowfall:1ca9ea2da3"
|
||||
implementation "eu.szkolny:agendacalendarview:ac0f3dcf42"
|
||||
implementation "eu.szkolny:cafebar:5bf0c618de"
|
||||
implementation "eu.szkolny.fslogin:lib:2.0.0"
|
||||
implementation "eu.szkolny:material-about-library:1d5ebaf47c"
|
||||
@ -162,10 +172,10 @@ dependencies {
|
||||
kapt "eu.szkolny.selective-dao:codegen:27f8f3f194"
|
||||
|
||||
// Iconics & related
|
||||
implementation "com.mikepenz:iconics-core:5.3.0-b01"
|
||||
implementation "com.mikepenz:iconics-views:5.3.0-b01"
|
||||
implementation "com.mikepenz:iconics-core:5.3.1"
|
||||
implementation "com.mikepenz:iconics-views:5.3.1"
|
||||
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
|
||||
implementation "eu.szkolny:szkolny-font:1.3"
|
||||
implementation "eu.szkolny:szkolny-font:77e33acc2a"
|
||||
|
||||
// Other dependencies
|
||||
implementation "cat.ereza:customactivityoncrash:2.3.0"
|
||||
@ -175,7 +185,6 @@ dependencies {
|
||||
implementation "com.github.bassaer:chatmessageview:2.0.1"
|
||||
implementation "com.github.CanHub:Android-Image-Cropper:2.2.2"
|
||||
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1"
|
||||
implementation "com.github.jetradarmobile:android-snowfall:1.2.0"
|
||||
implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31"
|
||||
implementation("com.heinrichreimersoftware:material-intro") { version { strictly "1.5.8" } }
|
||||
implementation "com.hypertrack:hyperlog:0.0.10"
|
||||
|
@ -84,7 +84,7 @@ private def buildGitInfo() {
|
||||
.stream()
|
||||
.map {
|
||||
it.name + "(" + it.URIs.stream()
|
||||
.map { it.rawPath }
|
||||
.map { it.rawPath.stripMargin('/').replace(".git", "") }
|
||||
.toArray()
|
||||
.join(", ") + ")"
|
||||
}
|
||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -32,8 +32,9 @@
|
||||
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
|
||||
|
||||
-keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); }
|
||||
-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; }
|
||||
-keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); }
|
||||
-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; }
|
||||
-keepclassmembernames class androidx.appcompat.view.menu.MenuItemImpl { private *; }
|
||||
|
||||
-keepclassmembernames class com.mikepenz.materialdrawer.widget.MiniDrawerSliderView { private *; }
|
||||
|
||||
|
@ -29,6 +29,8 @@
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<meta-data android:name="buildTimestamp" android:value="${buildTimestamp}" />
|
||||
|
||||
<!-- __ __ _ _ _ _ _
|
||||
| \/ | (_) /\ | | (_) (_) |
|
||||
| \ / | __ _ _ _ __ / \ ___| |_ ___ ___| |_ _ _
|
||||
@ -144,6 +146,7 @@
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity android:name=".ui.modules.base.BuildInvalidActivity" />
|
||||
<activity android:name=".ui.modules.settings.contributors.ContributorsActivity" />
|
||||
|
||||
<!-- _____ _
|
||||
| __ \ (_)
|
||||
|
@ -1,14 +1,8 @@
|
||||
<h3>Wersja 4.7-rc.1, 2021-04-01</h3>
|
||||
<h3>Wersja 4.10, 2021-09-22</h3>
|
||||
<ul>
|
||||
<li><u>Szkolny.eu jest teraz open source!</u> Zapraszamy na stronę https://szkolny.eu/ po więcej ważnych informacji.</li>
|
||||
<li>Poprawiono wybieranie obrazków (tła nagłówka, tła aplikacji oraz profilu) z dowolnego źródła.</li>
|
||||
<li>Naprawiono zatrzymanie aplikacji na Androidzie 4.4 i starszych.</li>
|
||||
<li>Naprawiono problemy z połączeniem internetowym na Androidzie 4.4 i starszych.</li>
|
||||
<li>Dodano ekran informacji o kompilacji w Ustawieniach.</li>
|
||||
<li>Zaktualizowano ekran licencji open source.</li>
|
||||
<li>Zoptymalizowano wielkość aplikacji.</li>
|
||||
<li>Dodano wyświetlanie informacji o frekwencji w planie lekcji. @Antoni-Czaplicki</li>
|
||||
</ul>
|
||||
<br>
|
||||
<br>
|
||||
Dzięki za korzystanie ze Szkolnego!<br>
|
||||
<i>© Kuba Szczodrzyński, Kacper Ziubryniewicz 2021</i>
|
||||
<i>© [Kuba Szczodrzyński](@kuba2k2), [Kacper Ziubryniewicz](@kapi2289) 2021</i>
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
/*secret password - removed for source code publication*/
|
||||
static toys AES_IV[16] = {
|
||||
0xdd, 0x0a, 0x72, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
|
||||
0xda, 0x2a, 0x5f, 0xbe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
|
||||
|
||||
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);
|
||||
|
||||
|
@ -58,6 +58,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
val profileId
|
||||
get() = profile.id
|
||||
|
||||
var enableChucker = false
|
||||
var debugMode = false
|
||||
var devMode = false
|
||||
}
|
||||
@ -70,6 +71,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
val permissionManager by lazy { PermissionManager(this) }
|
||||
val attendanceManager by lazy { AttendanceManager(this) }
|
||||
val buildManager by lazy { BuildManager(this) }
|
||||
val availabilityManager by lazy { AvailabilityManager(this) }
|
||||
|
||||
val db
|
||||
get() = App.db
|
||||
@ -115,9 +117,11 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
HyperLog.initialize(this)
|
||||
HyperLog.setLogLevel(Log.VERBOSE)
|
||||
HyperLog.setLogFormat(DebugLogFormat(this))
|
||||
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
|
||||
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
|
||||
builder.addInterceptor(chuckerInterceptor)
|
||||
if (enableChucker) {
|
||||
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
|
||||
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
|
||||
builder.addInterceptor(chuckerInterceptor)
|
||||
}
|
||||
}
|
||||
|
||||
http = builder.build()
|
||||
@ -171,7 +175,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
App.config = Config(App.db)
|
||||
App.profile = Profile(0, 0, 0, "")
|
||||
debugMode = BuildConfig.DEBUG
|
||||
devMode = config.debugMode || debugMode
|
||||
devMode = config.devMode ?: debugMode
|
||||
enableChucker = config.enableChucker ?: devMode
|
||||
|
||||
if (!profileLoadById(config.lastProfileId)) {
|
||||
db.profileDao().firstId?.let { profileLoadById(it) }
|
||||
|
@ -14,7 +14,9 @@ import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.*
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
@ -552,28 +554,46 @@ fun CharSequence?.asBoldSpannable(): Spannable {
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
return spannable
|
||||
}
|
||||
fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable {
|
||||
fun CharSequence.asSpannable(
|
||||
vararg spans: CharacterStyle,
|
||||
substring: CharSequence? = null,
|
||||
ignoreCase: Boolean = false,
|
||||
ignoreDiacritics: Boolean = false
|
||||
): Spannable {
|
||||
val spannable = SpannableString(this)
|
||||
if (substring == null) {
|
||||
spans.forEach {
|
||||
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
else if (substring.isNotEmpty()) {
|
||||
val string =
|
||||
if (ignoreDiacritics)
|
||||
this.cleanDiacritics()
|
||||
else this
|
||||
substring?.let { substr ->
|
||||
val string = if (ignoreDiacritics)
|
||||
this.cleanDiacritics()
|
||||
else
|
||||
this
|
||||
val search = if (ignoreDiacritics)
|
||||
substr.cleanDiacritics()
|
||||
else
|
||||
substr.toString()
|
||||
|
||||
var index = string.indexOf(substring, ignoreCase = ignoreCase)
|
||||
.takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase)
|
||||
while (index >= 0) {
|
||||
spans.forEach {
|
||||
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
var index = 0
|
||||
do {
|
||||
index = string.indexOf(
|
||||
string = search,
|
||||
startIndex = index,
|
||||
ignoreCase = ignoreCase
|
||||
)
|
||||
|
||||
if (index >= 0) {
|
||||
spans.forEach {
|
||||
spannable.setSpan(
|
||||
CharacterStyle.wrap(it),
|
||||
index,
|
||||
index + substring.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
index += substring.length.coerceAtLeast(1)
|
||||
}
|
||||
index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
||||
.takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
||||
}
|
||||
} while (index >= 0)
|
||||
|
||||
} ?: spans.forEach {
|
||||
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return spannable
|
||||
}
|
||||
@ -718,6 +738,7 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
|
||||
is Short -> putShort(property.first, property.second as Short)
|
||||
is Double -> putDouble(property.first, property.second as Double)
|
||||
is Boolean -> putBoolean(property.first, property.second as Boolean)
|
||||
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.navigation.NavOptions
|
||||
import com.danimahardhika.cafebar.CafeBar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.jetradarmobile.snowfall.SnowfallView
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
@ -81,12 +82,11 @@ import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
|
||||
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.Utils
|
||||
import pl.szczodrzynski.edziennik.utils.*
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
|
||||
import pl.szczodrzynski.edziennik.utils.appManagerIntentList
|
||||
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager
|
||||
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.NavTarget
|
||||
import pl.szczodrzynski.navlib.*
|
||||
@ -470,9 +470,21 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
||||
|
||||
// IT'S WINTER MY DUDES
|
||||
val today = Date.getToday()
|
||||
if ((today.month == 12 || today.month == 1) && app.config.ui.snowfall) {
|
||||
if ((today.month % 11 == 1) && app.config.ui.snowfall) {
|
||||
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
|
||||
}
|
||||
else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
|
||||
val eggfall = layoutInflater.inflate(R.layout.eggfall, b.rootFrame, false) as SnowfallView
|
||||
eggfall.setSnowflakeBitmaps(listOf(
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg1),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg2),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg3),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg4),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg5),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg6)
|
||||
))
|
||||
b.rootFrame.addView(eggfall)
|
||||
}
|
||||
|
||||
// WHAT'S NEW DIALOG
|
||||
if (app.config.appVersion < BuildConfig.VERSION_CODE) {
|
||||
@ -624,45 +636,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
||||
return
|
||||
}
|
||||
|
||||
app.profile.registerName?.let { registerName ->
|
||||
var status = app.config.sync.registerAvailability[registerName]
|
||||
if (status == null || status.nextCheckAt < currentTimeUnix()) {
|
||||
val api = SzkolnyApi(app)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
return@withContext api.runCatching({
|
||||
val availability = getRegisterAvailability()
|
||||
app.config.sync.registerAvailability = availability
|
||||
availability[registerName]
|
||||
}, onError = {
|
||||
if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) {
|
||||
return@withContext false
|
||||
}
|
||||
return@withContext it
|
||||
})
|
||||
}
|
||||
|
||||
when (result) {
|
||||
false -> {
|
||||
Toast.makeText(this@MainActivity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
|
||||
return@let
|
||||
}
|
||||
is Throwable -> {
|
||||
errorSnackbar.addError(result.toApiError(TAG)).show()
|
||||
return
|
||||
}
|
||||
is RegisterAvailabilityStatus -> {
|
||||
status = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) {
|
||||
val error = withContext(Dispatchers.IO) {
|
||||
app.availabilityManager.check(app.profile)
|
||||
}
|
||||
when (error?.type) {
|
||||
Type.NOT_AVAILABLE -> {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadTarget(DRAWER_ITEM_HOME)
|
||||
if (status != null)
|
||||
RegisterUnavailableDialog(this, status)
|
||||
RegisterUnavailableDialog(this, error.status!!)
|
||||
return
|
||||
}
|
||||
Type.API_ERROR -> {
|
||||
errorSnackbar.addError(error.apiError!!).show()
|
||||
return
|
||||
}
|
||||
Type.NO_API_ACCESS -> {
|
||||
Toast.makeText(this, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
swipeRefreshLayout.isRefreshing = true
|
||||
@ -689,10 +679,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
||||
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
|
||||
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
|
||||
EventBus.getDefault().removeStickyEvent(event)
|
||||
app.profile.registerName?.let { registerName ->
|
||||
event.data[registerName]?.let {
|
||||
RegisterUnavailableDialog(this, it)
|
||||
}
|
||||
val error = app.availabilityManager.check(app.profile, cacheOnly = true)
|
||||
if (error != null) {
|
||||
RegisterUnavailableDialog(this, error.status!!)
|
||||
}
|
||||
}
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
|
@ -12,10 +12,7 @@ import kotlinx.coroutines.launch
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.BuildConfig
|
||||
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
|
||||
import pl.szczodrzynski.edziennik.config.utils.ConfigMigration
|
||||
import pl.szczodrzynski.edziennik.config.utils.get
|
||||
import pl.szczodrzynski.edziennik.config.utils.set
|
||||
import pl.szczodrzynski.edziennik.config.utils.toHashMap
|
||||
import pl.szczodrzynski.edziennik.config.utils.*
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
|
||||
import pl.szczodrzynski.edziennik.data.db.AppDb
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -75,10 +72,15 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
|
||||
get() { mPrivacyPolicyAccepted = mPrivacyPolicyAccepted ?: values.get("privacyPolicyAccepted", false); return mPrivacyPolicyAccepted ?: false }
|
||||
set(value) { set("privacyPolicyAccepted", value); mPrivacyPolicyAccepted = value }
|
||||
|
||||
private var mDebugMode: Boolean? = null
|
||||
var debugMode: Boolean
|
||||
get() { mDebugMode = mDebugMode ?: values.get("debugMode", false); return mDebugMode ?: false }
|
||||
set(value) { set("debugMode", value); mDebugMode = value }
|
||||
private var mDevMode: Boolean? = null
|
||||
var devMode: Boolean?
|
||||
get() { mDevMode = mDevMode ?: values.getBooleanOrNull("debugMode"); return mDevMode }
|
||||
set(value) { set("debugMode", value?.toString()); mDevMode = value }
|
||||
|
||||
private var mEnableChucker: Boolean? = null
|
||||
var enableChucker: Boolean?
|
||||
get() { mEnableChucker = mEnableChucker ?: values.getBooleanOrNull("enableChucker"); return mEnableChucker }
|
||||
set(value) { set("enableChucker", value?.toString()); mEnableChucker = value }
|
||||
|
||||
private var mDevModePassword: String? = null
|
||||
var devModePassword: String?
|
||||
@ -120,6 +122,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
|
||||
get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert }
|
||||
set(value) { set("apiInvalidCert", value); mApiInvalidCert = value }
|
||||
|
||||
private var mApiAvailabilityCheck: Boolean? = null
|
||||
var apiAvailabilityCheck: Boolean
|
||||
get() { mApiAvailabilityCheck = mApiAvailabilityCheck ?: values.get("apiAvailabilityCheck", true); return mApiAvailabilityCheck ?: true }
|
||||
set(value) { set("apiAvailabilityCheck", value); mApiAvailabilityCheck = value }
|
||||
|
||||
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
|
||||
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
|
||||
init {
|
||||
|
@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.config
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import pl.szczodrzynski.edziennik.BuildConfig
|
||||
import pl.szczodrzynski.edziennik.config.utils.get
|
||||
import pl.szczodrzynski.edziennik.config.utils.getIntList
|
||||
import pl.szczodrzynski.edziennik.config.utils.set
|
||||
@ -123,6 +124,19 @@ class ConfigSync(private val config: Config) {
|
||||
|
||||
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
|
||||
var registerAvailability: Map<String, RegisterAvailabilityStatus>
|
||||
get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson<Map<String, RegisterAvailabilityStatus>>(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type) }; return mRegisterAvailability ?: mapOf() }
|
||||
set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value }
|
||||
get() {
|
||||
val flavor = config.values.get("registerAvailabilityFlavor", null as String?)
|
||||
if (BuildConfig.FLAVOR != flavor)
|
||||
return mapOf()
|
||||
|
||||
mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it ->
|
||||
gson.fromJson(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type)
|
||||
}
|
||||
return mRegisterAvailability ?: mapOf()
|
||||
}
|
||||
set(value) {
|
||||
config.setMap("registerAvailability", value)
|
||||
config.set("registerAvailabilityFlavor", BuildConfig.FLAVOR)
|
||||
mRegisterAvailability = value
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,11 @@ class ConfigUI(private val config: Config) {
|
||||
get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false }
|
||||
set(value) { config.set("snowfall", value); mSnowfall = value }
|
||||
|
||||
private var mEggfall: Boolean? = null
|
||||
var eggfall: Boolean
|
||||
get() { mEggfall = mEggfall ?: config.values.get("eggfall", false); return mEggfall ?: false }
|
||||
set(value) { config.set("eggfall", value); mEggfall = value }
|
||||
|
||||
private var mBottomSheetOpened: Boolean? = null
|
||||
var bottomSheetOpened: Boolean
|
||||
get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false }
|
||||
|
@ -15,8 +15,58 @@ class ProfileConfigUI(private val config: ProfileConfig) {
|
||||
get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT }
|
||||
set(value) { config.set("agendaViewType", value); mAgendaViewType = value }
|
||||
|
||||
private var mAgendaCompactMode: Boolean? = null
|
||||
var agendaCompactMode: Boolean
|
||||
get() { mAgendaCompactMode = mAgendaCompactMode ?: config.values.get("agendaCompactMode", false); return mAgendaCompactMode ?: false }
|
||||
set(value) { config.set("agendaCompactMode", value); mAgendaCompactMode = value }
|
||||
|
||||
private var mAgendaGroupByType: Boolean? = null
|
||||
var agendaGroupByType: Boolean
|
||||
get() { mAgendaGroupByType = mAgendaGroupByType ?: config.values.get("agendaGroupByType", false); return mAgendaGroupByType ?: false }
|
||||
set(value) { config.set("agendaGroupByType", value); mAgendaGroupByType = value }
|
||||
|
||||
private var mAgendaLessonChanges: Boolean? = null
|
||||
var agendaLessonChanges: Boolean
|
||||
get() { mAgendaLessonChanges = mAgendaLessonChanges ?: config.values.get("agendaLessonChanges", true); return mAgendaLessonChanges ?: true }
|
||||
set(value) { config.set("agendaLessonChanges", value); mAgendaLessonChanges = value }
|
||||
|
||||
private var mAgendaTeacherAbsence: Boolean? = null
|
||||
var agendaTeacherAbsence: Boolean
|
||||
get() { mAgendaTeacherAbsence = mAgendaTeacherAbsence ?: config.values.get("agendaTeacherAbsence", true); return mAgendaTeacherAbsence ?: true }
|
||||
set(value) { config.set("agendaTeacherAbsence", value); mAgendaTeacherAbsence = value }
|
||||
|
||||
private var mAgendaElearningMark: Boolean? = null
|
||||
var agendaElearningMark: Boolean
|
||||
get() { mAgendaElearningMark = mAgendaElearningMark ?: config.values.get("agendaElearningMark", false); return mAgendaElearningMark ?: false }
|
||||
set(value) { config.set("agendaElearningMark", value); mAgendaElearningMark = value }
|
||||
|
||||
private var mAgendaElearningGroup: Boolean? = null
|
||||
var agendaElearningGroup: Boolean
|
||||
get() { mAgendaElearningGroup = mAgendaElearningGroup ?: config.values.get("agendaElearningGroup", true); return mAgendaElearningGroup ?: true }
|
||||
set(value) { config.set("agendaElearningGroup", value); mAgendaElearningGroup = value }
|
||||
|
||||
private var mHomeCards: List<HomeCardModel>? = null
|
||||
var homeCards: List<HomeCardModel>
|
||||
get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() }
|
||||
set(value) { config.set("homeCards", value); mHomeCards = value }
|
||||
|
||||
private var mMessagesGreetingOnCompose: Boolean? = null
|
||||
var messagesGreetingOnCompose: Boolean
|
||||
get() { mMessagesGreetingOnCompose = mMessagesGreetingOnCompose ?: config.values.get("messagesGreetingOnCompose", true); return mMessagesGreetingOnCompose ?: true }
|
||||
set(value) { config.set("messagesGreetingOnCompose", value); mMessagesGreetingOnCompose = value }
|
||||
|
||||
private var mMessagesGreetingOnReply: Boolean? = null
|
||||
var messagesGreetingOnReply: Boolean
|
||||
get() { mMessagesGreetingOnReply = mMessagesGreetingOnReply ?: config.values.get("messagesGreetingOnReply", true); return mMessagesGreetingOnReply ?: true }
|
||||
set(value) { config.set("messagesGreetingOnReply", value); mMessagesGreetingOnReply = value }
|
||||
|
||||
private var mMessagesGreetingOnForward: Boolean? = null
|
||||
var messagesGreetingOnForward: Boolean
|
||||
get() { mMessagesGreetingOnForward = mMessagesGreetingOnForward ?: config.values.get("messagesGreetingOnForward", false); return mMessagesGreetingOnForward ?: false }
|
||||
set(value) { config.set("messagesGreetingOnForward", value); mMessagesGreetingOnForward = value }
|
||||
|
||||
private var mMessagesGreetingText: String? = null
|
||||
var messagesGreetingText: String?
|
||||
get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText }
|
||||
set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value }
|
||||
}
|
||||
|
@ -59,6 +59,9 @@ fun HashMap<String, String?>.get(key: String, default: String?): String? {
|
||||
fun HashMap<String, String?>.get(key: String, default: Boolean): Boolean {
|
||||
return this[key]?.toBoolean() ?: default
|
||||
}
|
||||
fun HashMap<String, String?>.getBooleanOrNull(key: String): Boolean? {
|
||||
return this[key]?.toBooleanStrictOrNull()
|
||||
}
|
||||
fun HashMap<String, String?>.get(key: String, default: Int): Int {
|
||||
return this[key]?.toIntOrNull() ?: default
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class ConfigMigration(app: App, config: Config) {
|
||||
if (dataVersion < 3) {
|
||||
update = null
|
||||
privacyPolicyAccepted = false
|
||||
debugMode = false
|
||||
devMode = null
|
||||
devModePassword = null
|
||||
appInstalledTime = 0L
|
||||
appRateSnackbarTime = 0L
|
||||
|
@ -24,7 +24,7 @@ const val FAKE_LIBRUS_ACCOUNTS = "/synergia_accounts.php"
|
||||
|
||||
val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp"
|
||||
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
|
||||
const val LIBRUS_CLIENT_ID = "0RbsDOkV9tyKEQYzlLv5hs3DM1ukrynFI4p6C1Yc"
|
||||
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
|
||||
const val LIBRUS_REDIRECT_URL = "app://librus"
|
||||
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code"
|
||||
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"
|
||||
@ -43,7 +43,7 @@ const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token"
|
||||
const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST"
|
||||
const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE="
|
||||
const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398"
|
||||
const val LIBRUS_API_CLIENT_ID_JST = "49"
|
||||
const val LIBRUS_API_CLIENT_ID_JST = "59"
|
||||
//const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42"
|
||||
|
||||
const val LIBRUS_JST_DEMO_CODE = "68656A21"
|
||||
|
@ -195,6 +195,7 @@ const val ERROR_VULCAN_HEBE_FIREBASE_ERROR = 362
|
||||
const val ERROR_VULCAN_HEBE_CERTIFICATE_GONE = 363
|
||||
const val ERROR_VULCAN_HEBE_SERVER_ERROR = 364
|
||||
const val ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND = 365
|
||||
const val ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY = 366
|
||||
const val ERROR_VULCAN_API_DEPRECATED = 390
|
||||
|
||||
const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501
|
||||
|
@ -117,6 +117,17 @@ object Regexes {
|
||||
}
|
||||
|
||||
|
||||
val MOBIDZIENNIK_TIMETABLE_TOP by lazy {
|
||||
"""<div class="plansc_top">.+?</div></div>""".toRegex(DOT_MATCHES_ALL)
|
||||
}
|
||||
val MOBIDZIENNIK_TIMETABLE_CELL by lazy {
|
||||
"""<div class="plansc_cnt_w" style="(.+?)">.+?style="(.+?)".+?title="(.+?)".+?>\s+(.+?)\s+</div>""".toRegex(DOT_MATCHES_ALL)
|
||||
}
|
||||
val MOBIDZIENNIK_TIMETABLE_LEFT by lazy {
|
||||
"""<div class="plansc_godz">.+?</div></div>""".toRegex(DOT_MATCHES_ALL)
|
||||
}
|
||||
|
||||
|
||||
|
||||
val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
|
||||
"""<input type="hidden".+?name="([A-z0-9_]+)?".+?value="([A-z0-9_+-/=]+)?".+?>""".toRegex(DOT_MATCHES_ALL)
|
||||
|
@ -18,7 +18,6 @@ import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
|
||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
|
||||
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
|
||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
|
||||
import pl.szczodrzynski.edziennik.data.api.task.IApiTask
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||
@ -27,6 +26,7 @@ import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
|
||||
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
|
||||
|
||||
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
|
||||
companion object {
|
||||
@ -90,35 +90,21 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
|
||||
return
|
||||
}
|
||||
|
||||
profile.registerName?.also { registerName ->
|
||||
var status = app.config.sync.registerAvailability[registerName]
|
||||
if (status == null || status.nextCheckAt < currentTimeUnix()) {
|
||||
val api = SzkolnyApi(app)
|
||||
api.runCatching({
|
||||
val availability = getRegisterAvailability()
|
||||
app.config.sync.registerAvailability = availability
|
||||
status = availability[registerName]
|
||||
}, onError = {
|
||||
val apiError = it.toApiError(TAG)
|
||||
if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) {
|
||||
return@also
|
||||
}
|
||||
taskCallback.onError(apiError)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
if (status?.available != true
|
||||
|| status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) {
|
||||
val error = app.availabilityManager.check(profile)
|
||||
when (error?.type) {
|
||||
Type.NOT_AVAILABLE -> {
|
||||
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
|
||||
EventBus.getDefault().postSticky(
|
||||
RegisterAvailabilityEvent(app.config.sync.registerAvailability)
|
||||
)
|
||||
EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
|
||||
}
|
||||
cancel()
|
||||
taskCallback.onCompleted()
|
||||
return
|
||||
}
|
||||
Type.API_ERROR -> {
|
||||
taskCallback.onError(error.apiError!!)
|
||||
return
|
||||
}
|
||||
else -> return@let
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,37 +111,6 @@ class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Dat
|
||||
val courseStudentEndpoint: String
|
||||
get() = "Course/$studentId/"
|
||||
|
||||
fun getSubject(longId: String, name: String): Subject {
|
||||
val id = longId.crc32()
|
||||
return subjectList.singleOrNull { it.id == id } ?: run {
|
||||
val subject = Subject(profileId, id, name, name)
|
||||
subjectList.put(id, subject)
|
||||
subject
|
||||
}
|
||||
}
|
||||
|
||||
fun getTeacher(firstName: String, lastName: String, longId: String? = null): Teacher {
|
||||
val name = "$firstName $lastName".fixName()
|
||||
val id = name.crc32()
|
||||
return teacherList.singleOrNull { it.id == id }?.also {
|
||||
if (longId != null && it.loginId == null) it.loginId = longId
|
||||
} ?: run {
|
||||
val teacher = Teacher(profileId, id, firstName, lastName, longId)
|
||||
teacherList.put(id, teacher)
|
||||
teacher
|
||||
}
|
||||
}
|
||||
|
||||
fun getTeacherByFirstLast(nameFirstLast: String, longId: String? = null): Teacher {
|
||||
val nameParts = nameFirstLast.split(" ")
|
||||
return getTeacher(nameParts[0], nameParts[1], longId)
|
||||
}
|
||||
|
||||
fun getTeacherByLastFirst(nameLastFirst: String, longId: String? = null): Teacher {
|
||||
val nameParts = nameLastFirst.split(" ")
|
||||
return getTeacher(nameParts[1], nameParts[0], longId)
|
||||
}
|
||||
|
||||
fun getEventType(longId: String, name: String): EventType {
|
||||
val id = longId.crc16().toLong()
|
||||
return eventTypes.singleOrNull { it.id == id } ?: run {
|
||||
|
@ -40,7 +40,7 @@ class EdudziennikWebExams(override val data: DataEdudziennik,
|
||||
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
|
||||
?: return@forEach
|
||||
val subjectName = subjectElement.text().trim()
|
||||
val subject = data.getSubject(subjectId, subjectName)
|
||||
val subject = data.getSubject(subjectId.crc32(), subjectName)
|
||||
|
||||
val dateString = examElement.child(2).text().trim()
|
||||
if (dateString.isBlank()) return@forEach
|
||||
|
@ -53,7 +53,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
|
||||
|
||||
val subjectId = subjectElement.id().trim()
|
||||
val subjectName = subjectElement.child(0).text().trim()
|
||||
val subject = data.getSubject(subjectId, subjectName)
|
||||
val subject = data.getSubject(subjectId.crc32(), subjectName)
|
||||
|
||||
val gradeType = when {
|
||||
subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM
|
||||
|
@ -41,7 +41,7 @@ class EdudziennikWebHomework(override val data: DataEdudziennik,
|
||||
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
|
||||
?: return@forEach
|
||||
val subjectName = subjectElement.text()
|
||||
val subject = data.getSubject(subjectId, subjectName)
|
||||
val subject = data.getSubject(subjectId.crc32(), subjectName)
|
||||
|
||||
val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date)
|
||||
val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime
|
||||
|
@ -73,7 +73,7 @@ class EdudziennikWebStart(override val data: DataEdudziennik,
|
||||
EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach {
|
||||
val id = it[1].trim()
|
||||
val name = it[2].trim()
|
||||
data.getSubject(id, name)
|
||||
data.getSubject(id.crc32(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
import pl.szczodrzynski.edziennik.crc32
|
||||
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID
|
||||
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
|
||||
@ -89,7 +90,7 @@ class EdudziennikWebTimetable(override val data: DataEdudziennik,
|
||||
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
|
||||
?: return@forEachIndexed
|
||||
val subjectName = subjectElement.text().trim()
|
||||
val subject = data.getSubject(subjectId, subjectName)
|
||||
val subject = data.getSubject(subjectId.crc32(), subjectName)
|
||||
|
||||
/* Getting teacher */
|
||||
|
||||
|
@ -18,6 +18,7 @@ const val ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE = 2050
|
||||
const val ENDPOINT_MOBIDZIENNIK_WEB_MANUALS = 2100
|
||||
const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200
|
||||
const val ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK = 2300 // not used as an endpoint
|
||||
const val ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE = 2400
|
||||
const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000
|
||||
|
||||
val MobidziennikFeatures = listOf(
|
||||
@ -38,6 +39,12 @@ val MobidziennikFeatures = listOf(
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Timetable - web scraping - does nothing if the API_MAIN timetable is enough.
|
||||
*/
|
||||
Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_TIMETABLE, listOf(
|
||||
ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_MOBIDZIENNIK_WEB
|
||||
), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)),
|
||||
/**
|
||||
* Agenda - "API" + web scraping.
|
||||
*/
|
||||
|
@ -84,6 +84,10 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
|
||||
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
|
||||
MobidziennikWebManuals(data, lastSync, onSuccess)
|
||||
}*/
|
||||
ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE-> {
|
||||
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
|
||||
MobidziennikWebTimetable(data, lastSync, onSuccess)
|
||||
}
|
||||
else -> onSuccess(endpointId)
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
|
||||
//syncWeeks.clear()
|
||||
//syncWeeks += Date.fromY_m_d("2019-12-19")
|
||||
|
||||
syncWeeks.minBy { it.value }?.let {
|
||||
syncWeeks.minByOrNull { it.value }?.let {
|
||||
data.toRemove.add(DataRemoveModel.Attendance.from(it))
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Metadata
|
||||
import pl.szczodrzynski.edziennik.fixName
|
||||
import pl.szczodrzynski.edziennik.singleOrNull
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import java.net.URLEncoder
|
||||
|
||||
class MobidziennikWebMessagesAll(override val data: DataMobidziennik,
|
||||
override val lastSync: Long?,
|
||||
@ -27,7 +28,8 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik,
|
||||
}
|
||||
|
||||
init {
|
||||
webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=+") { text ->
|
||||
val query = URLEncoder.encode(data.profile?.studentNameLong ?: "a", "UTF-8")
|
||||
webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=$query") { text ->
|
||||
MobidziennikLuckyNumberExtractor(data, text)
|
||||
|
||||
val doc = Jsoup.parse(text)
|
||||
|
@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-9-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import org.jsoup.Jsoup
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.Regexes
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.edziennik.utils.models.Week
|
||||
import kotlin.collections.set
|
||||
import kotlin.text.replace
|
||||
|
||||
class MobidziennikWebTimetable(
|
||||
override val data: DataMobidziennik,
|
||||
override val lastSync: Long?,
|
||||
val onSuccess: (endpointId: Int) -> Unit
|
||||
) : MobidziennikWeb(data, lastSync) {
|
||||
companion object {
|
||||
private const val TAG = "MobidziennikWebTimetable"
|
||||
}
|
||||
|
||||
private val rangesH = mutableMapOf<ClosedFloatingPointRange<Float>, Date>()
|
||||
private val hoursV = mutableMapOf<Int, Pair<Time, Int?>>()
|
||||
private var startDate: Date
|
||||
|
||||
private fun parseCss(css: String): Map<String, String> {
|
||||
return css.split(";").mapNotNull {
|
||||
val spl = it.split(":")
|
||||
if (spl.size != 2)
|
||||
return@mapNotNull null
|
||||
return@mapNotNull spl[0].trim() to spl[1].trim()
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun getRangeH(h: Float): Date? {
|
||||
return rangesH.entries.firstOrNull {
|
||||
h in it.key
|
||||
}?.value
|
||||
}
|
||||
|
||||
private fun stringToDate(date: String): Date? {
|
||||
val items = date.split(" ")
|
||||
val day = items.getOrNull(0)?.toIntOrNull() ?: return null
|
||||
val year = items.getOrNull(2)?.toIntOrNull() ?: return null
|
||||
val month = when (items.getOrNull(1)) {
|
||||
"stycznia" -> 1
|
||||
"lutego" -> 2
|
||||
"marca" -> 3
|
||||
"kwietnia" -> 4
|
||||
"maja" -> 5
|
||||
"czerwca" -> 6
|
||||
"lipca" -> 7
|
||||
"sierpnia" -> 8
|
||||
"września" -> 9
|
||||
"października" -> 10
|
||||
"listopada" -> 11
|
||||
"grudnia" -> 12
|
||||
else -> return null
|
||||
}
|
||||
return Date(year, month, day)
|
||||
}
|
||||
|
||||
init {
|
||||
val currentWeekStart = Week.getWeekStart()
|
||||
val nextWeekEnd = Week.getWeekEnd().stepForward(0, 0, 7)
|
||||
if (Date.getToday().weekDay > 4) {
|
||||
currentWeekStart.stepForward(0, 0, 7)
|
||||
}
|
||||
startDate = data.arguments?.getString("weekStart")?.let {
|
||||
Date.fromY_m_d(it)
|
||||
} ?: currentWeekStart
|
||||
|
||||
val syncFutureDate = startDate > nextWeekEnd
|
||||
// TODO: 2021-09-09 make DataRemoveModel keep extra lessons
|
||||
val syncExtraLessons = false && System.currentTimeMillis() - (lastSync ?: 0) > 2 * DAY * MS
|
||||
if (!syncFutureDate && !syncExtraLessons) {
|
||||
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE)
|
||||
}
|
||||
else {
|
||||
val types = when {
|
||||
syncFutureDate -> mutableListOf("podstawowy")//, "pozalekcyjny")
|
||||
syncExtraLessons -> mutableListOf("pozalekcyjny")
|
||||
else -> mutableListOf()
|
||||
}
|
||||
|
||||
syncTypes(types, startDate) {
|
||||
// set as synced now only when not syncing future date
|
||||
// (to avoid waiting 2 days for normal sync after future sync)
|
||||
if (syncExtraLessons)
|
||||
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS)
|
||||
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncTypes(types: MutableList<String>, startDate: Date, onSuccess: () -> Unit) {
|
||||
if (types.isEmpty()) {
|
||||
onSuccess()
|
||||
return
|
||||
}
|
||||
val type = types.removeAt(0)
|
||||
webGet(TAG, "/dziennik/planlekcji?typ=$type&tydzien=${startDate.stringY_m_d}") { html ->
|
||||
MobidziennikLuckyNumberExtractor(data, html)
|
||||
readRangesH(html)
|
||||
readRangesV(html)
|
||||
readLessons(html)
|
||||
syncTypes(types, startDate, onSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRangesH(html: String) {
|
||||
val htmlH = Regexes.MOBIDZIENNIK_TIMETABLE_TOP.find(html) ?: return
|
||||
val docH = Jsoup.parse(htmlH.value)
|
||||
|
||||
var posH = 0f
|
||||
for (el in docH.select("div > div")) {
|
||||
val css = parseCss(el.attr("style"))
|
||||
val width = css["width"]
|
||||
?.trimEnd('%')
|
||||
?.toFloatOrNull()
|
||||
?: continue
|
||||
val value = stringToDate(el.attr("title"))
|
||||
?: continue
|
||||
|
||||
val range = posH.rangeTo(posH + width)
|
||||
posH += width
|
||||
|
||||
rangesH[range] = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRangesV(html: String) {
|
||||
val htmlV = Regexes.MOBIDZIENNIK_TIMETABLE_LEFT.find(html) ?: return
|
||||
val docV = Jsoup.parse(htmlV.value)
|
||||
|
||||
for (el in docV.select("div > div")) {
|
||||
val css = parseCss(el.attr("style"))
|
||||
val top = css["top"]
|
||||
?.trimEnd('%')
|
||||
?.toFloatOrNull()
|
||||
?: continue
|
||||
val values = el.text().split(" ")
|
||||
|
||||
val time = values.getOrNull(0)?.let {
|
||||
Time.fromH_m(it)
|
||||
} ?: continue
|
||||
val num = values.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
hoursV[(top * 100).toInt()] = time to num
|
||||
}
|
||||
}
|
||||
|
||||
private val whitespaceRegex = "\\s+".toRegex()
|
||||
private val classroomRegex = "\\((.*)\\)".toRegex()
|
||||
private fun cleanup(str: String): List<String> {
|
||||
return str
|
||||
.replace(whitespaceRegex, " ")
|
||||
.replace("\n", "")
|
||||
.replace("<small>", "$")
|
||||
.replace("</small>", "$")
|
||||
.replace("<br />", "\n")
|
||||
.replace("<br/>", "\n")
|
||||
.replace("<br>", "\n")
|
||||
.replace("<br />", "\n")
|
||||
.replace("<br/>", "\n")
|
||||
.replace("<br>", "\n")
|
||||
.replace("<b>", "%")
|
||||
.replace("</b>", "%")
|
||||
.replace("<span>", "")
|
||||
.replace("</span>", "")
|
||||
.split("\n")
|
||||
.map { it.trim() }
|
||||
}
|
||||
|
||||
@SuppressLint("LongLogTag", "LogNotTimber")
|
||||
private fun readLessons(html: String) {
|
||||
val matches = Regexes.MOBIDZIENNIK_TIMETABLE_CELL.findAll(html)
|
||||
|
||||
val noLessonDays = mutableListOf<Date>()
|
||||
for (i in 0..6) {
|
||||
noLessonDays.add(startDate.clone().stepForward(0, 0, i))
|
||||
}
|
||||
|
||||
for (match in matches) {
|
||||
val css = parseCss("${match[1]};${match[2]}")
|
||||
val left = css["left"]?.trimEnd('%')?.toFloatOrNull() ?: continue
|
||||
val top = css["top"]?.trimEnd('%')?.toFloatOrNull() ?: continue
|
||||
val width = css["width"]?.trimEnd('%')?.toFloatOrNull() ?: continue
|
||||
val height = css["height"]?.trimEnd('%')?.toFloatOrNull() ?: continue
|
||||
|
||||
val posH = left + width / 2f
|
||||
val topInt = (top * 100).toInt()
|
||||
val bottomInt = ((top + height) * 100).toInt()
|
||||
|
||||
val lessonDate = getRangeH(posH) ?: continue
|
||||
val (startTime, lessonNumber) = hoursV[topInt] ?: continue
|
||||
val endTime = hoursV[bottomInt]?.first ?: continue
|
||||
|
||||
noLessonDays.remove(lessonDate)
|
||||
|
||||
var typeName: String? = null
|
||||
var subjectName: String? = null
|
||||
var teacherName: String? = null
|
||||
var classroomName: String? = null
|
||||
var teamName: String? = null
|
||||
val items = (cleanup(match[3]) + cleanup(match[4])).toMutableList()
|
||||
|
||||
var length = 0
|
||||
while (items.isNotEmpty() && length != items.size) {
|
||||
length = items.size
|
||||
val toRemove = mutableListOf<String?>()
|
||||
items.forEachIndexed { i, item ->
|
||||
when {
|
||||
item.isEmpty() ->
|
||||
toRemove.add(item)
|
||||
item.contains(":") && item.contains(" - ") ->
|
||||
toRemove.add(item)
|
||||
|
||||
item.startsWith("%") -> {
|
||||
subjectName = item.trim('%')
|
||||
// I have no idea what's going on here
|
||||
// ok now seriously.. the subject (long or short) item
|
||||
// may NOT be 0th, as the HH:MM - HH:MM item may be before
|
||||
// or even the typeName item. As these are always **before**,
|
||||
// they are removed in previous iterations, so the first not removed
|
||||
// item should be the long/short subjectName needing to be removed now.
|
||||
toRemove.add(items[toRemove.size])
|
||||
// ...and this has to be added later
|
||||
toRemove.add(item)
|
||||
}
|
||||
|
||||
item.startsWith("&") -> {
|
||||
typeName = item.trim('&')
|
||||
toRemove.add(item)
|
||||
}
|
||||
typeName != null && (item.contains(typeName!!) || item.contains("</small>")) -> {
|
||||
toRemove.add(item)
|
||||
}
|
||||
|
||||
item.contains("(") && item.contains(")") -> {
|
||||
classroomName = classroomRegex.find(item)?.get(1)
|
||||
items[i] = item.replace("($classroomName)", "").trim()
|
||||
}
|
||||
classroomName != null && item.contains(classroomName!!) -> {
|
||||
items[i] = item.replace("($classroomName)", "").trim()
|
||||
}
|
||||
|
||||
item.contains("class=\"wyjatek tooltip\"") ->
|
||||
toRemove.add(item)
|
||||
}
|
||||
}
|
||||
items.removeAll(toRemove)
|
||||
}
|
||||
|
||||
if (items.size == 2 && items[0].contains(" - ")) {
|
||||
val parts = items[0].split(" - ")
|
||||
teamName = parts[0]
|
||||
teacherName = parts[1]
|
||||
}
|
||||
else if (items.size == 2 && typeName?.contains("odwołana") == true) {
|
||||
teamName = items[0]
|
||||
}
|
||||
else if (items.size == 4) {
|
||||
teamName = items[0]
|
||||
teacherName = items[1]
|
||||
}
|
||||
|
||||
val type = when (typeName) {
|
||||
"zastępstwo" -> Lesson.TYPE_CHANGE
|
||||
"lekcja odwołana", "odwołana" -> Lesson.TYPE_CANCELLED
|
||||
else -> Lesson.TYPE_NORMAL
|
||||
}
|
||||
val subject = subjectName?.let { data.getSubject(null, it) }
|
||||
val teacher = teacherName?.let { data.getTeacherByLastFirst(it) }
|
||||
val team = teamName?.let { data.getTeam(
|
||||
id = null,
|
||||
name = it,
|
||||
schoolCode = data.loginServerName ?: return@let null,
|
||||
isTeamClass = false
|
||||
) }
|
||||
|
||||
Lesson(data.profileId, -1).also {
|
||||
it.type = type
|
||||
if (type == Lesson.TYPE_CANCELLED) {
|
||||
it.oldDate = lessonDate
|
||||
it.oldLessonNumber = lessonNumber
|
||||
it.oldStartTime = startTime
|
||||
it.oldEndTime = endTime
|
||||
it.oldSubjectId = subject?.id ?: -1
|
||||
it.oldTeamId = team?.id ?: -1
|
||||
}
|
||||
else {
|
||||
it.date = lessonDate
|
||||
it.lessonNumber = lessonNumber
|
||||
it.startTime = startTime
|
||||
it.endTime = endTime
|
||||
it.subjectId = subject?.id ?: -1
|
||||
it.teacherId = teacher?.id ?: -1
|
||||
it.teamId = team?.id ?: -1
|
||||
it.classroom = classroomName
|
||||
}
|
||||
|
||||
it.id = it.buildId()
|
||||
|
||||
val seen = profile?.empty == false || lessonDate < Date.getToday()
|
||||
|
||||
if (it.type != Lesson.TYPE_NORMAL) {
|
||||
data.metadataList.add(
|
||||
Metadata(
|
||||
data.profileId,
|
||||
Metadata.TYPE_LESSON_CHANGE,
|
||||
it.id,
|
||||
seen,
|
||||
seen
|
||||
)
|
||||
)
|
||||
}
|
||||
data.lessonList += it
|
||||
}
|
||||
}
|
||||
|
||||
for (date in noLessonDays) {
|
||||
data.lessonList += Lesson(data.profileId, date.value.toLong()).also {
|
||||
it.type = Lesson.TYPE_NO_LESSONS
|
||||
it.date = date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,39 +81,4 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
|
||||
|
||||
val loginShort: String?
|
||||
get() = studentLogin?.split('@')?.get(0)
|
||||
|
||||
fun getSubject(name: String): Subject {
|
||||
val id = name.crc32()
|
||||
return subjectList.singleOrNull { it.id == id } ?: run {
|
||||
val subject = Subject(profileId, id, name, name)
|
||||
subjectList.put(id, subject)
|
||||
subject
|
||||
}
|
||||
}
|
||||
|
||||
fun getTeacher(firstName: String, lastName: String): Teacher {
|
||||
val name = "$firstName $lastName".fixName()
|
||||
return teacherList.singleOrNull { it.fullName == name } ?: run {
|
||||
val id = name.crc32()
|
||||
val teacher = Teacher(profileId, id, firstName, lastName)
|
||||
teacherList.put(id, teacher)
|
||||
teacher
|
||||
}
|
||||
}
|
||||
|
||||
fun getTeam(name: String? = null): Team {
|
||||
if (name == "cała klasa" || name == null) return teamClass ?: run {
|
||||
val id = className!!.crc32()
|
||||
val teamCode = "$schoolShortName:$className"
|
||||
val team = Team(profileId, id, className, Team.TYPE_CLASS, teamCode, -1)
|
||||
teamList.put(id, team)
|
||||
return team
|
||||
} else {
|
||||
val id = name.crc32()
|
||||
val teamCode = "$schoolShortName:$name"
|
||||
val team = Team(profileId, id, name, Team.TYPE_VIRTUAL, teamCode, -1)
|
||||
teamList.put(id, team)
|
||||
return team
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List<JsonObject>)
|
||||
}
|
||||
|
||||
val subjectName = grade.getString("SchoolSubject") ?: return@forEach
|
||||
val subject = data.getSubject(subjectName)
|
||||
val subject = data.getSubject(null, subjectName)
|
||||
|
||||
val addedDate = if (profile.empty) profile.getSemesterStart(semester).inMillis
|
||||
else System.currentTimeMillis()
|
||||
|
@ -34,7 +34,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
|
||||
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
|
||||
|
||||
val subjectName = grade.getString("SchoolSubject") ?: return@forEach
|
||||
val subject = data.getSubject(subjectName)
|
||||
val subject = data.getSubject(null, subjectName)
|
||||
|
||||
val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis }
|
||||
?: System.currentTimeMillis()
|
||||
|
@ -22,7 +22,13 @@ class PodlasieApiMain(override val data: DataPodlasie,
|
||||
|
||||
init {
|
||||
apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
|
||||
data.getTeam() // Save the class team when it doesn't exist.
|
||||
// Save the class team when it doesn't exist.
|
||||
data.getTeam(
|
||||
id = null,
|
||||
name = data.className ?: "",
|
||||
schoolCode = data.schoolShortName ?: "",
|
||||
isTeamClass = true
|
||||
)
|
||||
|
||||
json.getInt("LuckyNumber")?.let { PodlasieApiLuckyNumber(data, it) }
|
||||
json.getJsonArray("Teacher")?.asJsonObjectList()?.let { PodlasieApiTeachers(data, it) }
|
||||
|
@ -43,14 +43,21 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List<JsonObject>) {
|
||||
val startTime = lesson.getString("TimeFrom")?.let { Time.fromH_m_s(it) }
|
||||
?: return@forEach
|
||||
val endTime = lesson.getString("TimeTo")?.let { Time.fromH_m_s(it) } ?: return@forEach
|
||||
val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(it) }
|
||||
val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(null, it) }
|
||||
?: return@forEach
|
||||
|
||||
val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach
|
||||
val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach
|
||||
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
|
||||
|
||||
val team = lesson.getString("Group")?.let { data.getTeam(it) } ?: return@forEach
|
||||
val team = lesson.getString("Group")?.let {
|
||||
data.getTeam(
|
||||
id = null,
|
||||
name = it,
|
||||
schoolCode = data.schoolShortName ?: "",
|
||||
isTeamClass = it == "cała klasa"
|
||||
)
|
||||
} ?: return@forEach
|
||||
val classroom = lesson.getString("Room")
|
||||
|
||||
Lesson(data.profileId, -1).also {
|
||||
|
@ -222,6 +222,16 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
|
||||
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
|
||||
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value }
|
||||
|
||||
private var mSenderAddressHash: String? = null
|
||||
var senderAddressHash: String?
|
||||
get() { mSenderAddressHash = mSenderAddressHash ?: profile?.getStudentData("senderAddressHash", null); return mSenderAddressHash }
|
||||
set(value) { profile?.putStudentData("senderAddressHash", value) ?: return; mSenderAddressHash = value }
|
||||
|
||||
private var mSenderAddressName: String? = null
|
||||
var senderAddressName: String?
|
||||
get() { mSenderAddressName = mSenderAddressName ?: profile?.getStudentData("senderAddressName", null); return mSenderAddressName }
|
||||
set(value) { profile?.putStudentData("senderAddressName", value) ?: return; mSenderAddressName = value }
|
||||
|
||||
val apiUrl: String?
|
||||
get() {
|
||||
val url = when (apiToken[symbol]?.substring(0, 3)) {
|
||||
|
@ -38,7 +38,7 @@ class VulcanHebeAttendance(
|
||||
lastSync = lastSync
|
||||
) { list, _ ->
|
||||
list.forEach { attendance ->
|
||||
val id = attendance.getLong("AuxPresenceId") ?: return@forEach
|
||||
val id = attendance.getLong("Id") ?: return@forEach
|
||||
val type = attendance.getJsonObject("PresenceType") ?: return@forEach
|
||||
val baseType = getBaseType(type)
|
||||
val typeName = type.getString("Name") ?: return@forEach
|
||||
|
@ -97,6 +97,10 @@ class VulcanHebeMain(
|
||||
val studentSemesterId = period.getInt("Id") ?: return@forEach
|
||||
val studentSemesterNumber = period.getInt("Number") ?: return@forEach
|
||||
|
||||
val senderEntry = student.getJsonObject("SenderEntry")
|
||||
val senderAddressName = senderEntry.getString("Address")
|
||||
val senderAddressHash = senderEntry.getString("AddressHash")
|
||||
|
||||
val hebeContext = student.getString("Context")
|
||||
|
||||
val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true)
|
||||
@ -143,6 +147,8 @@ class VulcanHebeMain(
|
||||
studentData["schoolSymbol"] = schoolSymbol
|
||||
studentData["schoolShort"] = schoolShort
|
||||
studentData["schoolName"] = schoolCode
|
||||
studentData["senderAddressName"] = senderAddressName
|
||||
studentData["senderAddressHash"] = senderAddressHash
|
||||
studentData["hebeContext"] = hebeContext
|
||||
}
|
||||
dateSemester1Start?.let {
|
||||
|
@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
|
||||
import com.google.gson.JsonObject
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY
|
||||
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
|
||||
@ -27,6 +28,22 @@ class VulcanHebeSendMessage(
|
||||
}
|
||||
|
||||
init {
|
||||
if (data.senderAddressName == null || data.senderAddressHash == null) {
|
||||
VulcanHebeMain(data).getStudents(data.profile, null) {
|
||||
if (data.senderAddressName == null || data.senderAddressHash == null) {
|
||||
data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY)
|
||||
}
|
||||
else {
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendMessage() {
|
||||
val recipientsArray = JsonArray()
|
||||
recipients.forEach { teacher ->
|
||||
recipientsArray += JsonObject(
|
||||
@ -40,10 +57,10 @@ class VulcanHebeSendMessage(
|
||||
val senderName = (profile?.accountName ?: profile?.studentNameLong)
|
||||
?.swapFirstLastName() ?: ""
|
||||
val sender = JsonObject(
|
||||
"Address" to senderName,
|
||||
"Address" to data.senderAddressName,
|
||||
"LoginId" to data.studentLoginId.toString(),
|
||||
"Initials" to senderName.getNameInitials(),
|
||||
"AddressHash" to senderName.sha1Hex()
|
||||
"AddressHash" to data.senderAddressHash
|
||||
)
|
||||
|
||||
apiPost(
|
||||
|
@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGE
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
|
||||
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE
|
||||
@ -47,7 +48,7 @@ class VulcanHebeTimetable(
|
||||
?: previousWeekStart
|
||||
val dateTo = dateFrom.clone().stepForward(0, 0, 13)
|
||||
|
||||
val lastSync = null
|
||||
val lastSync = 0L
|
||||
|
||||
apiGetList(
|
||||
TAG,
|
||||
@ -106,6 +107,8 @@ class VulcanHebeTimetable(
|
||||
"Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}"
|
||||
)
|
||||
|
||||
data.toRemove.add(DataRemoveModel.Timetable.between(dateFrom, dateTo))
|
||||
|
||||
data.lessonList.addAll(lessonList)
|
||||
|
||||
data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS)
|
||||
|
@ -4,8 +4,4 @@
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.api.events
|
||||
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
|
||||
|
||||
data class RegisterAvailabilityEvent(
|
||||
val data: Map< String, RegisterAvailabilityStatus>
|
||||
)
|
||||
class RegisterAvailabilityEvent()
|
||||
|
@ -2,6 +2,7 @@ package pl.szczodrzynski.edziennik.data.api.models
|
||||
|
||||
import android.util.LongSparseArray
|
||||
import android.util.SparseArray
|
||||
import androidx.core.util.set
|
||||
import androidx.core.util.size
|
||||
import androidx.room.OnConflictStrategy
|
||||
import com.google.gson.JsonObject
|
||||
@ -376,4 +377,108 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
|
||||
fun startProgress(stringRes: Int) {
|
||||
callback.onStartProgress(stringRes)
|
||||
}
|
||||
|
||||
/* _ _ _ _ _
|
||||
| | | | | (_) |
|
||||
| | | | |_ _| |___
|
||||
| | | | __| | / __|
|
||||
| |__| | |_| | \__ \
|
||||
\____/ \__|_|_|__*/
|
||||
fun getSubject(id: Long?, name: String, shortName: String = name): Subject {
|
||||
var subject = subjectList.singleOrNull { it.id == id }
|
||||
if (subject == null)
|
||||
subject = subjectList.singleOrNull { it.longName == name }
|
||||
if (subject == null)
|
||||
subject = subjectList.singleOrNull { it.shortName == name }
|
||||
|
||||
if (subject == null) {
|
||||
subject = Subject(
|
||||
profileId,
|
||||
id ?: name.crc32(),
|
||||
name,
|
||||
shortName
|
||||
)
|
||||
subjectList[subject.id] = subject
|
||||
}
|
||||
return subject
|
||||
}
|
||||
|
||||
fun getTeam(id: Long?, name: String, schoolCode: String, isTeamClass: Boolean = false): Team {
|
||||
if (isTeamClass && teamClass != null)
|
||||
return teamClass as Team
|
||||
var team = teamList.singleOrNull { it.id == id }
|
||||
|
||||
val namePlain = name.replace(" ", "")
|
||||
if (team == null)
|
||||
team = teamList.singleOrNull { it.name.replace(" ", "") == namePlain }
|
||||
|
||||
if (team == null) {
|
||||
team = Team(
|
||||
profileId,
|
||||
id ?: name.crc32(),
|
||||
name,
|
||||
if (isTeamClass) Team.TYPE_CLASS else Team.TYPE_VIRTUAL,
|
||||
"$schoolCode:$name",
|
||||
-1
|
||||
)
|
||||
teamList[team.id] = team
|
||||
}
|
||||
return team
|
||||
}
|
||||
|
||||
fun getTeacher(firstName: String, lastName: String, loginId: String? = null): Teacher {
|
||||
val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" }
|
||||
return validateTeacher(teacher, firstName, lastName, loginId)
|
||||
}
|
||||
|
||||
fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher {
|
||||
val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" }
|
||||
return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId)
|
||||
}
|
||||
|
||||
fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher {
|
||||
val nameParts = nameLastFirst.split(" ")
|
||||
return if (nameParts.size == 1)
|
||||
getTeacher(nameParts[0], "", loginId)
|
||||
else
|
||||
getTeacher(nameParts[1], nameParts[0], loginId)
|
||||
}
|
||||
|
||||
fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher {
|
||||
val nameParts = nameFirstLast.split(" ")
|
||||
return if (nameParts.size == 1)
|
||||
getTeacher(nameParts[0], "", loginId)
|
||||
else
|
||||
getTeacher(nameParts[0], nameParts[1], loginId)
|
||||
}
|
||||
|
||||
fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher {
|
||||
val nameParts = nameFDotLast.split(".")
|
||||
return if (nameParts.size == 1)
|
||||
getTeacher(nameParts[0], "", loginId)
|
||||
else
|
||||
getTeacher(nameParts[0][0], nameParts[1], loginId)
|
||||
}
|
||||
|
||||
fun getTeacherByFDotSpaceLast(nameFDotSpaceLast: String, loginId: String? = null): Teacher {
|
||||
val nameParts = nameFDotSpaceLast.split(".")
|
||||
return if (nameParts.size == 1)
|
||||
getTeacher(nameParts[0], "", loginId)
|
||||
else
|
||||
getTeacher(nameParts[0][0], nameParts[1], loginId)
|
||||
}
|
||||
|
||||
private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher {
|
||||
val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply {
|
||||
id = fullName.crc32()
|
||||
teacherList[id] = this
|
||||
}
|
||||
return obj.also {
|
||||
if (loginId != null && it.loginId != null)
|
||||
it.loginId = loginId
|
||||
if (firstName.length > 1)
|
||||
it.name = firstName
|
||||
it.surname = lastName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.ApiCacheIntercept
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.request.*
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.response.*
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Notification
|
||||
@ -373,6 +370,15 @@ class SzkolnyApi(val app: App) : CoroutineScope {
|
||||
throw SzkolnyApiException(null)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getContributors(): ContributorsResponse {
|
||||
val response = api.contributors().execute()
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
return parseResponse(response)
|
||||
}
|
||||
throw SzkolnyApiException(null)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getFirebaseToken(registerName: String): String {
|
||||
val response = api.firebaseToken(registerName).execute()
|
||||
|
@ -27,6 +27,9 @@ interface SzkolnyService {
|
||||
@POST("appUser")
|
||||
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Unit>>
|
||||
|
||||
@GET("contributors/android")
|
||||
fun contributors(): Call<ApiResponse<ContributorsResponse>>
|
||||
|
||||
@GET("updates/app")
|
||||
fun updates(@Query("channel") channel: String = "release"): Call<ApiResponse<List<Update>>>
|
||||
|
||||
|
@ -15,6 +15,7 @@ class ApiCacheInterceptor(val app: App) : Interceptor {
|
||||
val request = chain.request()
|
||||
if (request.url().host() == "api.szkolny.eu"
|
||||
&& Signing.appCertificate.md5() == app.config.apiInvalidCert
|
||||
&& !app.buildManager.isSigned
|
||||
) {
|
||||
val response = ApiResponse<Unit>(
|
||||
success = false,
|
||||
|
@ -46,6 +46,6 @@ object Signing {
|
||||
|
||||
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
|
||||
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
|
||||
return "$param1.MTIzNDU2Nzg5MDy+5jm3L0===.$param2".sha256()
|
||||
return "$param1.MTIzNDU2Nzg5MDY8+Uq3So===.$param2".sha256()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package pl.szczodrzynski.edziennik.data.api.szkolny.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
data class ContributorsResponse(
|
||||
val contributors: List<Item>,
|
||||
val translators: List<Item>
|
||||
) {
|
||||
|
||||
@Parcelize
|
||||
data class Item(
|
||||
val login: String,
|
||||
val name: String?,
|
||||
val avatarUrl: String,
|
||||
val profileUrl: String,
|
||||
val itemUrl: String,
|
||||
val contributions: Int?
|
||||
) : Parcelable
|
||||
}
|
@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.*
|
||||
import pl.szczodrzynski.edziennik.getNotificationTitle
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Week
|
||||
|
||||
class Notifications(val app: App, val notifications: MutableList<Notification>, val profiles: List<Profile>) {
|
||||
companion object {
|
||||
@ -42,13 +43,22 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
|
||||
val text = app.getString(
|
||||
R.string.notification_lesson_change_format,
|
||||
lesson.getDisplayChangeType(app),
|
||||
if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString,
|
||||
lesson.displayDate?.formattedString ?: "",
|
||||
lesson.changeSubjectName
|
||||
)
|
||||
val textLong = app.getString(
|
||||
R.string.notification_lesson_change_long_format,
|
||||
lesson.getDisplayChangeType(app),
|
||||
lesson.displayDate?.formattedString ?: "-",
|
||||
lesson.displayDate?.weekDay?.let { Week.getFullDayName(it) } ?: "-",
|
||||
lesson.changeSubjectName,
|
||||
lesson.changeTeacherName
|
||||
)
|
||||
notifications += Notification(
|
||||
id = Notification.buildId(lesson.profileId, Notification.TYPE_TIMETABLE_LESSON_CHANGE, lesson.id),
|
||||
title = app.getNotificationTitle(Notification.TYPE_TIMETABLE_LESSON_CHANGE),
|
||||
text = text,
|
||||
textLong = textLong,
|
||||
type = Notification.TYPE_TIMETABLE_LESSON_CHANGE,
|
||||
profileId = lesson.profileId,
|
||||
profileName = profiles.singleOrNull { it.id == lesson.profileId }?.name,
|
||||
@ -79,11 +89,21 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
|
||||
event.date.formattedString,
|
||||
event.subjectLongName
|
||||
)
|
||||
val textLong = app.getString(
|
||||
R.string.notification_event_long_format,
|
||||
event.typeName ?: "-",
|
||||
event.subjectLongName ?: "-",
|
||||
event.date.formattedString,
|
||||
Week.getFullDayName(event.date.weekDay),
|
||||
event.time?.stringHM ?: app.getString(R.string.event_all_day),
|
||||
event.topic.take(200)
|
||||
)
|
||||
val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT
|
||||
notifications += Notification(
|
||||
id = Notification.buildId(event.profileId, type, event.id),
|
||||
title = app.getNotificationTitle(type),
|
||||
text = text,
|
||||
textLong = textLong,
|
||||
type = type,
|
||||
profileId = event.profileId,
|
||||
profileName = profiles.singleOrNull { it.id == event.profileId }?.name,
|
||||
@ -102,11 +122,22 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
|
||||
event.date.formattedString,
|
||||
event.topic
|
||||
)
|
||||
val textLong = app.getString(
|
||||
R.string.notification_shared_event_long_format,
|
||||
event.sharedByName,
|
||||
event.typeName ?: "-",
|
||||
event.subjectLongName ?: "-",
|
||||
event.date.formattedString,
|
||||
Week.getFullDayName(event.date.weekDay),
|
||||
event.time?.stringHM ?: app.getString(R.string.event_all_day),
|
||||
event.topic.take(200)
|
||||
)
|
||||
val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT
|
||||
notifications += Notification(
|
||||
id = Notification.buildId(event.profileId, type, event.id),
|
||||
title = app.getNotificationTitle(type),
|
||||
text = text,
|
||||
textLong = textLong,
|
||||
type = type,
|
||||
profileId = event.profileId,
|
||||
profileName = profiles.singleOrNull { it.id == event.profileId }?.name,
|
||||
@ -130,10 +161,20 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
|
||||
gradeName,
|
||||
grade.subjectLongName
|
||||
)
|
||||
val textLong = app.getString(
|
||||
R.string.notification_grade_long_format,
|
||||
gradeName,
|
||||
grade.weight.toString(),
|
||||
grade.subjectLongName ?: "-",
|
||||
grade.category ?: "-",
|
||||
grade.description ?: "-",
|
||||
grade.teacherName ?: "-"
|
||||
)
|
||||
notifications += Notification(
|
||||
id = Notification.buildId(grade.profileId, Notification.TYPE_NEW_GRADE, grade.id),
|
||||
title = app.getNotificationTitle(Notification.TYPE_NEW_GRADE),
|
||||
text = text,
|
||||
textLong = textLong,
|
||||
type = Notification.TYPE_NEW_GRADE,
|
||||
profileId = grade.profileId,
|
||||
profileName = profiles.singleOrNull { it.id == grade.profileId }?.name,
|
||||
@ -158,10 +199,17 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
|
||||
notice.teacherName,
|
||||
Date.fromMillis(notice.addedDate).formattedString
|
||||
)
|
||||
val textLong = app.getString(
|
||||
R.string.notification_notice_long_format,
|
||||
noticeTypeStr,
|
||||
notice.teacherName ?: "-",
|
||||
notice.text.take(200)
|
||||
)
|
||||
notifications += Notification(
|
||||
id = Notification.buildId(notice.profileId, Notification.TYPE_NEW_NOTICE, notice.id),
|
||||
title = app.getNotificationTitle(Notification.TYPE_NEW_NOTICE),
|
||||
text = text,
|
||||
textLong = textLong,
|
||||
type = Notification.TYPE_NEW_NOTICE,
|
||||
profileId = notice.profileId,
|
||||
profileName = profiles.singleOrNull { it.id == notice.profileId }?.name,
|
||||
@ -193,10 +241,21 @@ class Notifications(val app: App, val notifications: MutableList<Notification>,
|
||||
attendance.subjectLongName,
|
||||
attendance.date.formattedString
|
||||
)
|
||||
val textLong = app.getString(
|
||||
R.string.notification_attendance_long_format,
|
||||
attendanceTypeStr,
|
||||
attendance.date.formattedString,
|
||||
attendance.startTime?.stringHM ?: "-",
|
||||
attendance.lessonNumber ?: "-",
|
||||
attendance.subjectLongName ?: "-",
|
||||
attendance.teacherName ?: "-",
|
||||
attendance.lessonTopic ?: "-"
|
||||
)
|
||||
notifications += Notification(
|
||||
id = Notification.buildId(attendance.profileId, Notification.TYPE_NEW_ATTENDANCE, attendance.id),
|
||||
title = app.getNotificationTitle(Notification.TYPE_NEW_ATTENDANCE),
|
||||
text = text,
|
||||
textLong = textLong,
|
||||
type = Notification.TYPE_NEW_ATTENDANCE,
|
||||
profileId = attendance.profileId,
|
||||
profileName = profiles.singleOrNull { it.id == attendance.profileId }?.name,
|
||||
|
@ -8,6 +8,9 @@ import android.util.SparseIntArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.util.forEach
|
||||
import androidx.core.util.set
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.*
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SERVER_MESSAGE
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
@ -107,6 +110,10 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
|
||||
.setContentText(buildSummaryText(summaryCounts))
|
||||
.setTicker(newNotificationsText)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(IconicsDrawable(app).apply {
|
||||
icon = CommunityMaterial.Icon.cmd_bell_ring_outline
|
||||
colorRes = R.color.colorPrimary
|
||||
}.toBitmap())
|
||||
.setStyle(NotificationCompat.InboxStyle()
|
||||
.also {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@ -137,8 +144,11 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
|
||||
.setSubText(if (it.type == TYPE_SERVER_MESSAGE) null else it.title)
|
||||
.setTicker("${it.profileName}: ${it.title}")
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(IconicsDrawable(app, it.getLargeIcon()).apply {
|
||||
colorRes = R.color.colorPrimary
|
||||
}.toBitmap())
|
||||
.setStyle(NotificationCompat.BigTextStyle()
|
||||
.bigText(it.text))
|
||||
.bigText(it.textLong ?: it.text))
|
||||
.setWhen(it.addedDate)
|
||||
.addDefaults()
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||
@ -160,6 +170,10 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
|
||||
.setContentText(buildSummaryText(summaryCounts))
|
||||
.setTicker(newNotificationsText)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setLargeIcon(IconicsDrawable(app).apply {
|
||||
icon = CommunityMaterial.Icon.cmd_bell_ring_outline
|
||||
colorRes = R.color.colorPrimary
|
||||
}.toBitmap())
|
||||
.addDefaults()
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(summaryIntent)
|
||||
|
@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
|
||||
LibrusLesson::class,
|
||||
TimetableManual::class,
|
||||
Metadata::class
|
||||
], version = 91)
|
||||
], version = 93)
|
||||
@TypeConverters(
|
||||
ConverterTime::class,
|
||||
ConverterDate::class,
|
||||
@ -176,7 +176,9 @@ abstract class AppDb : RoomDatabase() {
|
||||
Migration88(),
|
||||
Migration89(),
|
||||
Migration90(),
|
||||
Migration91()
|
||||
Migration91(),
|
||||
Migration92(),
|
||||
Migration93()
|
||||
).allowMainThreadQueries().build()
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ abstract class AttendanceDao : BaseDao<Attendance, AttendanceFull> {
|
||||
getRawNow("$QUERY WHERE notified = 0 $ORDER_BY")
|
||||
fun getNotNotifiedNow(profileId: Int) =
|
||||
getRawNow("$QUERY WHERE attendances.profileId = $profileId AND notified = 0 $ORDER_BY")
|
||||
fun getAllByDateNow(profileId: Int, date: Date) =
|
||||
getRawNow("$QUERY WHERE attendances.profileId = $profileId AND attendanceDate = '${date.stringY_m_d}' $ORDER_BY")
|
||||
|
||||
// GET ONE - NOW
|
||||
fun getByIdNow(profileId: Int, id: Long) =
|
||||
|
@ -9,30 +9,9 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT
|
||||
|
||||
@Dao
|
||||
abstract class EventTypeDao {
|
||||
@ -58,19 +37,18 @@ abstract class EventTypeDao {
|
||||
abstract val allNow: List<EventType>
|
||||
|
||||
fun addDefaultTypes(context: Context, profileId: Int): List<EventType> {
|
||||
val typeList = listOf(
|
||||
EventType(profileId, TYPE_HOMEWORK, context.getString(R.string.event_type_homework), COLOR_HOMEWORK),
|
||||
EventType(profileId, TYPE_DEFAULT, context.getString(R.string.event_other), COLOR_DEFAULT),
|
||||
EventType(profileId, TYPE_EXAM, context.getString(R.string.event_exam), COLOR_EXAM),
|
||||
EventType(profileId, TYPE_SHORT_QUIZ, context.getString(R.string.event_short_quiz), COLOR_SHORT_QUIZ),
|
||||
EventType(profileId, TYPE_ESSAY, context.getString(R.string.event_essay), COLOR_ESSAY),
|
||||
EventType(profileId, TYPE_PROJECT, context.getString(R.string.event_project), COLOR_PROJECT),
|
||||
EventType(profileId, TYPE_PT_MEETING, context.getString(R.string.event_pt_meeting), COLOR_PT_MEETING),
|
||||
EventType(profileId, TYPE_EXCURSION, context.getString(R.string.event_excursion), COLOR_EXCURSION),
|
||||
EventType(profileId, TYPE_READING, context.getString(R.string.event_reading), COLOR_READING),
|
||||
EventType(profileId, TYPE_CLASS_EVENT, context.getString(R.string.event_class_event), COLOR_CLASS_EVENT),
|
||||
EventType(profileId, TYPE_INFORMATION, context.getString(R.string.event_information), COLOR_INFORMATION)
|
||||
)
|
||||
var order = 100
|
||||
val colorMap = EventType.getTypeColorMap()
|
||||
val typeList = EventType.getTypeNameMap().map { (id, name) ->
|
||||
EventType(
|
||||
profileId = profileId,
|
||||
id = id,
|
||||
name = context.getString(name),
|
||||
color = colorMap[id] ?: COLOR_DEFAULT,
|
||||
order = order++,
|
||||
source = SOURCE_DEFAULT
|
||||
)
|
||||
}
|
||||
addAll(typeList)
|
||||
return typeList
|
||||
}
|
||||
|
@ -84,6 +84,8 @@ abstract class TimetableDao : BaseDao<Lesson, LessonFull> {
|
||||
"LIMIT 1")
|
||||
fun getBetweenDates(dateFrom: Date, dateTo: Date) =
|
||||
getRaw("$QUERY WHERE (type != 3 AND date >= '${dateFrom.stringY_m_d}' AND date <= '${dateTo.stringY_m_d}') OR ((type = 3 OR type = 1) AND oldDate >= '${dateFrom.stringY_m_d}' AND oldDate <= '${dateTo.stringY_m_d}') $ORDER_BY")
|
||||
fun getChanges(profileId: Int) =
|
||||
getRaw("$QUERY WHERE timetable.profileId = $profileId AND $IS_CHANGED $ORDER_BY")
|
||||
|
||||
// GET ALL - NOW
|
||||
fun getAllNow(profileId: Int) =
|
||||
|
@ -45,6 +45,7 @@ open class Event(
|
||||
var addedDate: Long = System.currentTimeMillis()
|
||||
) : Keepable() {
|
||||
companion object {
|
||||
const val TYPE_ELEARNING = -5L
|
||||
const val TYPE_UNDEFINED = -2L
|
||||
const val TYPE_HOMEWORK = -1L
|
||||
const val TYPE_DEFAULT = 0L
|
||||
@ -57,7 +58,7 @@ open class Event(
|
||||
const val TYPE_READING = 7L
|
||||
const val TYPE_CLASS_EVENT = 8L
|
||||
const val TYPE_INFORMATION = 9L
|
||||
const val TYPE_TEACHER_ABSENCE = 10L
|
||||
const val COLOR_ELEARNING = 0xfff57f17.toInt()
|
||||
const val COLOR_HOMEWORK = 0xff795548.toInt()
|
||||
const val COLOR_DEFAULT = 0xffffc107.toInt()
|
||||
const val COLOR_EXAM = 0xfff44336.toInt()
|
||||
@ -69,7 +70,6 @@ open class Event(
|
||||
const val COLOR_READING = 0xFFFFEB3B.toInt()
|
||||
const val COLOR_CLASS_EVENT = 0xff388e3c.toInt()
|
||||
const val COLOR_INFORMATION = 0xff039be5.toInt()
|
||||
const val COLOR_TEACHER_ABSENCE = 0xff039be5.toInt()
|
||||
}
|
||||
|
||||
@ColumnInfo(name = "eventAddedManually")
|
||||
@ -116,14 +116,7 @@ open class Event(
|
||||
var showAsUnseen: Boolean? = null
|
||||
|
||||
val startTimeCalendar: Calendar
|
||||
get() = Calendar.getInstance().also { it.set(
|
||||
date.year,
|
||||
date.month - 1,
|
||||
date.day,
|
||||
time?.hour ?: 0,
|
||||
time?.minute ?: 0,
|
||||
time?.second ?: 0
|
||||
) }
|
||||
get() = date.getAsCalendar(time)
|
||||
|
||||
val endTimeCalendar: Calendar
|
||||
get() = startTimeCalendar.also {
|
||||
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Kacper Ziubryniewicz 2020-1-6
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.db.entity;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
|
||||
@Entity(tableName = "eventTypes",
|
||||
primaryKeys = {"profileId", "eventType"})
|
||||
public class EventType {
|
||||
public int profileId;
|
||||
|
||||
@ColumnInfo(name = "eventType")
|
||||
public long id;
|
||||
|
||||
@ColumnInfo(name = "eventTypeName")
|
||||
public String name;
|
||||
@ColumnInfo(name = "eventTypeColor")
|
||||
public int color;
|
||||
|
||||
public EventType(int profileId, long id, String name, int color) {
|
||||
this.profileId = profileId;
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public EventType(int profileId, int id, String name, String color) {
|
||||
this(profileId, id, name, Color.parseColor(color));
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-19.
|
||||
*/
|
||||
package pl.szczodrzynski.edziennik.data.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ
|
||||
|
||||
@Entity(
|
||||
tableName = "eventTypes",
|
||||
primaryKeys = ["profileId", "eventType"]
|
||||
)
|
||||
class EventType(
|
||||
val profileId: Int,
|
||||
|
||||
@ColumnInfo(name = "eventType")
|
||||
val id: Long,
|
||||
|
||||
@ColumnInfo(name = "eventTypeName")
|
||||
val name: String,
|
||||
@ColumnInfo(name = "eventTypeColor")
|
||||
val color: Int,
|
||||
@ColumnInfo(name = "eventTypeOrder")
|
||||
var order: Int = id.toInt(),
|
||||
@ColumnInfo(name = "eventTypeSource")
|
||||
val source: Int = SOURCE_REGISTER
|
||||
) {
|
||||
companion object {
|
||||
const val SOURCE_DEFAULT = 0
|
||||
const val SOURCE_REGISTER = 1
|
||||
const val SOURCE_CUSTOM = 2
|
||||
const val SOURCE_SHARED = 3
|
||||
|
||||
fun getTypeColorMap() = mapOf(
|
||||
TYPE_ELEARNING to COLOR_ELEARNING,
|
||||
TYPE_HOMEWORK to COLOR_HOMEWORK,
|
||||
TYPE_DEFAULT to COLOR_DEFAULT,
|
||||
TYPE_EXAM to COLOR_EXAM,
|
||||
TYPE_SHORT_QUIZ to COLOR_SHORT_QUIZ,
|
||||
TYPE_ESSAY to COLOR_ESSAY,
|
||||
TYPE_PROJECT to COLOR_PROJECT,
|
||||
TYPE_PT_MEETING to COLOR_PT_MEETING,
|
||||
TYPE_EXCURSION to COLOR_EXCURSION,
|
||||
TYPE_READING to COLOR_READING,
|
||||
TYPE_CLASS_EVENT to COLOR_CLASS_EVENT,
|
||||
TYPE_INFORMATION to COLOR_INFORMATION
|
||||
)
|
||||
|
||||
fun getTypeNameMap() = mapOf(
|
||||
TYPE_ELEARNING to R.string.event_type_elearning,
|
||||
TYPE_HOMEWORK to R.string.event_type_homework,
|
||||
TYPE_DEFAULT to R.string.event_other,
|
||||
TYPE_EXAM to R.string.event_exam,
|
||||
TYPE_SHORT_QUIZ to R.string.event_short_quiz,
|
||||
TYPE_ESSAY to R.string.event_essay,
|
||||
TYPE_PROJECT to R.string.event_project,
|
||||
TYPE_PT_MEETING to R.string.event_pt_meeting,
|
||||
TYPE_EXCURSION to R.string.event_excursion,
|
||||
TYPE_READING to R.string.event_reading,
|
||||
TYPE_CLASS_EVENT to R.string.event_class_event,
|
||||
TYPE_INFORMATION to R.string.event_information
|
||||
)
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ import android.content.Intent
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.google.gson.JsonObject
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import pl.szczodrzynski.edziennik.MainActivity
|
||||
|
||||
@Entity(tableName = "notifications")
|
||||
@ -19,6 +21,7 @@ data class Notification(
|
||||
|
||||
val title: String,
|
||||
val text: String,
|
||||
val textLong: String? = null,
|
||||
|
||||
val type: Int,
|
||||
|
||||
@ -96,4 +99,19 @@ data class Notification(
|
||||
fillIntent(intent)
|
||||
return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
}
|
||||
|
||||
fun getLargeIcon(): IIcon = when (type) {
|
||||
TYPE_TIMETABLE_LESSON_CHANGE -> CommunityMaterial.Icon3.cmd_timetable
|
||||
TYPE_NEW_GRADE -> CommunityMaterial.Icon3.cmd_numeric_5_box_outline
|
||||
TYPE_NEW_EVENT -> CommunityMaterial.Icon.cmd_calendar_outline
|
||||
TYPE_NEW_HOMEWORK -> CommunityMaterial.Icon3.cmd_notebook_outline
|
||||
TYPE_NEW_SHARED_EVENT -> CommunityMaterial.Icon.cmd_calendar_outline
|
||||
TYPE_NEW_SHARED_HOMEWORK -> CommunityMaterial.Icon3.cmd_notebook_outline
|
||||
TYPE_NEW_MESSAGE -> CommunityMaterial.Icon.cmd_email_outline
|
||||
TYPE_NEW_NOTICE -> CommunityMaterial.Icon.cmd_emoticon_outline
|
||||
TYPE_NEW_ATTENDANCE -> CommunityMaterial.Icon.cmd_calendar_remove_outline
|
||||
TYPE_LUCKY_NUMBER -> CommunityMaterial.Icon.cmd_emoticon_excited_outline
|
||||
TYPE_NEW_ANNOUNCEMENT -> CommunityMaterial.Icon.cmd_bullhorn_outline
|
||||
else -> CommunityMaterial.Icon.cmd_bell_ring_outline
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,9 @@ open class Profile(
|
||||
val isParent
|
||||
get() = accountName != null
|
||||
|
||||
val accountOwnerName
|
||||
get() = accountName ?: studentNameLong
|
||||
|
||||
val registerName
|
||||
get() = when (loginStoreType) {
|
||||
LOGIN_TYPE_LIBRUS -> "librus"
|
||||
@ -137,7 +140,7 @@ open class Profile(
|
||||
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
|
||||
LOGIN_TYPE_PODLASIE -> "podlasie"
|
||||
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
|
||||
else -> null
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
override fun getImageDrawable(context: Context): Drawable {
|
||||
|
@ -30,7 +30,7 @@ class MessageFull(
|
||||
@Ignore
|
||||
var filterWeight = 0
|
||||
@Ignore
|
||||
var searchHighlightText: String? = null
|
||||
var searchHighlightText: CharSequence? = null
|
||||
|
||||
// metadata
|
||||
var seen = false
|
||||
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-15.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.db.migration
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_REGISTER
|
||||
import pl.szczodrzynski.edziennik.getInt
|
||||
|
||||
class Migration92 : Migration(91, 92) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// make eventTypeName not nullable
|
||||
database.execSQL("ALTER TABLE eventTypes RENAME TO _eventTypes;")
|
||||
database.execSQL("CREATE TABLE eventTypes (" +
|
||||
"profileId INTEGER NOT NULL, " +
|
||||
"eventType INTEGER NOT NULL, " +
|
||||
"eventTypeName TEXT NOT NULL, " +
|
||||
"eventTypeColor INTEGER NOT NULL, " +
|
||||
"PRIMARY KEY(profileId,eventType)" +
|
||||
");")
|
||||
database.execSQL("INSERT INTO eventTypes " +
|
||||
"(profileId, eventType, eventTypeName, eventTypeColor) " +
|
||||
"SELECT profileId, eventType, eventTypeName, eventTypeColor " +
|
||||
"FROM _eventTypes;")
|
||||
database.execSQL("DROP TABLE _eventTypes;")
|
||||
|
||||
// add columns for order and source
|
||||
database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeOrder INTEGER NOT NULL DEFAULT 0;")
|
||||
database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeSource INTEGER NOT NULL DEFAULT 0;")
|
||||
|
||||
// migrate existing types to show correct order and source
|
||||
database.execSQL("UPDATE eventTypes SET eventTypeOrder = eventType + 102;")
|
||||
database.execSQL("UPDATE eventTypes SET eventTypeSource = $SOURCE_REGISTER WHERE eventType > $TYPE_INFORMATION;")
|
||||
|
||||
// add new e-learning type
|
||||
val cursor = database.query("SELECT profileId FROM profiles;")
|
||||
cursor.use {
|
||||
while (it.moveToNext()) {
|
||||
val values = ContentValues().apply {
|
||||
put("profileId", it.getInt("profileId"))
|
||||
put("eventType", TYPE_ELEARNING)
|
||||
put("eventTypeName", "lekcja online")
|
||||
put("eventTypeColor", COLOR_ELEARNING)
|
||||
put("eventTypeOrder", 100)
|
||||
put("eventTypeSource", SOURCE_DEFAULT)
|
||||
}
|
||||
|
||||
database.insert("eventTypes", SQLiteDatabase.CONFLICT_REPLACE, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-5-26.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.data.db.migration
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration93 : Migration(92, 93) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// notifications - long text
|
||||
database.execSQL("ALTER TABLE notifications ADD COLUMN textLong TEXT DEFAULT NULL;")
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
|
||||
) ?: return@launch
|
||||
app.config.sync.registerAvailability = data
|
||||
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
|
||||
EventBus.getDefault().postSticky(RegisterAvailabilityEvent(data))
|
||||
EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.dialogs
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.MainActivity
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.MessagesConfigDialogBinding
|
||||
|
||||
class MessagesConfigDialog(
|
||||
private val activity: AppCompatActivity,
|
||||
private val reloadOnDismiss: Boolean = true,
|
||||
private val onShowListener: ((tag: String) -> Unit)? = null,
|
||||
private val onDismissListener: ((tag: String) -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
const val TAG = "MessagesConfigDialog"
|
||||
}
|
||||
|
||||
private val app by lazy { activity.application as App }
|
||||
private val config by lazy { app.config.ui }
|
||||
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||
|
||||
private lateinit var b: MessagesConfigDialogBinding
|
||||
private lateinit var dialog: AlertDialog
|
||||
|
||||
init { run {
|
||||
if (activity.isFinishing)
|
||||
return@run
|
||||
b = MessagesConfigDialogBinding.inflate(activity.layoutInflater)
|
||||
onShowListener?.invoke(TAG)
|
||||
dialog = MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.menu_messages_config)
|
||||
.setView(b.root)
|
||||
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
.setOnDismissListener {
|
||||
saveConfig()
|
||||
onDismissListener?.invoke(TAG)
|
||||
if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget()
|
||||
}
|
||||
.create()
|
||||
loadConfig()
|
||||
dialog.show()
|
||||
}}
|
||||
|
||||
private fun loadConfig() {
|
||||
b.config = profileConfig
|
||||
|
||||
b.greetingText.setText(
|
||||
profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveConfig() {
|
||||
val greetingText = b.greetingText.text?.toString()?.trim()
|
||||
if (greetingText.isNullOrEmpty())
|
||||
profileConfig.messagesGreetingText = null
|
||||
else
|
||||
profileConfig.messagesGreetingText = "\n\n$greetingText"
|
||||
}
|
||||
}
|
@ -42,8 +42,6 @@ class RegisterUnavailableDialog(
|
||||
init { run {
|
||||
if (activity.isFinishing)
|
||||
return@run
|
||||
if (status.available && status.minVersionCode <= BuildConfig.VERSION_CODE)
|
||||
return@run
|
||||
onShowListener?.invoke(TAG)
|
||||
app = activity.applicationContext as App
|
||||
|
||||
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-10.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.dialogs.agenda
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED
|
||||
import pl.szczodrzynski.edziennik.databinding.DialogConfigAgendaBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog
|
||||
import java.util.*
|
||||
|
||||
class AgendaConfigDialog(
|
||||
private val activity: AppCompatActivity,
|
||||
private val reloadOnDismiss: Boolean = true,
|
||||
private val onShowListener: ((tag: String) -> Unit)? = null,
|
||||
private val onDismissListener: ((tag: String) -> Unit)? = null
|
||||
) {
|
||||
companion object {
|
||||
const val TAG = "AgendaConfigDialog"
|
||||
}
|
||||
|
||||
private val app by lazy { activity.application as App }
|
||||
private val config by lazy { app.config.ui }
|
||||
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||
|
||||
private lateinit var b: DialogConfigAgendaBinding
|
||||
private lateinit var dialog: AlertDialog
|
||||
|
||||
init { run {
|
||||
if (activity.isFinishing)
|
||||
return@run
|
||||
b = DialogConfigAgendaBinding.inflate(activity.layoutInflater)
|
||||
onShowListener?.invoke(TAG)
|
||||
dialog = MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.menu_agenda_config)
|
||||
.setView(b.root)
|
||||
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
.setOnDismissListener {
|
||||
saveConfig()
|
||||
onDismissListener?.invoke(TAG)
|
||||
if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget()
|
||||
}
|
||||
.create()
|
||||
loadConfig()
|
||||
dialog.show()
|
||||
}}
|
||||
|
||||
private fun loadConfig() {
|
||||
b.config = profileConfig
|
||||
b.isAgendaMode = profileConfig.agendaViewType == Profile.AGENDA_DEFAULT
|
||||
|
||||
b.eventSharingEnabled.isChecked = app.profile.enableSharedEvents
|
||||
&& app.profile.registration == REGISTRATION_ENABLED
|
||||
b.eventSharingEnabled.onChange { _, isChecked ->
|
||||
if (isChecked && app.profile.registration != REGISTRATION_ENABLED) {
|
||||
b.eventSharingEnabled.isChecked = false
|
||||
val dialog = RegistrationConfigDialog(activity, app.profile, onChangeListener = { enabled ->
|
||||
b.eventSharingEnabled.isChecked = enabled
|
||||
setEventSharingEnabled(enabled)
|
||||
}, onShowListener, onDismissListener)
|
||||
dialog.showEnableDialog()
|
||||
return@onChange
|
||||
}
|
||||
setEventSharingEnabled(isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEventSharingEnabled(enabled: Boolean) {
|
||||
if (enabled == app.profile.enableSharedEvents)
|
||||
return
|
||||
app.profile.enableSharedEvents = enabled
|
||||
app.profileSave()
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.event_sharing)
|
||||
.setMessage(
|
||||
if (enabled)
|
||||
R.string.settings_register_shared_events_dialog_enabled_text
|
||||
else
|
||||
R.string.settings_register_shared_events_dialog_disabled_text
|
||||
)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveConfig() {
|
||||
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.Job
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.dp
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class ChangelogDialog(
|
||||
@ -43,9 +44,14 @@ class ChangelogDialog(
|
||||
val textView = TextView(activity)
|
||||
textView.setPadding(24.dp, 24.dp, 24.dp, 0)
|
||||
|
||||
val text = app.assets.open("pl-changelog.html").bufferedReader().use {
|
||||
var text = app.assets.open("pl-changelog.html").bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
|
||||
val commitsUrlPrefix = "https://github.com/szkolny-eu/szkolny-android/commits?author="
|
||||
text = text.replace("""\[(.+?)]\(@([A-z0-9-]+)\)""".toRegex(), "<a href=\"$commitsUrlPrefix$2\">$1</a>")
|
||||
text = text.replace("""\s@([A-z0-9-]+)""".toRegex(), " <a href=\"$commitsUrlPrefix$1\">@$1</a>")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
textView.text = Html.fromHtml(text)
|
||||
}
|
||||
@ -53,6 +59,8 @@ class ChangelogDialog(
|
||||
textView.text = Html.fromHtml(text.replace("<li>", "<br><li> - "))
|
||||
}
|
||||
|
||||
textView.movementMethod = BetterLinkMovementMethod.getInstance()
|
||||
|
||||
val scrollView = ScrollView(activity)
|
||||
scrollView.addView(textView)
|
||||
|
||||
@ -67,4 +75,4 @@ class ChangelogDialog(
|
||||
}
|
||||
.show()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ package pl.szczodrzynski.edziennik.ui.dialogs.day
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.*
|
||||
@ -19,6 +19,10 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
|
||||
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
@ -29,6 +33,7 @@ class DayDialog(
|
||||
val activity: AppCompatActivity,
|
||||
val profileId: Int,
|
||||
val date: Date,
|
||||
val eventTypeId: Long? = null,
|
||||
val onShowListener: ((tag: String) -> Unit)? = null,
|
||||
val onDismissListener: ((tag: String) -> Unit)? = null
|
||||
) : CoroutineScope {
|
||||
@ -109,38 +114,51 @@ class DayDialog(
|
||||
}
|
||||
|
||||
lessonChanges.ifNotEmpty {
|
||||
b.lessonChangeContainer.root.visibility = View.VISIBLE
|
||||
b.lessonChangeContainer.lessonChangeCount.text = it.size.toString()
|
||||
LessonChangesEventRenderer().render(
|
||||
b.lessonChanges, LessonChangesEvent(
|
||||
profileId = profileId,
|
||||
date = date,
|
||||
count = it.size,
|
||||
showBadge = false
|
||||
)
|
||||
)
|
||||
|
||||
b.lessonChangeLayout.onClick {
|
||||
b.lessonChangesFrame.onClick {
|
||||
LessonChangeDialog(
|
||||
activity,
|
||||
profileId,
|
||||
date,
|
||||
onShowListener = onShowListener,
|
||||
onDismissListener = onDismissListener
|
||||
activity,
|
||||
profileId,
|
||||
date,
|
||||
onShowListener = onShowListener,
|
||||
onDismissListener = onDismissListener
|
||||
)
|
||||
}
|
||||
}
|
||||
b.lessonChangesFrame.isVisible = lessonChanges.isNotEmpty()
|
||||
|
||||
val teacherAbsences = withContext(Dispatchers.Default) {
|
||||
app.db.teacherAbsenceDao().getAllByDateNow(profileId, date)
|
||||
}
|
||||
|
||||
teacherAbsences.ifNotEmpty {
|
||||
b.teacherAbsenceContainer.root.visibility = View.VISIBLE
|
||||
b.teacherAbsenceContainer.teacherAbsenceCount.text = it.size.toString()
|
||||
TeacherAbsenceEventRenderer().render(
|
||||
b.teacherAbsence, TeacherAbsenceEvent(
|
||||
profileId = profileId,
|
||||
date = date,
|
||||
count = it.size
|
||||
)
|
||||
)
|
||||
|
||||
b.teacherAbsenceLayout.onClick {
|
||||
b.teacherAbsenceFrame.onClick {
|
||||
TeacherAbsenceDialog(
|
||||
activity,
|
||||
profileId,
|
||||
date,
|
||||
onShowListener = onShowListener,
|
||||
onDismissListener = onDismissListener
|
||||
activity,
|
||||
profileId,
|
||||
date,
|
||||
onShowListener = onShowListener,
|
||||
onDismissListener = onDismissListener
|
||||
)
|
||||
}
|
||||
}
|
||||
b.teacherAbsenceFrame.isVisible = teacherAbsences.isNotEmpty()
|
||||
|
||||
adapter = EventListAdapter(
|
||||
activity,
|
||||
@ -169,8 +187,12 @@ class DayDialog(
|
||||
}
|
||||
)
|
||||
|
||||
app.db.eventDao().getAllByDate(profileId, date).observe(activity, Observer { events ->
|
||||
adapter.items = events
|
||||
app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events ->
|
||||
adapter.items = if (eventTypeId != null)
|
||||
events.filter { it.type == eventTypeId }
|
||||
else
|
||||
events
|
||||
|
||||
if (b.eventsView.adapter == null) {
|
||||
b.eventsView.adapter = adapter
|
||||
b.eventsView.apply {
|
||||
@ -189,6 +211,6 @@ class DayDialog(
|
||||
b.eventsView.visibility = View.GONE
|
||||
b.eventsNoData.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class EventDetailsDialog(
|
||||
val activity: AppCompatActivity,
|
||||
val event: EventFull,
|
||||
var event: EventFull,
|
||||
val onShowListener: ((tag: String) -> Unit)? = null,
|
||||
val onDismissListener: ((tag: String) -> Unit)? = null
|
||||
) : CoroutineScope {
|
||||
@ -46,6 +46,8 @@ class EventDetailsDialog(
|
||||
private var removeEventDialog: AlertDialog? = null
|
||||
private val eventShared = event.sharedBy != null
|
||||
private val eventOwn = event.sharedBy == "self"
|
||||
private val manager
|
||||
get() = app.eventManager
|
||||
|
||||
private val job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
@ -92,6 +94,10 @@ class EventDetailsDialog(
|
||||
b.eventShared = eventShared
|
||||
b.eventOwn = eventOwn
|
||||
|
||||
if (!event.seen) {
|
||||
manager.markAsSeen(event)
|
||||
}
|
||||
|
||||
val bullet = " • "
|
||||
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
|
||||
|
||||
@ -100,6 +106,8 @@ class EventDetailsDialog(
|
||||
}
|
||||
catch (_: Exception) {}
|
||||
|
||||
manager.setLegendText(b.legend, event)
|
||||
|
||||
b.typeColor.background?.setTintColor(event.eventColor)
|
||||
|
||||
b.details = mutableListOf(
|
||||
@ -135,6 +143,7 @@ class EventDetailsDialog(
|
||||
launch(Dispatchers.Default) {
|
||||
app.db.eventDao().replace(event)
|
||||
}
|
||||
update()
|
||||
b.checkDoneButton.isChecked = true
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
@ -145,6 +154,7 @@ class EventDetailsDialog(
|
||||
launch(Dispatchers.Default) {
|
||||
app.db.eventDao().replace(event)
|
||||
}
|
||||
update()
|
||||
}
|
||||
}
|
||||
b.checkDoneButton.attachToastHint(R.string.hint_mark_as_done)
|
||||
@ -156,6 +166,14 @@ class EventDetailsDialog(
|
||||
activity,
|
||||
event.profileId,
|
||||
editingEvent = event,
|
||||
onSaveListener = {
|
||||
if (it == null) {
|
||||
dialog.dismiss()
|
||||
return@EventManualDialog
|
||||
}
|
||||
event = it
|
||||
update()
|
||||
},
|
||||
onShowListener = onShowListener,
|
||||
onDismissListener = onDismissListener
|
||||
)
|
||||
@ -199,10 +217,14 @@ class EventDetailsDialog(
|
||||
}
|
||||
b.downloadButton.attachToastHint(R.string.hint_download_again)
|
||||
|
||||
BetterLink.attach(b.topic, onActionSelected = dialog::dismiss)
|
||||
|
||||
b.topic.text = event.topic
|
||||
BetterLink.attach(b.topic) {
|
||||
dialog.dismiss()
|
||||
event.teacherName?.let { name ->
|
||||
BetterLink.attach(
|
||||
b.teacherName,
|
||||
teachers = mapOf(event.teacherId to name),
|
||||
onActionSelected = dialog::dismiss
|
||||
)
|
||||
}
|
||||
|
||||
if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) {
|
||||
@ -220,10 +242,7 @@ class EventDetailsDialog(
|
||||
b.bodyTitle.isVisible = true
|
||||
b.bodyProgressBar.isVisible = false
|
||||
b.body.isVisible = true
|
||||
b.body.text = event.homeworkBody
|
||||
BetterLink.attach(b.body) {
|
||||
dialog.dismiss()
|
||||
}
|
||||
BetterLink.attach(b.body, onActionSelected = dialog::dismiss)
|
||||
}
|
||||
|
||||
if (event.attachmentIds.isNullOrEmpty() || event.attachmentNames.isNullOrEmpty()) {
|
||||
@ -322,8 +341,6 @@ class EventDetailsDialog(
|
||||
removeEventDialog?.dismiss()
|
||||
dialog.dismiss()
|
||||
Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show()
|
||||
if (activity is MainActivity && activity.navTargetId == MainActivity.DRAWER_ITEM_AGENDA)
|
||||
activity.reloadTarget()
|
||||
}
|
||||
|
||||
private fun openInCalendar() { launch {
|
||||
|
@ -33,7 +33,8 @@ class EventListAdapter(
|
||||
) : RecyclerView.Adapter<EventListAdapter.ViewHolder>(), CoroutineScope {
|
||||
|
||||
private val app = context.applicationContext as App
|
||||
private val manager = app.eventManager
|
||||
private val manager
|
||||
get() = app.eventManager
|
||||
|
||||
private val job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
@ -67,7 +68,7 @@ class EventListAdapter(
|
||||
|
||||
b.simpleMode = simpleMode
|
||||
|
||||
b.topic.text = event.topic
|
||||
manager.setEventTopic(b.topic, event, showType = false)
|
||||
b.topic.maxLines = if (simpleMode) 2 else 3
|
||||
|
||||
b.details.text = mutableListOf<CharSequence?>(
|
||||
@ -102,8 +103,6 @@ class EventListAdapter(
|
||||
}
|
||||
b.editButton.attachToastHint(R.string.hint_edit_event)
|
||||
|
||||
b.isDone.isVisible = event.isDone
|
||||
|
||||
if (event.showAsUnseen == null)
|
||||
event.showAsUnseen = !event.seen
|
||||
|
||||
|
@ -4,8 +4,6 @@
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.dialogs.event
|
||||
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
@ -20,23 +18,18 @@ import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA
|
||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
|
||||
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
|
||||
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent
|
||||
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Event
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.*
|
||||
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
||||
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS
|
||||
import pl.szczodrzynski.edziennik.utils.Anim
|
||||
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@ -49,6 +42,7 @@ class EventManualDialog(
|
||||
val defaultTime: Time? = null,
|
||||
val defaultType: Long? = null,
|
||||
val editingEvent: EventFull? = null,
|
||||
val onSaveListener: ((event: EventFull?) -> Unit)? = null,
|
||||
val onShowListener: ((tag: String) -> Unit)? = null,
|
||||
val onDismissListener: ((tag: String) -> Unit)? = null
|
||||
) : CoroutineScope {
|
||||
@ -323,57 +317,41 @@ class EventManualDialog(
|
||||
selectDefault(defaultLesson?.displayTeacherId)
|
||||
}
|
||||
|
||||
with (b.typeDropdown) {
|
||||
db = app.db
|
||||
profileId = this@EventManualDialog.profileId
|
||||
loadItems()
|
||||
selectDefault(editingEvent?.type)
|
||||
selectDefault(defaultType)
|
||||
|
||||
val deferred = async(Dispatchers.Default) {
|
||||
// get the event type list
|
||||
var eventTypes = app.db.eventTypeDao().getAllNow(profileId)
|
||||
|
||||
if (eventTypes.none { it.id in -1L..10L }) {
|
||||
eventTypes = app.db.eventTypeDao().addDefaultTypes(activity, profileId)
|
||||
onTypeSelected = {
|
||||
b.typeColor.background.setTintColor(it.color)
|
||||
customColor = null
|
||||
}
|
||||
|
||||
b.typeDropdown.clear()
|
||||
b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) }
|
||||
}
|
||||
deferred.await()
|
||||
|
||||
b.typeDropdown.isEnabled = true
|
||||
|
||||
defaultType?.let {
|
||||
b.typeDropdown.select(it)
|
||||
}
|
||||
|
||||
b.typeDropdown.selected?.let { item ->
|
||||
customColor = (item.tag as EventType).color
|
||||
}
|
||||
|
||||
// copy IDs from event being edited
|
||||
// copy data from event being edited
|
||||
editingEvent?.let {
|
||||
b.topic.setText(it.topic)
|
||||
b.typeDropdown.select(it.type)?.let { item ->
|
||||
customColor = (item.tag as EventType).color
|
||||
}
|
||||
if (it.color != null && it.color != -1)
|
||||
if (it.color != -1)
|
||||
customColor = it.color
|
||||
}
|
||||
|
||||
b.typeColor.background.setTintColor(
|
||||
customColor
|
||||
?: b.typeDropdown.getSelected()?.color
|
||||
?: Event.COLOR_DEFAULT
|
||||
)
|
||||
|
||||
// copy IDs from the LessonFull
|
||||
defaultLesson?.let {
|
||||
b.teamDropdown.select(it.displayTeamId)
|
||||
}
|
||||
|
||||
b.typeDropdown.setOnChangeListener {
|
||||
b.typeColor.background.colorFilter = PorterDuffColorFilter((it.tag as EventType).color, PorterDuff.Mode.SRC_ATOP)
|
||||
customColor = null
|
||||
return@setOnChangeListener true
|
||||
}
|
||||
|
||||
(customColor ?: Event.COLOR_DEFAULT).let {
|
||||
b.typeColor.background.colorFilter = PorterDuffColorFilter(it, PorterDuff.Mode.SRC_ATOP)
|
||||
}
|
||||
|
||||
b.typeColor.onClick {
|
||||
val currentColor = (b.typeDropdown.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT
|
||||
val currentColor = customColor
|
||||
?: b.typeDropdown.getSelected()?.color
|
||||
?: Event.COLOR_DEFAULT
|
||||
val colorPickerDialog = ColorPickerDialog.newBuilder()
|
||||
.setColor(currentColor)
|
||||
.create()
|
||||
@ -381,7 +359,7 @@ class EventManualDialog(
|
||||
object : ColorPickerDialogListener {
|
||||
override fun onDialogDismissed(dialogId: Int) {}
|
||||
override fun onColorSelected(dialogId: Int, color: Int) {
|
||||
b.typeColor.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
b.typeColor.background.setTintColor(color)
|
||||
customColor = color
|
||||
}
|
||||
})
|
||||
@ -416,11 +394,11 @@ class EventManualDialog(
|
||||
private fun saveEvent() {
|
||||
val date = b.dateDropdown.getSelected() as? Date
|
||||
val timeSelected = b.timeDropdown.getSelected()
|
||||
val teamId = b.teamDropdown.getSelected() as? Long
|
||||
val type = b.typeDropdown.selected?.id
|
||||
val team = b.teamDropdown.getSelected()
|
||||
val type = b.typeDropdown.getSelected()
|
||||
val topic = b.topic.text?.toString()
|
||||
val subjectId = b.subjectDropdown.getSelected() as? Long
|
||||
val teacherId = b.teacherDropdown.getSelected()
|
||||
val subject = b.subjectDropdown.getSelected() as? Subject
|
||||
val teacher = b.teacherDropdown.getSelected()
|
||||
|
||||
val share = b.shareSwitch.isChecked
|
||||
|
||||
@ -451,7 +429,7 @@ class EventManualDialog(
|
||||
isError = true
|
||||
}
|
||||
|
||||
if (share && teamId == null) {
|
||||
if (share && team == null) {
|
||||
b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose)
|
||||
if (!isError) b.teamDropdown.parent.requestChildFocus(b.teamDropdown, b.teamDropdown)
|
||||
isError = true
|
||||
@ -487,10 +465,10 @@ class EventManualDialog(
|
||||
time = startTime,
|
||||
topic = topic,
|
||||
color = customColor,
|
||||
type = type ?: Event.TYPE_DEFAULT,
|
||||
teacherId = teacherId ?: -1,
|
||||
subjectId = subjectId ?: -1,
|
||||
teamId = teamId ?: -1,
|
||||
type = type?.id ?: Event.TYPE_DEFAULT,
|
||||
teacherId = teacher?.id ?: -1,
|
||||
subjectId = subject?.id ?: -1,
|
||||
teamId = team?.id ?: -1,
|
||||
addedDate = editingEvent?.addedDate ?: System.currentTimeMillis()
|
||||
).also {
|
||||
it.addedManually = true
|
||||
@ -498,7 +476,7 @@ class EventManualDialog(
|
||||
|
||||
val metadataObject = Metadata(
|
||||
profileId,
|
||||
when (type) {
|
||||
when (type?.id) {
|
||||
Event.TYPE_HOMEWORK -> Metadata.TYPE_HOMEWORK
|
||||
else -> Metadata.TYPE_EVENT
|
||||
},
|
||||
@ -597,10 +575,14 @@ class EventManualDialog(
|
||||
}
|
||||
}
|
||||
|
||||
onSaveListener?.invoke(eventObject.withMetadata(metadataObject).also {
|
||||
it.subjectLongName = (b.subjectDropdown.getSelected() as? Subject)?.longName
|
||||
it.teacherName = b.teacherDropdown.getSelected()?.fullName
|
||||
it.teamName = b.teamDropdown.getSelected()?.name
|
||||
it.typeName = b.typeDropdown.getSelected()?.name
|
||||
})
|
||||
dialog.dismiss()
|
||||
Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show()
|
||||
if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA)
|
||||
activity.reloadTarget()
|
||||
}
|
||||
private fun finishRemoving() {
|
||||
editingEvent ?: return
|
||||
@ -611,9 +593,8 @@ class EventManualDialog(
|
||||
}
|
||||
|
||||
removeEventDialog?.dismiss()
|
||||
onSaveListener?.invoke(null)
|
||||
dialog.dismiss()
|
||||
Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show()
|
||||
if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA)
|
||||
activity.reloadTarget()
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding
|
||||
import pl.szczodrzynski.edziennik.onClick
|
||||
import pl.szczodrzynski.edziennik.setTintColor
|
||||
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLink
|
||||
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@ -68,6 +69,14 @@ class GradeDetailsDialog(
|
||||
GradesConfigDialog(activity, reloadOnDismiss = true)
|
||||
}
|
||||
|
||||
grade.teacherName?.let { name ->
|
||||
BetterLink.attach(
|
||||
b.teacherName,
|
||||
teachers = mapOf(grade.teacherId to name),
|
||||
onActionSelected = dialog::dismiss
|
||||
)
|
||||
}
|
||||
|
||||
launch {
|
||||
val historyList = withContext(Dispatchers.Default) {
|
||||
app.db.gradeDao().getByParentIdNow(App.profileId, grade.id)
|
||||
|
@ -4,11 +4,14 @@
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.dialogs.timetable
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.graphics.*
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
@ -373,25 +376,31 @@ class GenerateBlockTimetableDialog(
|
||||
|
||||
val today = Date.getToday().stringY_m_d
|
||||
val now = Time.getNow().stringH_M_S
|
||||
val filename = "plan_lekcji_${app.profile.name}_${today}_${now}.png"
|
||||
val resolver: ContentResolver = activity.applicationContext.contentResolver
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
|
||||
|
||||
val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() }
|
||||
val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
|
||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, File(Environment.DIRECTORY_PICTURES, "Szkolny.eu").path)
|
||||
} else {
|
||||
val picturesDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Szkolny.eu")
|
||||
picturesDirectory.mkdirs()
|
||||
values.put(MediaStore.MediaColumns.DATA, File(picturesDirectory, filename).path)
|
||||
}
|
||||
|
||||
try {
|
||||
val fos = FileOutputStream(outputFile)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.close()
|
||||
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null
|
||||
resolver.openOutputStream(uri).use {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
|
||||
}
|
||||
uri
|
||||
} catch (e: Exception) {
|
||||
Log.e("SAVE_IMAGE", e.message, e)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile)
|
||||
} else {
|
||||
Uri.parse("file://" + outputFile.absolutePath)
|
||||
}
|
||||
uri
|
||||
}
|
||||
|
||||
progressDialog.dismiss()
|
||||
|
@ -8,15 +8,20 @@ import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
|
||||
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
|
||||
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
||||
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
|
||||
import pl.szczodrzynski.edziennik.onClick
|
||||
@ -24,7 +29,9 @@ import pl.szczodrzynski.edziennik.setText
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceDetailsDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLink
|
||||
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Week
|
||||
@ -33,6 +40,7 @@ import kotlin.coroutines.CoroutineContext
|
||||
class LessonDetailsDialog(
|
||||
val activity: AppCompatActivity,
|
||||
val lesson: LessonFull,
|
||||
val attendance: AttendanceFull? = null,
|
||||
val onShowListener: ((tag: String) -> Unit)? = null,
|
||||
val onDismissListener: ((tag: String) -> Unit)? = null
|
||||
) : CoroutineScope {
|
||||
@ -49,7 +57,10 @@ class LessonDetailsDialog(
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private lateinit var adapter: EventListAdapter
|
||||
private val manager by lazy { app.timetableManager }
|
||||
private val manager
|
||||
get() = app.timetableManager
|
||||
private val attendanceManager
|
||||
get() = app.attendanceManager
|
||||
|
||||
init { run {
|
||||
if (activity.isFinishing)
|
||||
@ -168,6 +179,27 @@ class LessonDetailsDialog(
|
||||
b.teamName = lesson.teamName
|
||||
}
|
||||
|
||||
b.attendanceDivider.isVisible = attendance != null
|
||||
b.attendanceLayout.isVisible = attendance != null
|
||||
if (attendance != null) {
|
||||
b.attendanceView.setAttendance(attendance, app.attendanceManager, bigView = true)
|
||||
b.attendanceType.text = attendance.typeName
|
||||
b.attendanceIcon.isVisible = attendance.let {
|
||||
val icon = attendanceManager.getAttendanceIcon(it) ?: return@let false
|
||||
val color = attendanceManager.getAttendanceColor(it)
|
||||
b.attendanceIcon.setImageDrawable(
|
||||
IconicsDrawable(activity, icon).apply {
|
||||
colorInt = color
|
||||
sizeDp = 24
|
||||
}
|
||||
)
|
||||
true
|
||||
}
|
||||
b.attendanceDetails.onClick {
|
||||
AttendanceDetailsDialog(activity, attendance, onShowListener, onDismissListener)
|
||||
}
|
||||
}
|
||||
|
||||
adapter = EventListAdapter(
|
||||
activity,
|
||||
showWeekDay = false,
|
||||
@ -216,5 +248,19 @@ class LessonDetailsDialog(
|
||||
b.eventsNoData.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
|
||||
lesson.displayTeacherName?.let { name ->
|
||||
lesson.displayTeacherId ?: return@let
|
||||
BetterLink.attach(
|
||||
b.teacherNameView,
|
||||
teachers = mapOf(lesson.displayTeacherId!! to name),
|
||||
onActionSelected = dialog::dismiss
|
||||
)
|
||||
BetterLink.attach(
|
||||
b.oldTeacherNameView,
|
||||
teachers = mapOf(lesson.displayTeacherId!! to name),
|
||||
onActionSelected = dialog::dismiss
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,6 @@ import android.widget.Toast
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.applandeo.materialcalendarview.EventDay
|
||||
import com.github.tibolte.agendacalendarview.CalendarPickerController
|
||||
import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent
|
||||
import com.github.tibolte.agendacalendarview.models.CalendarEvent
|
||||
import com.github.tibolte.agendacalendarview.models.IDayItem
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
@ -25,21 +21,14 @@ import kotlinx.coroutines.*
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.MainActivity
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile
|
||||
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding
|
||||
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeCounter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEventRenderer
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceCounter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||
@ -59,7 +48,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private var type: Int = Profile.AGENDA_DEFAULT
|
||||
private var actualDate: Date? = null
|
||||
|
||||
private var agendaDefault: AgendaFragmentDefault? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
if (getActivity() == null || context == null) return null
|
||||
@ -82,38 +72,61 @@ class AgendaFragment : Fragment(), CoroutineScope {
|
||||
.withTitle(R.string.menu_add_event)
|
||||
.withDescription(R.string.menu_add_event_desc)
|
||||
.withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline)
|
||||
.withOnClickListener(View.OnClickListener {
|
||||
.withOnClickListener {
|
||||
activity.bottomSheet.close()
|
||||
EventManualDialog(activity, app.profileId, defaultDate = actualDate)
|
||||
}),
|
||||
EventManualDialog(
|
||||
activity,
|
||||
app.profileId,
|
||||
defaultDate = AgendaFragmentDefault.selectedDate
|
||||
)
|
||||
},
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_agenda_config)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||
.withOnClickListener {
|
||||
activity.bottomSheet.close()
|
||||
AgendaConfigDialog(activity, true, null, null)
|
||||
},
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_agenda_change_view)
|
||||
.withIcon(if (type == Profile.AGENDA_DEFAULT) CommunityMaterial.Icon.cmd_calendar_outline else CommunityMaterial.Icon2.cmd_format_list_bulleted_square)
|
||||
.withOnClickListener(View.OnClickListener {
|
||||
.withOnClickListener {
|
||||
activity.bottomSheet.close()
|
||||
type = if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT
|
||||
type =
|
||||
if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT
|
||||
app.config.forProfile().ui.agendaViewType = type
|
||||
activity.reloadTarget()
|
||||
}),
|
||||
},
|
||||
BottomSheetSeparatorItem(true),
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_mark_as_read)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
|
||||
.withOnClickListener(View.OnClickListener { launch {
|
||||
activity.bottomSheet.close()
|
||||
withContext(Dispatchers.Default) {
|
||||
App.db.metadataDao().setAllSeen(app.profileId, Metadata.TYPE_EVENT, true)
|
||||
.withOnClickListener {
|
||||
launch {
|
||||
activity.bottomSheet.close()
|
||||
withContext(Dispatchers.Default) {
|
||||
App.db.metadataDao()
|
||||
.setAllSeen(app.profileId, Metadata.TYPE_EVENT, true)
|
||||
}
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.main_menu_mark_as_read_success,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
|
||||
}})
|
||||
}
|
||||
)
|
||||
|
||||
activity.navView.bottomBar.fabEnable = true
|
||||
activity.navView.bottomBar.fabExtendedText = getString(R.string.add)
|
||||
activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon3.cmd_plus
|
||||
activity.navView.setFabOnClickListener(View.OnClickListener {
|
||||
EventManualDialog(activity, app.profileId, defaultDate = actualDate)
|
||||
})
|
||||
activity.navView.setFabOnClickListener {
|
||||
EventManualDialog(
|
||||
activity,
|
||||
app.profileId,
|
||||
defaultDate = AgendaFragmentDefault.selectedDate
|
||||
)
|
||||
}
|
||||
|
||||
activity.gainAttention()
|
||||
activity.gainAttentionFAB()
|
||||
@ -124,151 +137,30 @@ class AgendaFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkEventTypes() {
|
||||
withContext(Dispatchers.Default) {
|
||||
val eventTypes = app.db.eventTypeDao().getAllNow(app.profileId).map {
|
||||
it.id
|
||||
}
|
||||
val defaultEventTypes = EventType.getTypeColorMap().keys
|
||||
if (!eventTypes.containsAll(defaultEventTypes)) {
|
||||
app.db.eventTypeDao().addDefaultTypes(activity, app.profileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch {
|
||||
if (!isAdded)
|
||||
return@launch
|
||||
checkEventTypes()
|
||||
delay(500)
|
||||
|
||||
val eventList = mutableListOf<CalendarEvent>()
|
||||
|
||||
val minDate = Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -2)
|
||||
set(Calendar.DAY_OF_MONTH, 1)
|
||||
}
|
||||
val maxDate = Calendar.getInstance().apply { add(Calendar.MONTH, 2) }
|
||||
|
||||
/**
|
||||
* LESSON CHANGES
|
||||
*/
|
||||
if (!isAdded)
|
||||
return@launch
|
||||
|
||||
val lessons = withContext(Dispatchers.Default) { app.db.timetableDao().getChangesNow(app.profileId) }
|
||||
val lessonChangeCounters = mutableListOf<LessonChangeCounter>()
|
||||
|
||||
lessons.forEach { lesson ->
|
||||
lessonChangeCounters.firstOrNull { it.lessonChangeDate == lesson.displayDate }?.let {
|
||||
it.lessonChangeCount += 1
|
||||
} ?: run {
|
||||
lessonChangeCounters.add(LessonChangeCounter(
|
||||
lesson.displayDate ?: return@forEach,
|
||||
1
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
lessonChangeCounters.forEach { counter ->
|
||||
eventList.add(LessonChangeEvent(
|
||||
counter.lessonChangeDate.inMillis,
|
||||
0xff78909c.toInt(),
|
||||
Colors.legibleTextColor(0xff78909c.toInt()),
|
||||
counter.startTime,
|
||||
counter.endTime,
|
||||
app.profileId,
|
||||
counter.lessonChangeDate,
|
||||
counter.lessonChangeCount
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* TEACHER ABSENCES
|
||||
*/
|
||||
if (!isAdded)
|
||||
return@launch
|
||||
|
||||
val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true)
|
||||
|
||||
if (showTeacherAbsences) {
|
||||
val teacherAbsenceList = withContext(Dispatchers.Default) { app.db.teacherAbsenceDao().getAllNow(app.profileId) }
|
||||
val teacherAbsenceCounters = mutableListOf<TeacherAbsenceCounter>()
|
||||
|
||||
teacherAbsenceList.forEach { absence ->
|
||||
val date = absence.dateFrom.clone()
|
||||
|
||||
while (date <= absence.dateTo) {
|
||||
teacherAbsenceCounters.firstOrNull { it.teacherAbsenceDate == date }?.let {
|
||||
it.teacherAbsenceCount += 1
|
||||
} ?: run {
|
||||
teacherAbsenceCounters.add(TeacherAbsenceCounter(date.clone(), 1))
|
||||
}
|
||||
|
||||
date.stepForward(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
teacherAbsenceCounters.forEach { counter ->
|
||||
eventList.add(TeacherAbsenceEvent(
|
||||
counter.teacherAbsenceDate.inMillis,
|
||||
0xffff1744.toInt(),
|
||||
Colors.legibleTextColor(0xffff1744.toInt()),
|
||||
counter.startTime,
|
||||
counter.endTime,
|
||||
app.profileId,
|
||||
counter.teacherAbsenceDate,
|
||||
counter.teacherAbsenceCount
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EVENTS
|
||||
*/
|
||||
if (!isAdded)
|
||||
return@launch
|
||||
|
||||
val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) }
|
||||
val unreadEventDates = mutableSetOf<Int>()
|
||||
|
||||
events.forEach { event ->
|
||||
eventList.add(BaseCalendarEvent(
|
||||
"${event.typeName ?: "wydarzenie"} - ${event.topic}",
|
||||
"",
|
||||
(if (event.time == null) getString(R.string.agenda_event_all_day) else event.time!!.stringHM) +
|
||||
(event.subjectLongName?.let { ", $it" } ?: "") +
|
||||
(event.teacherName?.let { ", $it" } ?: "") +
|
||||
(event.teamName?.let { ", $it" } ?: ""),
|
||||
event.eventColor,
|
||||
Colors.legibleTextColor(event.eventColor),
|
||||
event.startTimeCalendar,
|
||||
event.endTimeCalendar,
|
||||
event.time == null,
|
||||
event.id,
|
||||
!event.seen
|
||||
))
|
||||
|
||||
if (!event.seen) unreadEventDates.add(event.date.value)
|
||||
}
|
||||
|
||||
b.agendaDefaultView.init(eventList, minDate, maxDate, Locale.getDefault(), object : CalendarPickerController {
|
||||
override fun onDaySelected(dayItem: IDayItem?) {}
|
||||
|
||||
override fun onScrollToDate(calendar: Calendar) { this@AgendaFragment.launch {
|
||||
val date = Date.fromCalendar(calendar)
|
||||
actualDate = date
|
||||
|
||||
// Mark as read scrolled date
|
||||
if (date.value in unreadEventDates) {
|
||||
withContext(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, date, true) }
|
||||
unreadEventDates.remove(date.value)
|
||||
}
|
||||
}}
|
||||
|
||||
override fun onEventSelected(event: CalendarEvent) {
|
||||
val date = Date.fromCalendar(event.instanceDay)
|
||||
|
||||
when (event) {
|
||||
is BaseCalendarEvent -> DayDialog(activity, app.profileId, date)
|
||||
is LessonChangeEvent -> LessonChangeDialog(activity, app.profileId, date)
|
||||
is TeacherAbsenceEvent -> TeacherAbsenceDialog(activity, app.profileId, date)
|
||||
}
|
||||
}
|
||||
|
||||
}, LessonChangeEventRenderer(), TeacherAbsenceEventRenderer())
|
||||
|
||||
b.progressBar.visibility = View.GONE
|
||||
agendaDefault = AgendaFragmentDefault(activity, app, b)
|
||||
agendaDefault?.initView(this@AgendaFragment)
|
||||
}}}
|
||||
|
||||
private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch {
|
||||
checkEventTypes()
|
||||
delay(300)
|
||||
|
||||
val dayList = mutableListOf<EventDay>()
|
||||
|
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda
|
||||
|
||||
import android.util.SparseIntArray
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AbsListView.OnScrollListener
|
||||
import androidx.core.util.forEach
|
||||
import androidx.core.util.set
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.tibolte.agendacalendarview.CalendarManager
|
||||
import com.github.tibolte.agendacalendarview.CalendarPickerController
|
||||
import com.github.tibolte.agendacalendarview.agenda.AgendaAdapter
|
||||
import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent
|
||||
import com.github.tibolte.agendacalendarview.models.CalendarEvent
|
||||
import com.github.tibolte.agendacalendarview.models.IDayItem
|
||||
import kotlinx.coroutines.*
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.MainActivity
|
||||
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventGroup
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventGroupRenderer
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.event.AgendaEventRenderer
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges.LessonChangesEventRenderer
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import java.util.*
|
||||
|
||||
class AgendaFragmentDefault(
|
||||
private val activity: MainActivity,
|
||||
private val app: App,
|
||||
private val b: FragmentAgendaDefaultBinding
|
||||
) : OnScrollListener, CoroutineScope {
|
||||
companion object {
|
||||
var selectedDate: Date = Date.getToday()
|
||||
}
|
||||
|
||||
override val coroutineContext = Job() + Dispatchers.Main
|
||||
|
||||
private val unreadDates = mutableSetOf<Int>()
|
||||
private val events = mutableListOf<CalendarEvent>()
|
||||
private var isInitialized = false
|
||||
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||
|
||||
private val listView
|
||||
get() = b.agendaDefaultView.agendaView.agendaListView
|
||||
private val adapter
|
||||
get() = listView.adapter as? AgendaAdapter
|
||||
private val manager
|
||||
get() = CalendarManager.getInstance()
|
||||
|
||||
private var scrollState = OnScrollListener.SCROLL_STATE_IDLE
|
||||
private var updatePending = false
|
||||
private var notifyPending = false
|
||||
override fun onScrollStateChanged(view: AbsListView?, newScrollState: Int) {
|
||||
b.agendaDefaultView.agendaScrollListener.onScrollStateChanged(view, scrollState)
|
||||
scrollState = newScrollState
|
||||
if (updatePending) updateData()
|
||||
if (notifyPending) notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
view: AbsListView?,
|
||||
firstVisibleItem: Int,
|
||||
visibleItemCount: Int,
|
||||
totalItemCount: Int
|
||||
) = b.agendaDefaultView.agendaScrollListener.onScroll(
|
||||
view,
|
||||
firstVisibleItem,
|
||||
visibleItemCount,
|
||||
totalItemCount
|
||||
)
|
||||
|
||||
/**
|
||||
* Mark the data as needing update, either after 1 second (when
|
||||
* not scrolling) or 1 second after scrolling stops.
|
||||
*/
|
||||
private fun updateData() = launch {
|
||||
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
|
||||
updatePending = false
|
||||
delay(1000)
|
||||
notifyDataSetChanged()
|
||||
} else updatePending = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the adapter about changes, either instantly or after
|
||||
* scrolling stops.
|
||||
*/
|
||||
private fun notifyDataSetChanged() {
|
||||
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
|
||||
notifyPending = false
|
||||
adapter?.notifyDataSetChanged()
|
||||
} else notifyPending = true
|
||||
}
|
||||
|
||||
suspend fun initView(fragment: AgendaFragment) {
|
||||
isInitialized = false
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
if (profileConfig.agendaLessonChanges)
|
||||
addLessonChanges(events)
|
||||
|
||||
if (profileConfig.agendaTeacherAbsence)
|
||||
addTeacherAbsence(events)
|
||||
}
|
||||
|
||||
app.db.eventDao().getAll(app.profileId).observe(fragment) {
|
||||
addEvents(events, it)
|
||||
if (isInitialized)
|
||||
updateView()
|
||||
else
|
||||
initViewPriv()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViewPriv() {
|
||||
val dateStart = app.profile.dateSemester1Start.asCalendar
|
||||
val dateEnd = app.profile.dateYearEnd.asCalendar
|
||||
|
||||
val isCompactMode = profileConfig.agendaCompactMode
|
||||
|
||||
b.agendaDefaultView.init(
|
||||
events,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
Locale.getDefault(),
|
||||
object : CalendarPickerController {
|
||||
override fun onDaySelected(dayItem: IDayItem) {
|
||||
val c = Calendar.getInstance()
|
||||
c.time = dayItem.date
|
||||
if (c.timeInMillis == selectedDate.inMillis) {
|
||||
DayDialog(activity, app.profileId, selectedDate)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventSelected(event: CalendarEvent) {
|
||||
val date = Date.fromCalendar(event.instanceDay)
|
||||
|
||||
when (event) {
|
||||
is AgendaEvent -> EventDetailsDialog(activity, event.event)
|
||||
is LessonChangesEvent -> LessonChangeDialog(activity, app.profileId, date)
|
||||
is TeacherAbsenceEvent -> TeacherAbsenceDialog(
|
||||
activity,
|
||||
app.profileId,
|
||||
date
|
||||
)
|
||||
is AgendaEventGroup -> DayDialog(activity, app.profileId, date, eventTypeId = event.typeId)
|
||||
is BaseCalendarEvent -> if (event.isPlaceHolder)
|
||||
DayDialog(activity, app.profileId, date)
|
||||
}
|
||||
|
||||
if (event is BaseEvent && event.showItemBadge) {
|
||||
val unreadCount = manager.events.count {
|
||||
it.instanceDay.equals(event.instanceDay) && it.showBadge
|
||||
}
|
||||
// only clicked event is unread, remove the day badge
|
||||
if (unreadCount == 1 && event.showBadge) {
|
||||
event.dayReference.showBadge = false
|
||||
unreadDates.remove(date.value)
|
||||
}
|
||||
setAsRead(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrollToDate(calendar: Calendar) {
|
||||
selectedDate = Date.fromCalendar(calendar)
|
||||
|
||||
// Mark as read scrolled date
|
||||
if (selectedDate.value in unreadDates) {
|
||||
setAsRead(calendar)
|
||||
activity.launch(Dispatchers.Default) {
|
||||
app.db.eventDao().setSeenByDate(app.profileId, selectedDate, true)
|
||||
}
|
||||
unreadDates.remove(selectedDate.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
AgendaEventRenderer(app.eventManager, isCompactMode),
|
||||
AgendaEventGroupRenderer(),
|
||||
LessonChangesEventRenderer(),
|
||||
TeacherAbsenceEventRenderer()
|
||||
)
|
||||
|
||||
listView.setOnScrollListener(this)
|
||||
|
||||
isInitialized = true
|
||||
b.progressBar.isVisible = false
|
||||
}
|
||||
|
||||
private fun updateView() {
|
||||
manager.events.clear()
|
||||
manager.loadEvents(events, BaseCalendarEvent())
|
||||
|
||||
adapter?.updateEvents(manager.events)
|
||||
//listView.scrollToCurrentDate(selectedDate.asCalendar)
|
||||
}
|
||||
|
||||
private fun setAsRead(date: Calendar) {
|
||||
// get all events matching the date
|
||||
val events = manager.events.filter {
|
||||
if (it.instanceDay.equals(date) && it.showBadge && it is AgendaEvent) {
|
||||
// hide the day badge for the date
|
||||
it.dayReference.showBadge = false
|
||||
return@filter true
|
||||
}
|
||||
false
|
||||
}
|
||||
// set this date's events as read
|
||||
setAsRead(*events.toTypedArray())
|
||||
}
|
||||
|
||||
private fun setAsRead(vararg event: CalendarEvent) {
|
||||
// hide per-event badges
|
||||
for (e in event) {
|
||||
events.firstOrNull {
|
||||
it == e
|
||||
}?.showBadge = false
|
||||
e.showBadge = false
|
||||
}
|
||||
|
||||
listView.setOnScrollListener(this)
|
||||
updateData()
|
||||
}
|
||||
|
||||
private fun addEvents(
|
||||
events: MutableList<CalendarEvent>,
|
||||
eventList: List<EventFull>
|
||||
) {
|
||||
events.removeAll { it is AgendaEvent || it is AgendaEventGroup }
|
||||
|
||||
if (!profileConfig.agendaGroupByType) {
|
||||
events += eventList.map {
|
||||
if (!it.seen)
|
||||
unreadDates.add(it.date.value)
|
||||
AgendaEvent(it)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
eventList.groupBy {
|
||||
it.date.value to it.type
|
||||
}.forEach { (_, list) ->
|
||||
val event = list.first()
|
||||
if (list.size == 1) {
|
||||
if (!event.seen)
|
||||
unreadDates.add(event.date.value)
|
||||
events += AgendaEvent(event)
|
||||
} else {
|
||||
events.add(0, AgendaEventGroup(
|
||||
profileId = event.profileId,
|
||||
date = event.date,
|
||||
typeId = event.type,
|
||||
typeName = event.typeName ?: "-",
|
||||
typeColor = event.typeColor ?: event.eventColor,
|
||||
count = list.size,
|
||||
showBadge = list.any { !it.seen }
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLessonChanges(events: MutableList<CalendarEvent>) {
|
||||
val lessons = app.db.timetableDao().getChangesNow(app.profileId)
|
||||
|
||||
val grouped = lessons.groupBy {
|
||||
it.displayDate
|
||||
}
|
||||
|
||||
events += grouped.mapNotNull { (date, changes) ->
|
||||
LessonChangesEvent(
|
||||
app.profileId,
|
||||
date = date ?: return@mapNotNull null,
|
||||
count = changes.size,
|
||||
showBadge = changes.any { !it.seen }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTeacherAbsence(events: MutableList<CalendarEvent>) {
|
||||
val teacherAbsence = app.db.teacherAbsenceDao().getAllNow(app.profileId)
|
||||
|
||||
val countMap = SparseIntArray()
|
||||
|
||||
for (absence in teacherAbsence) {
|
||||
while (absence.dateFrom <= absence.dateTo) {
|
||||
countMap[absence.dateFrom.value] += 1
|
||||
absence.dateFrom.stepForward(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
countMap.forEach { dateInt, count ->
|
||||
events += TeacherAbsenceEvent(
|
||||
app.profileId,
|
||||
date = Date.fromValue(dateInt),
|
||||
count = count
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-9.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda
|
||||
|
||||
import com.github.tibolte.agendacalendarview.models.CalendarEvent
|
||||
import com.github.tibolte.agendacalendarview.models.IDayItem
|
||||
import com.github.tibolte.agendacalendarview.models.IWeekItem
|
||||
import java.util.*
|
||||
|
||||
open class BaseEvent(
|
||||
private val id: Long,
|
||||
private val time: Calendar,
|
||||
private val color: Int,
|
||||
private var showBadge: Boolean,
|
||||
var showItemBadge: Boolean = showBadge
|
||||
) : CalendarEvent {
|
||||
|
||||
override fun copy() = BaseEvent(id, time, color, showBadge)
|
||||
|
||||
private lateinit var date: Calendar
|
||||
override fun getInstanceDay() = date
|
||||
override fun setInstanceDay(value: Calendar) {
|
||||
date = value
|
||||
}
|
||||
|
||||
private lateinit var dayReference: IDayItem
|
||||
override fun getDayReference() = dayReference
|
||||
override fun setDayReference(value: IDayItem) {
|
||||
dayReference = value
|
||||
}
|
||||
|
||||
private lateinit var weekReference: IWeekItem
|
||||
override fun getWeekReference() = weekReference
|
||||
override fun setWeekReference(value: IWeekItem) {
|
||||
weekReference = value
|
||||
}
|
||||
|
||||
override fun getShowBadge() = showBadge
|
||||
override fun setShowBadge(value: Boolean) {
|
||||
showBadge = value
|
||||
showItemBadge = value
|
||||
}
|
||||
|
||||
override fun getId() = id
|
||||
override fun getStartTime() = time
|
||||
override fun getEndTime() = time
|
||||
override fun getTitle() = ""
|
||||
override fun getDescription() = ""
|
||||
override fun getLocation() = ""
|
||||
override fun getColor() = color
|
||||
override fun getTextColor() = 0
|
||||
override fun isPlaceholder() = false
|
||||
override fun isAllDay() = false
|
||||
|
||||
override fun setId(value: Long) = Unit
|
||||
override fun setStartTime(value: Calendar) = Unit
|
||||
override fun setEndTime(value: Calendar) = Unit
|
||||
override fun setTitle(value: String) = Unit
|
||||
override fun setDescription(value: String) = Unit
|
||||
override fun setLocation(value: String) = Unit
|
||||
override fun setTextColor(value: Int) = Unit
|
||||
override fun setPlaceholder(value: Boolean) = Unit
|
||||
override fun setAllDay(value: Boolean) = Unit
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.event
|
||||
|
||||
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent
|
||||
|
||||
class AgendaEvent(
|
||||
val event: EventFull,
|
||||
showBadge: Boolean = !event.seen
|
||||
) : BaseEvent(
|
||||
id = event.id,
|
||||
time = event.startTimeCalendar,
|
||||
color = event.eventColor,
|
||||
showBadge = showBadge
|
||||
) {
|
||||
override fun copy() = AgendaEvent(event, showBadge)
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-10.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.event
|
||||
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
|
||||
class AgendaEventGroup(
|
||||
val profileId: Int,
|
||||
val date: Date,
|
||||
val typeId: Long,
|
||||
val typeName: String,
|
||||
val typeColor: Int,
|
||||
val count: Int,
|
||||
showBadge: Boolean
|
||||
) : BaseEvent(
|
||||
id = date.value.toLong(),
|
||||
time = date.asCalendar,
|
||||
color = typeColor,
|
||||
showBadge = showBadge
|
||||
) {
|
||||
override fun copy() = AgendaEventGroup(profileId, date, typeId, typeName, typeColor, count, showBadge)
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-10.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.event
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedGroupBinding
|
||||
import pl.szczodrzynski.edziennik.resolveAttr
|
||||
import pl.szczodrzynski.edziennik.setTintColor
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
|
||||
class AgendaEventGroupRenderer : EventRenderer<AgendaEventGroup>() {
|
||||
|
||||
override fun render(view: View, event: AgendaEventGroup) {
|
||||
val b = AgendaWrappedGroupBinding.bind(view).item
|
||||
|
||||
b.card.foreground.setTintColor(event.color)
|
||||
b.card.background.setTintColor(event.color)
|
||||
b.name.text = event.typeName
|
||||
b.name.setTextColor(Colors.legibleTextColor(event.color))
|
||||
b.count.text = event.count.toString()
|
||||
b.count.background.setTintColor(android.R.attr.colorBackground.resolveAttr(view.context))
|
||||
|
||||
b.badge.isVisible = event.showItemBadge
|
||||
}
|
||||
|
||||
override fun getEventLayout(): Int = R.layout.agenda_wrapped_group
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user