mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2025-06-20 09:08:01 +02:00
Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
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,
|
||||
)
|
151
.github/workflows/build-nightly-apk.yml
vendored
Normal file
151
.github/workflows/build-nightly-apk.yml
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
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 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
rm -rf app/release/*
|
||||
rm -rf app/build/outputs/apk/*
|
||||
rm -rf app/build/outputs/bundle/*
|
||||
- name: Assemble official release with Gradle
|
||||
run: ./gradlew assembleOfficialRelease
|
||||
sign:
|
||||
name: Sign APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
outputs:
|
||||
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
|
||||
steps:
|
||||
- name: Sign build artifacts
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||
env:
|
||||
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
|
||||
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
- name: Rename signed artifacts
|
||||
id: artifacts
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
|
||||
publish:
|
||||
name: Publish APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- sign
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Extract changelogs
|
||||
id: changelog
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload APK to SFTP
|
||||
uses: easingthemes/ssh-deploy@v2.1.6
|
||||
env:
|
||||
REMOTE_HOST: ${{ secrets.SSH_IP }}
|
||||
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
|
||||
SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }}
|
||||
TARGET: ${{ secrets.SSH_PATH_NIGHTLY }}
|
||||
- name: Save version metadata
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Distribute to App Distribution
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
token: ${{ secrets.FIREBASE_TOKEN }}
|
||||
groups: ${{ secrets.FIREBASE_GROUPS_NIGHTLY }}
|
||||
file: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
releaseNotesFile: ${{ steps.changelog.outputs.commitLogPlainFile }}
|
||||
|
||||
- name: Post Discord webhook
|
||||
env:
|
||||
APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
|
||||
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
if: true
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.appVersionName }}
|
||||
path: |
|
||||
app/release/whatsnew*/
|
||||
app/release/*.apk
|
||||
app/release/*.aab
|
||||
app/release/*.json
|
||||
app/release/*.txt
|
128
.github/workflows/build-release-aab-play.yml
vendored
Normal file
128
.github/workflows/build-release-aab-play.yml
vendored
Normal file
@ -0,0 +1,128 @@
|
||||
name: Release build - Google Play [AAB]
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare build environment
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- name: Set executable permissions to gradlew
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install packages
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: |
|
||||
python-dotenv
|
||||
pycryptodome
|
||||
mysql-connector-python
|
||||
requests
|
||||
- name: Write signing passwords
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
|
||||
build:
|
||||
name: Build App Bundle
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- prepare
|
||||
outputs:
|
||||
androidHome: ${{ env.ANDROID_HOME }}
|
||||
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
|
||||
steps:
|
||||
- name: Setup JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
rm -rf app/release/*
|
||||
rm -rf app/build/outputs/apk/*
|
||||
rm -rf app/build/outputs/bundle/*
|
||||
- name: Bundle play release with Gradle
|
||||
run: ./gradlew bundlePlayRelease
|
||||
sign:
|
||||
name: Sign App Bundle
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
outputs:
|
||||
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
|
||||
steps:
|
||||
- name: Sign build artifacts
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||
env:
|
||||
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
|
||||
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
- name: Rename signed artifacts
|
||||
id: artifacts
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
|
||||
publish:
|
||||
name: Publish App Bundle
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- sign
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Extract changelogs
|
||||
id: changelog
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Save version metadata
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Publish AAB to Google Play
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
if: ${{ endsWith(needs.sign.outputs.signedReleaseFile, '.aab') }}
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||
packageName: pl.szczodrzynski.edziennik
|
||||
releaseFile: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
releaseName: ${{ steps.changelog.outputs.appVersionName }}
|
||||
track: ${{ secrets.PLAY_RELEASE_TRACK }}
|
||||
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
|
151
.github/workflows/build-release-apk.yml
vendored
Normal file
151
.github/workflows/build-release-apk.yml
vendored
Normal file
@ -0,0 +1,151 @@
|
||||
name: Release build - official
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare build environment
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- name: Set executable permissions to gradlew
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install packages
|
||||
uses: BSFishy/pip-action@v1
|
||||
with:
|
||||
packages: |
|
||||
python-dotenv
|
||||
pycryptodome
|
||||
mysql-connector-python
|
||||
requests
|
||||
- name: Write signing passwords
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
|
||||
build:
|
||||
name: Build APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- prepare
|
||||
outputs:
|
||||
androidHome: ${{ env.ANDROID_HOME }}
|
||||
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
|
||||
steps:
|
||||
- name: Setup JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
rm -rf app/release/*
|
||||
rm -rf app/build/outputs/apk/*
|
||||
rm -rf app/build/outputs/bundle/*
|
||||
- name: Assemble official release with Gradle
|
||||
run: ./gradlew assembleOfficialRelease
|
||||
sign:
|
||||
name: Sign APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- build
|
||||
outputs:
|
||||
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
|
||||
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
|
||||
steps:
|
||||
- name: Sign build artifacts
|
||||
id: sign_app
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/release
|
||||
signingKeyBase64: ${{ secrets.KEY_STORE }}
|
||||
alias: ${{ secrets.KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
|
||||
env:
|
||||
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
|
||||
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
|
||||
BUILD_TOOLS_VERSION: "30.0.2"
|
||||
- name: Rename signed artifacts
|
||||
id: artifacts
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
|
||||
publish:
|
||||
name: Publish APK
|
||||
runs-on: self-hosted
|
||||
needs:
|
||||
- sign
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
|
||||
- name: Extract changelogs
|
||||
id: changelog
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload APK to SFTP
|
||||
uses: easingthemes/ssh-deploy@v2.1.6
|
||||
env:
|
||||
REMOTE_HOST: ${{ secrets.SSH_IP }}
|
||||
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
|
||||
SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }}
|
||||
TARGET: ${{ secrets.SSH_PATH_RELEASE }}
|
||||
- name: Save version metadata
|
||||
env:
|
||||
DB_HOST: ${{ secrets.DB_HOST }}
|
||||
DB_USER: ${{ secrets.DB_USER }}
|
||||
DB_PASS: ${{ secrets.DB_PASS }}
|
||||
DB_NAME: ${{ secrets.DB_NAME }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Distribute to App Distribution
|
||||
uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
with:
|
||||
appId: ${{ secrets.FIREBASE_APP_ID }}
|
||||
token: ${{ secrets.FIREBASE_TOKEN }}
|
||||
groups: ${{ secrets.FIREBASE_GROUPS_RELEASE }}
|
||||
file: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
releaseNotesFile: ${{ steps.changelog.outputs.changelogPlainTitledFile }}
|
||||
- name: Release on GitHub
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.changelogTitle }}
|
||||
body_path: ${{ steps.changelog.outputs.changelogMarkdownFile }}
|
||||
files: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Post Discord webhook
|
||||
env:
|
||||
APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }}
|
||||
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
|
||||
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
|
||||
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
|
||||
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
|
||||
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE
|
||||
|
||||
- name: Upload workflow artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
if: true
|
||||
with:
|
||||
name: ${{ steps.changelog.outputs.appVersionName }}
|
||||
path: |
|
||||
app/release/whatsnew*/
|
||||
app/release/*.apk
|
||||
app/release/*.aab
|
||||
app/release/*.json
|
||||
app/release/*.txt
|
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>
|
||||
|
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.**
|
||||
|
||||
|
@ -18,8 +18,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
|
||||
|
||||
@ -98,7 +100,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'
|
||||
@ -147,7 +152,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:5431f03098"
|
||||
implementation "eu.szkolny:cafebar:5bf0c618de"
|
||||
implementation "eu.szkolny.fslogin:lib:2.0.0"
|
||||
implementation "eu.szkolny:material-about-library:1d5ebaf47c"
|
||||
@ -175,7 +181,6 @@ dependencies {
|
||||
implementation "com.github.bassaer:chatmessageview:2.0.1"
|
||||
implementation "com.github.CanHub:Android-Image-Cropper:2.2.2"
|
||||
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1"
|
||||
implementation "com.github.jetradarmobile:android-snowfall:1.2.0"
|
||||
implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31"
|
||||
implementation("com.heinrichreimersoftware:material-intro") { version { strictly "1.5.8" } }
|
||||
implementation "com.hypertrack:hyperlog:0.0.10"
|
||||
|
@ -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}" />
|
||||
|
||||
<!-- __ __ _ _ _ _ _
|
||||
| \/ | (_) /\ | | (_) (_) |
|
||||
| \ / | __ _ _ _ __ / \ ___| |_ ___ ___| |_ _ _
|
||||
|
@ -1,14 +1,9 @@
|
||||
<h3>Wersja 4.7-rc.1, 2021-04-01</h3>
|
||||
<h3>Wersja 4.8.2, 2021-06-15</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>Poprawiono funkcje logowania. @6Arin9</li>
|
||||
<li>Naprawiono zatrzymanie aplikacji na ekranie planu lekcji. @doteq</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 };
|
||||
0x1d, 0xa7, 0x6e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
|
||||
|
||||
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);
|
||||
|
||||
|
@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
@ -552,28 +553,46 @@ fun CharSequence?.asBoldSpannable(): Spannable {
|
||||
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
return spannable
|
||||
}
|
||||
fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable {
|
||||
fun CharSequence.asSpannable(
|
||||
vararg spans: CharacterStyle,
|
||||
substring: CharSequence? = null,
|
||||
ignoreCase: Boolean = false,
|
||||
ignoreDiacritics: Boolean = false
|
||||
): Spannable {
|
||||
val spannable = SpannableString(this)
|
||||
if (substring == null) {
|
||||
spans.forEach {
|
||||
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
else if (substring.isNotEmpty()) {
|
||||
val string =
|
||||
if (ignoreDiacritics)
|
||||
this.cleanDiacritics()
|
||||
else this
|
||||
substring?.let { substr ->
|
||||
val string = if (ignoreDiacritics)
|
||||
this.cleanDiacritics()
|
||||
else
|
||||
this
|
||||
val search = if (ignoreDiacritics)
|
||||
substr.cleanDiacritics()
|
||||
else
|
||||
substr.toString()
|
||||
|
||||
var index = string.indexOf(substring, ignoreCase = ignoreCase)
|
||||
.takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase)
|
||||
while (index >= 0) {
|
||||
spans.forEach {
|
||||
spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
var index = 0
|
||||
do {
|
||||
index = string.indexOf(
|
||||
string = search,
|
||||
startIndex = index,
|
||||
ignoreCase = ignoreCase
|
||||
)
|
||||
|
||||
if (index >= 0) {
|
||||
spans.forEach {
|
||||
spannable.setSpan(
|
||||
CharacterStyle.wrap(it),
|
||||
index,
|
||||
index + substring.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
index += substring.length.coerceAtLeast(1)
|
||||
}
|
||||
index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
||||
.takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase)
|
||||
}
|
||||
} while (index >= 0)
|
||||
|
||||
} ?: spans.forEach {
|
||||
spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return spannable
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.navigation.NavOptions
|
||||
import com.danimahardhika.cafebar.CafeBar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.jetradarmobile.snowfall.SnowfallView
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
@ -81,12 +82,9 @@ import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
|
||||
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.Utils
|
||||
import pl.szczodrzynski.edziennik.utils.*
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
|
||||
import pl.szczodrzynski.edziennik.utils.appManagerIntentList
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.NavTarget
|
||||
import pl.szczodrzynski.navlib.*
|
||||
@ -470,9 +468,21 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
||||
|
||||
// IT'S WINTER MY DUDES
|
||||
val today = Date.getToday()
|
||||
if ((today.month == 12 || today.month == 1) && app.config.ui.snowfall) {
|
||||
if ((today.month % 11 == 1) && app.config.ui.snowfall) {
|
||||
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
|
||||
}
|
||||
else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
|
||||
val eggfall = layoutInflater.inflate(R.layout.eggfall, b.rootFrame, false) as SnowfallView
|
||||
eggfall.setSnowflakeBitmaps(listOf(
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg1),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg2),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg3),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg4),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg5),
|
||||
BitmapFactory.decodeResource(resources, R.drawable.egg6)
|
||||
))
|
||||
b.rootFrame.addView(eggfall)
|
||||
}
|
||||
|
||||
// WHAT'S NEW DIALOG
|
||||
if (app.config.appVersion < BuildConfig.VERSION_CODE) {
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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.MTIzNDU2Nzg5MDn7mcwDD+===.$param2".sha256()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;")
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -25,6 +25,7 @@ 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.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
|
||||
@ -49,7 +50,8 @@ class LessonDetailsDialog(
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private lateinit var adapter: EventListAdapter
|
||||
private val manager by lazy { app.timetableManager }
|
||||
private val manager
|
||||
get() = app.timetableManager
|
||||
|
||||
init { run {
|
||||
if (activity.isFinishing)
|
||||
@ -216,5 +218,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
|
||||
@ -29,17 +25,9 @@ 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 +47,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 +71,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()
|
||||
@ -129,143 +141,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
|
||||
return@launch
|
||||
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.event
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
||||
import com.mikepenz.iconics.view.IconicsTextView
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding
|
||||
import pl.szczodrzynski.edziennik.join
|
||||
import pl.szczodrzynski.edziennik.resolveAttr
|
||||
import pl.szczodrzynski.edziennik.setTintColor
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
import pl.szczodrzynski.edziennik.utils.managers.EventManager
|
||||
|
||||
class AgendaEventRenderer(
|
||||
val manager: EventManager,
|
||||
val isCompact: Boolean
|
||||
) : EventRenderer<AgendaEvent>() {
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun render(view: View, aEvent: AgendaEvent) {
|
||||
if (isCompact) {
|
||||
val b = AgendaWrappedEventCompactBinding.bind(view).item
|
||||
bindView(aEvent, b.card, b.title, null, b.badgeBackground, b.badge)
|
||||
} else {
|
||||
val b = AgendaWrappedEventBinding.bind(view).item
|
||||
bindView(aEvent, b.card, b.title, b.subtitle, b.badgeBackground, b.badge)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindView(
|
||||
aEvent: AgendaEvent,
|
||||
card: FrameLayout,
|
||||
title: IconicsTextView,
|
||||
subtitle: TextView?,
|
||||
badgeBackground: View,
|
||||
badge: View
|
||||
) {
|
||||
val event = aEvent.event
|
||||
|
||||
val textColor = Colors.legibleTextColor(event.eventColor)
|
||||
|
||||
val timeText = if (event.time == null)
|
||||
card.context.getString(R.string.agenda_event_all_day)
|
||||
else
|
||||
event.time!!.stringHM
|
||||
|
||||
val eventSubtitle = listOfNotNull(
|
||||
timeText,
|
||||
event.subjectLongName,
|
||||
event.teacherName,
|
||||
event.teamName
|
||||
).join(", ")
|
||||
|
||||
card.foreground.setTintColor(event.eventColor)
|
||||
card.background.setTintColor(event.eventColor)
|
||||
manager.setEventTopic(title, event, doneIconColor = textColor)
|
||||
title.setTextColor(textColor)
|
||||
subtitle?.text = eventSubtitle
|
||||
subtitle?.setTextColor(textColor)
|
||||
|
||||
badgeBackground.isVisible = aEvent.showItemBadge
|
||||
badgeBackground.background.setTintColor(
|
||||
android.R.attr.colorBackground.resolveAttr(card.context)
|
||||
)
|
||||
badge.isVisible = aEvent.showItemBadge
|
||||
}
|
||||
|
||||
override fun getEventLayout() = if (isCompact)
|
||||
R.layout.agenda_wrapped_event_compact
|
||||
else
|
||||
R.layout.agenda_wrapped_event
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange
|
||||
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import java.util.*
|
||||
|
||||
class LessonChangeCounter(
|
||||
val lessonChangeDate: Date,
|
||||
var lessonChangeCount: Int
|
||||
) {
|
||||
val startTime: Calendar
|
||||
get() = Calendar.getInstance().apply {
|
||||
set(lessonChangeDate.year, lessonChangeDate.month - 1, lessonChangeDate.day, 10, 0, 0)
|
||||
}
|
||||
|
||||
val endTime: Calendar
|
||||
get() = Calendar.getInstance().apply {
|
||||
timeInMillis = startTime.timeInMillis + (45 * 60 * 1000)
|
||||
}
|
||||
}
|
@ -1,243 +0,0 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange;
|
||||
|
||||
import com.github.tibolte.agendacalendarview.models.CalendarEvent;
|
||||
import com.github.tibolte.agendacalendarview.models.IDayItem;
|
||||
import com.github.tibolte.agendacalendarview.models.IWeekItem;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date;
|
||||
|
||||
public class LessonChangeEvent implements CalendarEvent {
|
||||
|
||||
/**
|
||||
* Id of the event.
|
||||
*/
|
||||
private long mId;
|
||||
/**
|
||||
* Color to be displayed in the agenda view.
|
||||
*/
|
||||
private int mColor;
|
||||
/**
|
||||
* Text color displayed on the background color
|
||||
*/
|
||||
private int mTextColor;
|
||||
/**
|
||||
* Calendar instance helping sorting the events per section in the agenda view.
|
||||
*/
|
||||
private Calendar mInstanceDay;
|
||||
/**
|
||||
* Start time of the event.
|
||||
*/
|
||||
private Calendar mStartTime;
|
||||
/**
|
||||
* End time of the event.
|
||||
*/
|
||||
private Calendar mEndTime;
|
||||
/**
|
||||
* References to a DayItem instance for that event, used to link interaction between the
|
||||
* calendar view and the agenda view.
|
||||
*/
|
||||
private IDayItem mDayReference;
|
||||
/**
|
||||
* References to a WeekItem instance for that event, used to link interaction between the
|
||||
* calendar view and the agenda view.
|
||||
*/
|
||||
private IWeekItem mWeekReference;
|
||||
|
||||
|
||||
private int profileId;
|
||||
private Date lessonChangeDate;
|
||||
private int lessonChangeCount;
|
||||
|
||||
public LessonChangeEvent(LessonChangeEvent calendarEvent) {
|
||||
this.mId = calendarEvent.getId();
|
||||
this.mColor = calendarEvent.getColor();
|
||||
this.mTextColor = calendarEvent.getTextColor();
|
||||
this.mStartTime = calendarEvent.getStartTime();
|
||||
this.mEndTime = calendarEvent.getEndTime();
|
||||
this.profileId = calendarEvent.getProfileId();
|
||||
this.lessonChangeDate = calendarEvent.getLessonChangeDate();
|
||||
this.lessonChangeCount = calendarEvent.getLessonChangeCount();
|
||||
}
|
||||
|
||||
public LessonChangeEvent(long mId, int mColor, int mTextColor, Calendar mStartTime, Calendar mEndTime, int profileId, Date lessonChangeDate, int lessonChangeCount) {
|
||||
this.mId = mId;
|
||||
this.mColor = mColor;
|
||||
this.mTextColor = mTextColor;
|
||||
this.mStartTime = mStartTime;
|
||||
this.mEndTime = mEndTime;
|
||||
this.profileId = profileId;
|
||||
this.lessonChangeDate = lessonChangeDate;
|
||||
this.lessonChangeCount = lessonChangeCount;
|
||||
}
|
||||
|
||||
public int getProfileId() {
|
||||
return profileId;
|
||||
}
|
||||
|
||||
public Date getLessonChangeDate() {
|
||||
return lessonChangeDate;
|
||||
}
|
||||
|
||||
public int getLessonChangeCount() {
|
||||
return lessonChangeCount;
|
||||
}
|
||||
|
||||
public void setProfileId(int profileId) {
|
||||
this.profileId = profileId;
|
||||
}
|
||||
|
||||
public void setLessonChangeDate(Date lessonChangeDate) {
|
||||
this.lessonChangeDate = lessonChangeDate;
|
||||
}
|
||||
|
||||
public void setLessonChangeCount(int lessonChangeCount) {
|
||||
this.lessonChangeCount = lessonChangeCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaceholder(boolean placeholder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPlaceholder() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocation(String mLocation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(long mId) {
|
||||
this.mId = mId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getShowBadge() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShowBadge(boolean mShowBadge) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTextColor() {
|
||||
return mTextColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextColor(int mTextColor) {
|
||||
this.mTextColor = mTextColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescription(String mDescription) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllDay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAllDay(boolean allDay) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Calendar getStartTime() {
|
||||
return mStartTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStartTime(Calendar mStartTime) {
|
||||
this.mStartTime = mStartTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Calendar getEndTime() {
|
||||
return mEndTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEndTime(Calendar mEndTime) {
|
||||
this.mEndTime = mEndTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(String mTitle) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Calendar getInstanceDay() {
|
||||
return mInstanceDay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstanceDay(Calendar mInstanceDay) {
|
||||
this.mInstanceDay = mInstanceDay;
|
||||
this.mInstanceDay.set(Calendar.HOUR, 0);
|
||||
this.mInstanceDay.set(Calendar.MINUTE, 0);
|
||||
this.mInstanceDay.set(Calendar.SECOND, 0);
|
||||
this.mInstanceDay.set(Calendar.MILLISECOND, 0);
|
||||
this.mInstanceDay.set(Calendar.AM_PM, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IDayItem getDayReference() {
|
||||
return mDayReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDayReference(IDayItem mDayReference) {
|
||||
this.mDayReference = mDayReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IWeekItem getWeekReference() {
|
||||
return mWeekReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWeekReference(IWeekItem mWeekReference) {
|
||||
this.mWeekReference = mWeekReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CalendarEvent copy() {
|
||||
return new LessonChangeEvent(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor() {
|
||||
return mColor;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
|
||||
class LessonChangeEventRenderer : EventRenderer<LessonChangeEvent>() {
|
||||
override fun render(view: View?, event: LessonChangeEvent) {
|
||||
val card = view?.findViewById<CardView>(R.id.lesson_change_card)
|
||||
val changeText = view?.findViewById<TextView>(R.id.lesson_change_text)
|
||||
val changeCount = view?.findViewById<TextView>(R.id.lessonChangeCount)
|
||||
card?.setCardBackgroundColor(event.color)
|
||||
changeText?.setTextColor(event.textColor)
|
||||
changeCount?.setTextColor(event.textColor)
|
||||
changeCount?.text = event.lessonChangeCount.toString()
|
||||
}
|
||||
|
||||
override fun getEventLayout(): Int = R.layout.agenda_event_lesson_change
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges
|
||||
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
|
||||
class LessonChangesEvent(
|
||||
val profileId: Int,
|
||||
val date: Date,
|
||||
val count: Int,
|
||||
showBadge: Boolean
|
||||
) : BaseEvent(
|
||||
id = date.value.toLong(),
|
||||
time = date.asCalendar,
|
||||
color = 0xff78909c.toInt(),
|
||||
showBadge = false,
|
||||
showItemBadge = showBadge
|
||||
) {
|
||||
override fun copy() = LessonChangesEvent(profileId, date, count, showItemBadge)
|
||||
|
||||
override fun getShowBadge() = false
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchanges
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding
|
||||
import pl.szczodrzynski.edziennik.resolveAttr
|
||||
import pl.szczodrzynski.edziennik.setTintColor
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
|
||||
class LessonChangesEventRenderer : EventRenderer<LessonChangesEvent>() {
|
||||
|
||||
override fun render(view: View, event: LessonChangesEvent) {
|
||||
val b = AgendaWrappedCounterBinding.bind(view).item
|
||||
val textColor = Colors.legibleTextColor(event.color)
|
||||
|
||||
b.card.foreground.setTintColor(event.color)
|
||||
b.card.background.setTintColor(event.color)
|
||||
b.name.setText(R.string.agenda_lesson_changes)
|
||||
b.name.setTextColor(textColor)
|
||||
b.count.text = event.count.toString()
|
||||
b.count.setTextColor(textColor)
|
||||
|
||||
b.badgeBackground.isVisible = event.showItemBadge
|
||||
b.badgeBackground.background.setTintColor(
|
||||
android.R.attr.colorBackground.resolveAttr(view.context)
|
||||
)
|
||||
b.badge.isVisible = event.showItemBadge
|
||||
}
|
||||
|
||||
fun render(b: AgendaCounterItemBinding, event: LessonChangesEvent) {
|
||||
val textColor = Colors.legibleTextColor(event.color)
|
||||
|
||||
b.card.foreground.setTintColor(event.color)
|
||||
b.card.background.setTintColor(event.color)
|
||||
b.name.setText(R.string.agenda_lesson_changes)
|
||||
b.name.setTextColor(textColor)
|
||||
b.count.text = event.count.toString()
|
||||
b.count.setTextColor(textColor)
|
||||
|
||||
b.badgeBackground.isVisible = event.showItemBadge
|
||||
b.badgeBackground.background.setTintColor(
|
||||
android.R.attr.colorBackground.resolveAttr(b.root.context)
|
||||
)
|
||||
b.badge.isVisible = event.showItemBadge
|
||||
}
|
||||
|
||||
override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence
|
||||
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import java.util.*
|
||||
|
||||
class TeacherAbsenceCounter (
|
||||
val teacherAbsenceDate: Date,
|
||||
var teacherAbsenceCount: Int = 0
|
||||
) {
|
||||
val startTime: Calendar
|
||||
get() = Calendar.getInstance().apply {
|
||||
set(teacherAbsenceDate.year, teacherAbsenceDate.month - 1, teacherAbsenceDate.day, 10, 0, 0)
|
||||
}
|
||||
|
||||
val endTime: Calendar
|
||||
get() = Calendar.getInstance().apply {
|
||||
timeInMillis = startTime.timeInMillis + (45 * 60 * 1000)
|
||||
}
|
||||
}
|
@ -1,188 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence
|
||||
|
||||
import com.github.tibolte.agendacalendarview.models.CalendarEvent
|
||||
import com.github.tibolte.agendacalendarview.models.IDayItem
|
||||
import com.github.tibolte.agendacalendarview.models.IWeekItem
|
||||
import pl.szczodrzynski.edziennik.ui.modules.agenda.BaseEvent
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import java.util.*
|
||||
|
||||
class TeacherAbsenceEvent : CalendarEvent {
|
||||
/**
|
||||
* Id of the event.
|
||||
*/
|
||||
private var mId: Long = 0
|
||||
/**
|
||||
* Color to be displayed in the agenda view.
|
||||
*/
|
||||
private var mColor: Int = 0
|
||||
/**
|
||||
* Text color displayed on the background color
|
||||
*/
|
||||
private var mTextColor: Int = 0
|
||||
/**
|
||||
* Calendar instance helping sorting the events per section in the agenda view.
|
||||
*/
|
||||
private var mInstanceDay: Calendar? = null
|
||||
/**
|
||||
* Start time of the event.
|
||||
*/
|
||||
private var mStartTime: Calendar? = null
|
||||
/**
|
||||
* End time of the event.
|
||||
*/
|
||||
private var mEndTime: Calendar? = null
|
||||
/**
|
||||
* References to a DayItem instance for that event, used to link interaction between the
|
||||
* calendar view and the agenda view.
|
||||
*/
|
||||
private var mDayReference: IDayItem? = null
|
||||
/**
|
||||
* References to a WeekItem instance for that event, used to link interaction between the
|
||||
* calendar view and the agenda view.
|
||||
*/
|
||||
private var mWeekReference: IWeekItem? = null
|
||||
|
||||
|
||||
private var profileId: Int = 0
|
||||
var teacherAbsenceDate: Date? = null
|
||||
var teacherAbsenceCount: Int = 0
|
||||
|
||||
constructor(calendarEvent: TeacherAbsenceEvent) {
|
||||
this.mId = calendarEvent.id
|
||||
this.mColor = calendarEvent.color
|
||||
this.mTextColor = calendarEvent.textColor
|
||||
this.mStartTime = calendarEvent.startTime
|
||||
this.mEndTime = calendarEvent.endTime
|
||||
this.profileId = calendarEvent.profileId
|
||||
this.teacherAbsenceDate = calendarEvent.teacherAbsenceDate
|
||||
this.teacherAbsenceCount = calendarEvent.teacherAbsenceCount
|
||||
}
|
||||
|
||||
constructor(mId: Long, mColor: Int, mTextColor: Int, mStartTime: Calendar, mEndTime: Calendar, profileId: Int, teacherAbsenceDate: Date, teacherAbsenceCount: Int) {
|
||||
this.mId = mId
|
||||
this.mColor = mColor
|
||||
this.mTextColor = mTextColor
|
||||
this.mStartTime = mStartTime
|
||||
this.mEndTime = mEndTime
|
||||
this.profileId = profileId
|
||||
this.teacherAbsenceDate = teacherAbsenceDate
|
||||
this.teacherAbsenceCount = teacherAbsenceCount
|
||||
}
|
||||
|
||||
override fun setPlaceholder(placeholder: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun isPlaceholder(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getLocation(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun setLocation(mLocation: String) {
|
||||
|
||||
}
|
||||
|
||||
override fun getId(): Long {
|
||||
return mId
|
||||
}
|
||||
|
||||
override fun setId(mId: Long) {
|
||||
this.mId = mId
|
||||
}
|
||||
|
||||
override fun getShowBadge(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun setShowBadge(mShowBadge: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun getTextColor(): Int {
|
||||
return mTextColor
|
||||
}
|
||||
|
||||
override fun setTextColor(mTextColor: Int) {
|
||||
this.mTextColor = mTextColor
|
||||
}
|
||||
|
||||
override fun getDescription(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun setDescription(mDescription: String) {
|
||||
|
||||
}
|
||||
|
||||
override fun isAllDay(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun setAllDay(allDay: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun getStartTime(): Calendar? {
|
||||
return mStartTime
|
||||
}
|
||||
|
||||
override fun setStartTime(mStartTime: Calendar) {
|
||||
this.mStartTime = mStartTime
|
||||
}
|
||||
|
||||
override fun getEndTime(): Calendar? {
|
||||
return mEndTime
|
||||
}
|
||||
|
||||
override fun setEndTime(mEndTime: Calendar) {
|
||||
this.mEndTime = mEndTime
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun setTitle(mTitle: String) {
|
||||
|
||||
}
|
||||
|
||||
override fun getInstanceDay(): Calendar? {
|
||||
return mInstanceDay
|
||||
}
|
||||
|
||||
override fun setInstanceDay(mInstanceDay: Calendar) {
|
||||
this.mInstanceDay = mInstanceDay
|
||||
this.mInstanceDay!!.set(Calendar.HOUR, 0)
|
||||
this.mInstanceDay!!.set(Calendar.MINUTE, 0)
|
||||
this.mInstanceDay!!.set(Calendar.SECOND, 0)
|
||||
this.mInstanceDay!!.set(Calendar.MILLISECOND, 0)
|
||||
this.mInstanceDay!!.set(Calendar.AM_PM, 0)
|
||||
}
|
||||
|
||||
override fun getDayReference(): IDayItem? {
|
||||
return mDayReference
|
||||
}
|
||||
|
||||
override fun setDayReference(mDayReference: IDayItem) {
|
||||
this.mDayReference = mDayReference
|
||||
}
|
||||
|
||||
override fun getWeekReference(): IWeekItem? {
|
||||
return mWeekReference
|
||||
}
|
||||
|
||||
override fun setWeekReference(mWeekReference: IWeekItem) {
|
||||
this.mWeekReference = mWeekReference
|
||||
}
|
||||
|
||||
override fun copy(): CalendarEvent {
|
||||
return TeacherAbsenceEvent(this)
|
||||
}
|
||||
|
||||
override fun getColor(): Int {
|
||||
return mColor
|
||||
}
|
||||
class TeacherAbsenceEvent(
|
||||
val profileId: Int,
|
||||
val date: Date,
|
||||
val count: Int
|
||||
) : BaseEvent(
|
||||
id = date.value.toLong(),
|
||||
time = date.asCalendar,
|
||||
color = 0xffff1744.toInt(),
|
||||
showBadge = false
|
||||
) {
|
||||
override fun copy() = TeacherAbsenceEvent(profileId, date, count)
|
||||
}
|
||||
|
@ -1,21 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-8.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.isVisible
|
||||
import com.github.tibolte.agendacalendarview.render.EventRenderer
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding
|
||||
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding
|
||||
import pl.szczodrzynski.edziennik.setTintColor
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
|
||||
class TeacherAbsenceEventRenderer : EventRenderer<TeacherAbsenceEvent>() {
|
||||
override fun render(view: View?, event: TeacherAbsenceEvent) {
|
||||
val card = view?.findViewById<CardView>(R.id.teacherAbsenceCard)
|
||||
val changeText = view?.findViewById<TextView>(R.id.teacherAbsenceText)
|
||||
val changeCount = view?.findViewById<TextView>(R.id.teacherAbsenceCount)
|
||||
card?.setCardBackgroundColor(event.color)
|
||||
changeText?.setTextColor(event.textColor)
|
||||
changeCount?.setTextColor(event.textColor)
|
||||
changeCount?.text = event.teacherAbsenceCount.toString()
|
||||
|
||||
override fun render(view: View, event: TeacherAbsenceEvent) {
|
||||
val b = AgendaWrappedCounterBinding.bind(view).item
|
||||
val textColor = Colors.legibleTextColor(event.color)
|
||||
|
||||
b.card.foreground.setTintColor(event.color)
|
||||
b.card.background.setTintColor(event.color)
|
||||
b.name.setText(R.string.agenda_teacher_absence)
|
||||
b.name.setTextColor(textColor)
|
||||
b.count.text = event.count.toString()
|
||||
b.count.setTextColor(textColor)
|
||||
|
||||
b.badgeBackground.isVisible = false
|
||||
b.badge.isVisible = false
|
||||
}
|
||||
|
||||
override fun getEventLayout(): Int = R.layout.agenda_event_teacher_absence
|
||||
fun render(b: AgendaCounterItemBinding, event: TeacherAbsenceEvent) {
|
||||
val textColor = Colors.legibleTextColor(event.color)
|
||||
|
||||
b.card.foreground.setTintColor(event.color)
|
||||
b.card.background.setTintColor(event.color)
|
||||
b.name.setText(R.string.agenda_teacher_absence)
|
||||
b.name.setTextColor(textColor)
|
||||
b.count.text = event.count.toString()
|
||||
b.count.setTextColor(textColor)
|
||||
|
||||
b.badgeBackground.isVisible = false
|
||||
b.badge.isVisible = false
|
||||
}
|
||||
|
||||
override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
|
||||
import pl.szczodrzynski.edziennik.databinding.AttendanceDetailsDialogBinding
|
||||
import pl.szczodrzynski.edziennik.setTintColor
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLink
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class AttendanceDetailsDialog(
|
||||
@ -60,5 +61,13 @@ class AttendanceDetailsDialog(
|
||||
b.attendanceName.background.setTintColor(attendanceColor)
|
||||
|
||||
b.attendanceIsCounted.setText(if (attendance.isCounted) R.string.yes else R.string.no)
|
||||
|
||||
attendance.teacherName?.let { name ->
|
||||
BetterLink.attach(
|
||||
b.teacherName,
|
||||
teachers = mapOf(attendance.teacherId to name),
|
||||
onActionSelected = dialog::dismiss
|
||||
)
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
@ -41,7 +41,8 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope {
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// local/private variables go here
|
||||
private val manager by lazy { app.attendanceManager }
|
||||
private val manager
|
||||
get() = app.attendanceManager
|
||||
private var viewType = AttendanceFragment.VIEW_DAYS
|
||||
private var expandSubjectId = 0L
|
||||
|
||||
|
@ -47,7 +47,8 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope {
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// local/private variables go here
|
||||
private val manager by lazy { app.attendanceManager }
|
||||
private val manager
|
||||
get() = app.attendanceManager
|
||||
private var expandSubjectId = 0L
|
||||
private var attendance = listOf<AttendanceFull>()
|
||||
|
||||
|
@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Notice
|
||||
import pl.szczodrzynski.edziennik.data.db.full.NoticeFull
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLink
|
||||
import pl.szczodrzynski.edziennik.utils.Utils.bs
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
|
||||
@ -83,6 +84,14 @@ class NoticesAdapter//getting the context and product list with constructor
|
||||
} else {
|
||||
holder.noticesItemReason.background = null
|
||||
}
|
||||
|
||||
BetterLink.attach(holder.noticesItemReason)
|
||||
|
||||
notice.teacherName?.let { name ->
|
||||
BetterLink.attach(holder.noticesItemTeacherName, teachers = mapOf(
|
||||
notice.teacherId to name
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
|
@ -40,7 +40,8 @@ class GradesAdapter(
|
||||
}
|
||||
|
||||
private val app = activity.applicationContext as App
|
||||
private val manager = app.gradesManager
|
||||
private val manager
|
||||
get() = app.gradesManager
|
||||
|
||||
private val job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
|
@ -48,9 +48,12 @@ class GradesListFragment : Fragment(), CoroutineScope {
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
// local/private variables go here
|
||||
private val manager by lazy { app.gradesManager }
|
||||
private val dontCountEnabled by lazy { manager.dontCountEnabled }
|
||||
private val dontCountGrades by lazy { manager.dontCountGrades }
|
||||
private val manager
|
||||
get() = app.gradesManager
|
||||
private val dontCountEnabled
|
||||
get() = manager.dontCountEnabled
|
||||
private val dontCountGrades
|
||||
get() = manager.dontCountGrades
|
||||
private var expandSubjectId = 0L
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -10,12 +10,14 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.RotateAnimation
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
@ -29,7 +31,9 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailability
|
||||
import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod
|
||||
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class LoginChooserFragment : Fragment(), CoroutineScope {
|
||||
@ -62,6 +66,15 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (!isAdded) return
|
||||
|
||||
b.versionText.setText(
|
||||
R.string.login_chooser_version_format,
|
||||
app.buildManager.versionName,
|
||||
Date.fromMillis(app.buildManager.buildTimestamp).stringY_m_d
|
||||
)
|
||||
b.versionText.onClick {
|
||||
app.buildManager.showVersionDialog(activity)
|
||||
}
|
||||
|
||||
val adapter = LoginChooserAdapter(activity, this::onLoginModeClicked)
|
||||
|
||||
LoginInfo.chooserList = LoginInfo.chooserList
|
||||
@ -203,6 +216,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
|
||||
return
|
||||
}
|
||||
|
||||
if (!app.config.privacyPolicyAccepted) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.privacy_policy)
|
||||
.setMessage(Html.fromHtml(activity.getString(R.string.privacy_policy_dialog_html)))
|
||||
.setPositiveButton(R.string.i_agree) { _, _ ->
|
||||
app.config.privacyPolicyAccepted = true
|
||||
onLoginModeClicked(loginType, loginMode)
|
||||
}
|
||||
.setNegativeButton(R.string.i_disagree, null)
|
||||
.show()
|
||||
.also { dialog ->
|
||||
dialog.findViewById<TextView>(android.R.id.message)?.movementMethod =
|
||||
BetterLinkMovementMethod.getInstance()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
launch {
|
||||
if (!checkAvailability(loginType.loginType))
|
||||
return@launch
|
||||
|
@ -95,8 +95,10 @@ class LoginEggsFragment : Fragment(), CoroutineScope {
|
||||
anim.interpolator = AccelerateDecelerateInterpolator()
|
||||
anim.duration = 10
|
||||
anim.fillAfter = true
|
||||
activity.getRootView().startAnimation(anim)
|
||||
nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions)
|
||||
activity.runOnUiThread {
|
||||
activity.getRootView().startAnimation(anim)
|
||||
nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions)
|
||||
}
|
||||
}
|
||||
}, "EggInterface")
|
||||
loadUrl("https://szkolny.eu/game/runner.html")
|
||||
|
@ -5,7 +5,6 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.login
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -75,19 +74,6 @@ class LoginSummaryFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
|
||||
b.finishButton.onClick {
|
||||
if (!app.config.privacyPolicyAccepted) {
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.privacy_policy)
|
||||
.setMessage(Html.fromHtml("Korzystając z aplikacji potwierdzasz <a href=\"http://szkolny.eu/privacy-policy\">przeczytanie Polityki prywatności</a> i akceptujesz jej postanowienia."))
|
||||
.setPositiveButton(R.string.i_agree) { _, _ ->
|
||||
app.config.privacyPolicyAccepted = true
|
||||
b.finishButton.performClick()
|
||||
}
|
||||
.setNegativeButton(R.string.i_disagree, null)
|
||||
.show()
|
||||
return@onClick
|
||||
}
|
||||
|
||||
val args = Bundle(
|
||||
"registrationAllowed" to b.registerMeSwitch.isChecked
|
||||
)
|
||||
|
@ -30,10 +30,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.utils.Anim
|
||||
import pl.szczodrzynski.edziennik.utils.BetterLink
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||
import pl.szczodrzynski.navlib.colorAttr
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.min
|
||||
@ -64,10 +66,20 @@ class MessageFragment : Fragment(), CoroutineScope {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (!isAdded) return
|
||||
|
||||
activity.bottomSheet.prependItem(
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_messages_config)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||
.withOnClickListener {
|
||||
activity.bottomSheet.close()
|
||||
MessagesConfigDialog(activity, false, null, null)
|
||||
}
|
||||
)
|
||||
|
||||
b.closeButton.setImageDrawable(
|
||||
IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply {
|
||||
colorAttr(activity, android.R.attr.textColorSecondary)
|
||||
sizeDp = 16
|
||||
sizeDp = 24
|
||||
}
|
||||
)
|
||||
b.closeButton.setOnClickListener { activity.navigateUp() }
|
||||
|
@ -1,11 +1,8 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.messages
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -13,22 +10,19 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.cleanDiacritics
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.utils.MessagesFilter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.min
|
||||
|
||||
class MessagesAdapter(
|
||||
val activity: AppCompatActivity,
|
||||
val teachers: List<Teacher>,
|
||||
val onItemClick: ((item: MessageFull) -> Unit)? = null
|
||||
val activity: AppCompatActivity,
|
||||
val teachers: List<Teacher>,
|
||||
val onItemClick: ((item: MessageFull) -> Unit)? = null
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope, Filterable {
|
||||
companion object {
|
||||
private const val TAG = "MessagesAdapter"
|
||||
@ -43,41 +37,10 @@ class MessagesAdapter(
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
var items = mutableListOf<Any>()
|
||||
var allItems = mutableListOf<Any>()
|
||||
var items = listOf<Any>()
|
||||
var allItems = listOf<Any>()
|
||||
val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) }
|
||||
val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
|
||||
private val comparator by lazy { Comparator { o1: Any, o2: Any ->
|
||||
if (o1 !is MessageFull || o2 !is MessageFull)
|
||||
return@Comparator 0
|
||||
when {
|
||||
// standard sorting
|
||||
o1.filterWeight > o2.filterWeight -> return@Comparator 1
|
||||
o1.filterWeight < o2.filterWeight -> return@Comparator -1
|
||||
else -> when {
|
||||
// reversed sorting
|
||||
o1.addedDate > o2.addedDate -> return@Comparator -1
|
||||
o1.addedDate < o2.addedDate -> return@Comparator 1
|
||||
else -> return@Comparator 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
val textWatcher by lazy {
|
||||
object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
getFilter().filter(s.toString())
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
/*items.getOrNull(0)?.let {
|
||||
if (it is MessagesSearch) {
|
||||
it.searchText = s?.toString() ?: ""
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
@ -103,138 +66,16 @@ class MessagesAdapter(
|
||||
return
|
||||
|
||||
when {
|
||||
holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this)
|
||||
holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this)
|
||||
holder is MessageViewHolder
|
||||
&& item is MessageFull -> holder.onBind(activity, app, item, position, this)
|
||||
holder is SearchViewHolder
|
||||
&& item is MessagesSearch -> holder.onBind(activity, app, item, position, this)
|
||||
}
|
||||
}
|
||||
|
||||
private val messagesFilter by lazy {
|
||||
MessagesFilter(this)
|
||||
}
|
||||
override fun getItemCount() = items.size
|
||||
override fun getFilter() = filter
|
||||
private var prevCount = -1
|
||||
private val filter by lazy { object : Filter() {
|
||||
override fun performFiltering(prefix: CharSequence?): FilterResults {
|
||||
val results = FilterResults()
|
||||
|
||||
if (prevCount == -1)
|
||||
prevCount = allItems.size
|
||||
|
||||
if (prefix.isNullOrEmpty()) {
|
||||
allItems.forEach {
|
||||
if (it is MessageFull)
|
||||
it.searchHighlightText = null
|
||||
}
|
||||
results.values = allItems.toList()
|
||||
results.count = allItems.size
|
||||
return results
|
||||
}
|
||||
|
||||
val items = mutableListOf<Any>()
|
||||
val prefixString = prefix.toString()
|
||||
|
||||
allItems.forEach {
|
||||
if (it !is MessageFull) {
|
||||
items.add(it)
|
||||
return@forEach
|
||||
}
|
||||
it.filterWeight = 100
|
||||
it.searchHighlightText = null
|
||||
|
||||
var weight: Int
|
||||
if (it.type == Message.TYPE_SENT) {
|
||||
it.recipients?.forEach { recipient ->
|
||||
weight = getMatchWeight(recipient.fullName, prefixString)
|
||||
if (weight != 100) {
|
||||
if (weight == 3)
|
||||
weight = 31
|
||||
it.filterWeight = min(it.filterWeight, 10 + weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
weight = getMatchWeight(it.senderName, prefixString)
|
||||
if (weight != 100) {
|
||||
if (weight == 3)
|
||||
weight = 31
|
||||
it.filterWeight = min(it.filterWeight, 10 + weight)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
weight = getMatchWeight(it.subject, prefixString)
|
||||
if (weight != 100) {
|
||||
if (weight == 3)
|
||||
weight = 22
|
||||
it.filterWeight = min(it.filterWeight, 20 + weight)
|
||||
}
|
||||
|
||||
if (it.filterWeight != 100) {
|
||||
it.searchHighlightText = prefixString
|
||||
items.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(items, comparator)
|
||||
results.values = items
|
||||
results.count = items.size
|
||||
return results
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
|
||||
results.values?.let { items = it as MutableList<Any> }
|
||||
// do not re-bind the search box
|
||||
val count = results.count - 1
|
||||
|
||||
// this tries to update every item except the search field
|
||||
when {
|
||||
count > prevCount -> {
|
||||
notifyItemRangeInserted(prevCount + 1, count - prevCount)
|
||||
notifyItemRangeChanged(1, prevCount)
|
||||
}
|
||||
count < prevCount -> {
|
||||
notifyItemRangeRemoved(prevCount + 1, prevCount - count)
|
||||
notifyItemRangeChanged(1, count)
|
||||
}
|
||||
else -> {
|
||||
notifyItemRangeChanged(1, count)
|
||||
}
|
||||
}
|
||||
|
||||
/*if (prevCount != count) {
|
||||
items.getOrNull(0)?.let {
|
||||
if (it is MessagesSearch) {
|
||||
it.count = count
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
prevCount = count
|
||||
}
|
||||
}}
|
||||
|
||||
private fun getMatchWeight(name: CharSequence?, prefix: String): Int {
|
||||
if (name == null)
|
||||
return 100
|
||||
|
||||
val nameClean = name.cleanDiacritics()
|
||||
|
||||
// First match against the whole, non-split value
|
||||
if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) {
|
||||
return 1
|
||||
} else {
|
||||
// check if prefix matches any of the words
|
||||
val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray()
|
||||
for (word in words) {
|
||||
if (word.startsWith(prefix, ignoreCase = true)) {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
// finally check if the prefix matches any part of the name
|
||||
if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) {
|
||||
return 3
|
||||
}
|
||||
|
||||
return 100
|
||||
}
|
||||
override fun getFilter() = messagesFilter
|
||||
}
|
||||
|
@ -12,7 +12,9 @@ import kotlinx.coroutines.Job
|
||||
import pl.szczodrzynski.edziennik.*
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||
import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
|
||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class MessagesFragment : Fragment(), CoroutineScope {
|
||||
@ -100,9 +102,19 @@ class MessagesFragment : Fragment(), CoroutineScope {
|
||||
fabIcon = CommunityMaterial.Icon3.cmd_pencil_outline
|
||||
}
|
||||
|
||||
setFabOnClickListener(View.OnClickListener {
|
||||
bottomSheet.prependItem(
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_messages_config)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||
.withOnClickListener {
|
||||
activity.bottomSheet.close()
|
||||
MessagesConfigDialog(activity, false, null, null)
|
||||
}
|
||||
)
|
||||
|
||||
setFabOnClickListener {
|
||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
activity.gainAttentionFAB()
|
||||
|
@ -33,6 +33,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
private lateinit var app: App
|
||||
private lateinit var activity: MainActivity
|
||||
private lateinit var b: MessagesListFragmentBinding
|
||||
private var adapter: MessagesAdapter? = null
|
||||
|
||||
private val job: Job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
@ -53,21 +54,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
|
||||
var topPosition = arguments.getInt("topPosition", NO_POSITION)
|
||||
var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
|
||||
val searchText = arguments.getString("searchText", "")
|
||||
|
||||
teachers = withContext(Dispatchers.Default) {
|
||||
app.db.teacherDao().getAllNow(App.profileId)
|
||||
}
|
||||
|
||||
val adapter = MessagesAdapter(activity, teachers) {
|
||||
adapter = MessagesAdapter(activity, teachers) {
|
||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
||||
"messageId" to it.id
|
||||
))
|
||||
}
|
||||
|
||||
app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { items ->
|
||||
app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages ->
|
||||
if (!isAdded) return@Observer
|
||||
|
||||
items.forEach { message ->
|
||||
messages.forEach { message ->
|
||||
message.recipients?.removeAll { it.profileId != message.profileId }
|
||||
message.recipients?.forEach { recipient ->
|
||||
if (recipient.fullName == null) {
|
||||
@ -77,13 +79,22 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
}
|
||||
|
||||
// load & configure the adapter
|
||||
adapter.items = items.toMutableList()
|
||||
adapter.items.add(0, MessagesSearch().also {
|
||||
it.count = items.size
|
||||
val items = messages.toMutableList<Any>()
|
||||
items.add(0, MessagesSearch().also {
|
||||
it.searchText = searchText
|
||||
})
|
||||
adapter.allItems = adapter.items.toMutableList()
|
||||
|
||||
adapter?.items = items
|
||||
adapter?.allItems = items
|
||||
|
||||
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
|
||||
b.list.adapter = adapter
|
||||
if (searchText.isNotBlank())
|
||||
adapter?.filter?.filter(searchText) {
|
||||
b.list.adapter = adapter
|
||||
}
|
||||
else
|
||||
b.list.adapter = adapter
|
||||
|
||||
b.list.apply {
|
||||
setHasFixedSize(true)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
@ -92,7 +103,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
addOnScrollListener(onScrollListener)
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty())
|
||||
|
||||
(b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager ->
|
||||
@ -119,10 +130,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!isAdded) return
|
||||
if (!isAdded)
|
||||
return
|
||||
val layoutManager = (b.list.layoutManager as? LinearLayoutManager)
|
||||
val searchItem = adapter?.items?.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||
|
||||
onPageDestroy?.invoke(position, Bundle(
|
||||
"topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(),
|
||||
"bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition()
|
||||
"topPosition" to layoutManager?.findFirstVisibleItemPosition(),
|
||||
"bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(),
|
||||
"searchText" to searchItem?.searchText?.toString()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.messages.compose
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
@ -43,6 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
@ -50,6 +52,7 @@ import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||
import pl.szczodrzynski.navlib.elevateSurface
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.replace
|
||||
@ -67,6 +70,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||
private val greetingText
|
||||
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
||||
|
||||
private var teachers = mutableListOf<Teacher>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
@ -131,6 +138,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}*/
|
||||
|
||||
activity.bottomSheet.prependItem(
|
||||
BottomSheetPrimaryItem(true)
|
||||
.withTitle(R.string.menu_messages_config)
|
||||
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||
.withOnClickListener {
|
||||
activity.bottomSheet.close()
|
||||
MessagesConfigDialog(activity, false, null, null)
|
||||
}
|
||||
)
|
||||
|
||||
launch {
|
||||
delay(100)
|
||||
getRecipientList()
|
||||
@ -290,7 +307,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
b.recipients.setIllegalCharacterIdentifier { c ->
|
||||
c.toString().matches("[\\n;:_ ]".toRegex())
|
||||
}
|
||||
b.recipients.setOnChipRemoveListener { _ ->
|
||||
b.recipients.setOnChipRemoveListener {
|
||||
b.recipients.setSelection(b.recipients.text.length)
|
||||
}
|
||||
|
||||
@ -318,14 +335,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
fabExtendedText = getString(R.string.messages_compose_send)
|
||||
fabIcon = CommunityMaterial.Icon3.cmd_send_outline
|
||||
|
||||
setFabOnClickListener(View.OnClickListener {
|
||||
setFabOnClickListener {
|
||||
sendMessage()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
activity.gainAttentionFAB()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun updateRecipientList(list: List<Teacher>) { launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
teachers = list.sortedBy { it.fullName }.toMutableList()
|
||||
@ -344,10 +362,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
|
||||
b.recipients.setAdapter(adapter)
|
||||
|
||||
if (profileConfig.messagesGreetingOnCompose)
|
||||
b.text.setText(greetingText)
|
||||
|
||||
handleReplyMessage()
|
||||
handleMailToIntent()
|
||||
}}
|
||||
|
||||
private fun handleReplyMessage() { launch {
|
||||
private fun handleReplyMessage() = launch {
|
||||
val replyMessage = arguments?.getString("message")
|
||||
if (replyMessage != null) {
|
||||
val chipList = mutableListOf<ChipInfo>()
|
||||
@ -369,8 +391,10 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
|
||||
if (arguments?.getString("type") == "reply") {
|
||||
// add greeting text
|
||||
span.replace(0, 0, "\n\nZ poważaniem,\n${app.profile.accountName
|
||||
?: app.profile.studentNameLong ?: ""}\n\n\n")
|
||||
if (profileConfig.messagesGreetingOnReply)
|
||||
span.replace(0, 0, "$greetingText\n\n\n")
|
||||
else
|
||||
span.replace(0, 0, "\n\n")
|
||||
|
||||
teachers.firstOrNull { it.id == msg.senderId }?.let { teacher ->
|
||||
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
|
||||
@ -378,7 +402,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
subject = "Re: ${msg.subject}"
|
||||
} else {
|
||||
span.replace(0, 0, "\n\n")
|
||||
// add greeting text
|
||||
if (profileConfig.messagesGreetingOnForward)
|
||||
span.replace(0, 0, "$greetingText\n\n\n")
|
||||
else
|
||||
span.replace(0, 0, "\n\n")
|
||||
|
||||
subject = "Fwd: ${msg.subject}"
|
||||
}
|
||||
body = MessagesUtils.htmlToSpannable(activity, msg.body
|
||||
@ -400,7 +429,23 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
else {
|
||||
b.recipients.requestFocus()
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
private fun handleMailToIntent() {
|
||||
val teacherId = arguments?.getLong("messageRecipientId")
|
||||
if (teacherId == 0L)
|
||||
return
|
||||
|
||||
val chipList = mutableListOf<ChipInfo>()
|
||||
teachers.firstOrNull { it.id == teacherId }?.let { teacher ->
|
||||
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
|
||||
chipList += ChipInfo(teacher.fullName, teacher)
|
||||
}
|
||||
b.recipients.addTextWithChips(chipList)
|
||||
|
||||
val subject = arguments?.getString("messageSubject")
|
||||
b.subject.setText(subject ?: return)
|
||||
}
|
||||
|
||||
private fun sendMessage() {
|
||||
b.recipientsLayout.error = null
|
||||
|
@ -5,7 +5,5 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.messages.models
|
||||
|
||||
class MessagesSearch {
|
||||
var isFocused = false
|
||||
var searchText = ""
|
||||
var count = 0
|
||||
var searchText: CharSequence = ""
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.messages.utils
|
||||
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
|
||||
class MessagesComparator : Comparator<Any> {
|
||||
|
||||
override fun compare(o1: Any?, o2: Any?): Int {
|
||||
if (o1 !is MessageFull || o2 !is MessageFull)
|
||||
return 0
|
||||
|
||||
return when {
|
||||
// standard sorting
|
||||
o1.filterWeight > o2.filterWeight -> 1
|
||||
o1.filterWeight < o2.filterWeight -> -1
|
||||
else -> when {
|
||||
// reversed sorting
|
||||
o1.addedDate > o2.addedDate -> -1
|
||||
o1.addedDate < o2.addedDate -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.messages.utils
|
||||
|
||||
import android.widget.Filter
|
||||
import pl.szczodrzynski.edziennik.cleanDiacritics
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
class MessagesFilter(
|
||||
private val adapter: MessagesAdapter
|
||||
) : Filter() {
|
||||
companion object {
|
||||
private const val NO_MATCH = 1000
|
||||
}
|
||||
|
||||
private val comparator = MessagesComparator()
|
||||
private var prevCount = -1
|
||||
|
||||
private val allItems
|
||||
get() = adapter.allItems
|
||||
|
||||
private fun getMatchWeight(name: CharSequence?, prefix: CharSequence): Int {
|
||||
if (name == null)
|
||||
return NO_MATCH
|
||||
|
||||
val prefixClean = prefix.cleanDiacritics()
|
||||
val nameClean = name.cleanDiacritics()
|
||||
|
||||
return when {
|
||||
// First match against the whole, non-split value
|
||||
nameClean.startsWith(prefixClean, ignoreCase = true) -> 1
|
||||
// check if prefix matches any of the words
|
||||
nameClean.split(" ").any {
|
||||
it.startsWith(prefixClean, ignoreCase = true)
|
||||
} -> 2
|
||||
// finally check if the prefix matches any part of the name
|
||||
nameClean.contains(prefixClean, ignoreCase = true) -> 3
|
||||
|
||||
else -> NO_MATCH
|
||||
}
|
||||
}
|
||||
|
||||
override fun performFiltering(prefix: CharSequence?): FilterResults {
|
||||
val results = FilterResults()
|
||||
|
||||
if (prevCount == -1)
|
||||
prevCount = allItems.size
|
||||
|
||||
if (prefix.isNullOrBlank()) {
|
||||
allItems.forEach {
|
||||
if (it is MessageFull)
|
||||
it.searchHighlightText = null
|
||||
}
|
||||
results.values = allItems.toList()
|
||||
results.count = allItems.size
|
||||
return results
|
||||
}
|
||||
|
||||
val items = mutableListOf<Any>()
|
||||
|
||||
allItems.forEach {
|
||||
if (it !is MessageFull) {
|
||||
items.add(it)
|
||||
return@forEach
|
||||
}
|
||||
it.filterWeight = NO_MATCH
|
||||
it.searchHighlightText = null
|
||||
|
||||
var weight: Int
|
||||
// weights 11..13 and 110
|
||||
if (it.type == Message.TYPE_SENT) {
|
||||
it.recipients?.forEach { recipient ->
|
||||
weight = getMatchWeight(recipient.fullName, prefix)
|
||||
if (weight != NO_MATCH) {
|
||||
if (weight == 3)
|
||||
weight = 100
|
||||
it.filterWeight = min(it.filterWeight, 10 + weight)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
weight = getMatchWeight(it.senderName, prefix)
|
||||
if (weight != NO_MATCH) {
|
||||
if (weight == 3)
|
||||
weight = 100
|
||||
it.filterWeight = min(it.filterWeight, 10 + weight)
|
||||
}
|
||||
}
|
||||
|
||||
// weights 21..23 and 120
|
||||
weight = getMatchWeight(it.subject, prefix)
|
||||
if (weight != NO_MATCH) {
|
||||
if (weight == 3)
|
||||
weight = 100
|
||||
it.filterWeight = min(it.filterWeight, 20 + weight)
|
||||
}
|
||||
|
||||
// weights 31..33 and 130
|
||||
weight = getMatchWeight(it.body, prefix)
|
||||
if (weight != NO_MATCH) {
|
||||
if (weight == 3)
|
||||
weight = 100
|
||||
it.filterWeight = min(it.filterWeight, 30 + weight)
|
||||
}
|
||||
|
||||
if (it.filterWeight != NO_MATCH) {
|
||||
it.searchHighlightText = prefix
|
||||
items.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(items, comparator)
|
||||
results.values = items
|
||||
results.count = items.size
|
||||
return results
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
|
||||
results.values?.let {
|
||||
adapter.items = it as MutableList<Any>
|
||||
}
|
||||
// do not re-bind the search box
|
||||
val count = results.count - 1
|
||||
|
||||
// this tries to update every item except the search field
|
||||
with(adapter) {
|
||||
when {
|
||||
count > prevCount -> {
|
||||
notifyItemRangeInserted(prevCount + 1, count - prevCount)
|
||||
notifyItemRangeChanged(1, prevCount)
|
||||
}
|
||||
count < prevCount -> {
|
||||
notifyItemRangeRemoved(prevCount + 1, prevCount - count)
|
||||
notifyItemRangeChanged(1, count)
|
||||
}
|
||||
else -> {
|
||||
notifyItemRangeChanged(1, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevCount = count
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.messages.utils
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
||||
|
||||
class SearchTextWatcher(
|
||||
private val b: MessagesListItemSearchBinding,
|
||||
private val filter: MessagesFilter,
|
||||
private val item: MessagesSearch
|
||||
) : TextWatcher {
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
item.searchText = s ?: ""
|
||||
filter.filter(s) { count ->
|
||||
if (s.isNullOrBlank())
|
||||
b.searchLayout.helperText = " "
|
||||
else
|
||||
b.searchLayout.helperText =
|
||||
b.root.context.getString(R.string.messages_search_results, count - 1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is SearchTextWatcher
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = b.hashCode()
|
||||
result = 31 * result + filter.hashCode()
|
||||
result = 31 * result + item.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
@ -22,17 +22,21 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
|
||||
class MessageViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false)
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false)
|
||||
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessageFull, MessagesAdapter> {
|
||||
companion object {
|
||||
private const val TAG = "MessageViewHolder"
|
||||
}
|
||||
|
||||
override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) {
|
||||
val manager = app.gradesManager
|
||||
|
||||
override fun onBind(
|
||||
activity: AppCompatActivity,
|
||||
app: App,
|
||||
item: MessageFull,
|
||||
position: Int,
|
||||
adapter: MessagesAdapter
|
||||
) {
|
||||
b.messageSubject.text = item.subject
|
||||
b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort
|
||||
b.messageAttachmentImage.isVisible = item.hasAttachments
|
||||
@ -55,15 +59,17 @@ class MessageViewHolder(
|
||||
b.messageProfileBackground.setImageBitmap(messageInfo.profileImage)
|
||||
b.messageSender.text = messageInfo.profileName
|
||||
|
||||
item.searchHighlightText?.let { highlight ->
|
||||
item.searchHighlightText?.toString()?.let { highlight ->
|
||||
val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity)
|
||||
|
||||
b.messageSubject.text = b.messageSubject.text.asSpannable(
|
||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true)
|
||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true
|
||||
)
|
||||
b.messageSender.text = b.messageSender.text.asSpannable(
|
||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true)
|
||||
StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight),
|
||||
substring = highlight, ignoreCase = true, ignoreDiacritics = true
|
||||
)
|
||||
}
|
||||
|
||||
adapter.onItemClick?.let { listener ->
|
||||
|
@ -9,38 +9,43 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding
|
||||
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.utils.SearchTextWatcher
|
||||
|
||||
class SearchViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false)
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(
|
||||
inflater,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
) : RecyclerView.ViewHolder(b.root), BindableViewHolder<MessagesSearch, MessagesAdapter> {
|
||||
companion object {
|
||||
private const val TAG = "SearchViewHolder"
|
||||
}
|
||||
|
||||
override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) {
|
||||
b.searchEdit.removeTextChangedListener(adapter.textWatcher)
|
||||
b.searchEdit.addTextChangedListener(adapter.textWatcher)
|
||||
override fun onBind(
|
||||
activity: AppCompatActivity,
|
||||
app: App,
|
||||
item: MessagesSearch,
|
||||
position: Int,
|
||||
adapter: MessagesAdapter
|
||||
) {
|
||||
val watcher = SearchTextWatcher(b, adapter.filter, item)
|
||||
b.searchEdit.removeTextChangedListener(watcher)
|
||||
|
||||
/*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener {
|
||||
override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) {
|
||||
item.isFocused = showing
|
||||
}
|
||||
})*/
|
||||
if (adapter.items.isEmpty() || adapter.items.size == adapter.allItems.size)
|
||||
b.searchLayout.helperText = " "
|
||||
else
|
||||
b.searchLayout.helperText =
|
||||
b.root.context.getString(R.string.messages_search_results, adapter.items.size - 1)
|
||||
b.searchEdit.setText(item.searchText)
|
||||
|
||||
/*if (b.searchEdit.text.toString() != item.searchText) {
|
||||
b.searchEdit.setText(item.searchText)
|
||||
b.searchEdit.setSelection(item.searchText.length)
|
||||
}*/
|
||||
|
||||
//b.searchLayout.helperText = app.getString(R.string.messages_search_results, item.count)
|
||||
|
||||
/*if (item.isFocused && !b.searchEdit.isFocused)
|
||||
b.searchEdit.requestFocus()*/
|
||||
b.searchEdit.addTextChangedListener(watcher)
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.utils.colorRes
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -43,6 +45,10 @@ class NotificationsAdapter(
|
||||
val date = Date.fromMillis(item.addedDate).formattedString
|
||||
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
|
||||
|
||||
b.notificationIcon.background = IconicsDrawable(app, item.getLargeIcon()).apply {
|
||||
colorRes = R.color.colorPrimary
|
||||
}
|
||||
|
||||
b.title.text = item.text
|
||||
b.profileDate.text = listOf(
|
||||
item.profileName ?: "",
|
||||
|
@ -7,10 +7,13 @@ package pl.szczodrzynski.edziennik.ui.modules.settings.cards
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import eu.szkolny.font.SzkolnyFont
|
||||
import pl.szczodrzynski.edziennik.App
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.after
|
||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.agenda.AgendaConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog
|
||||
@ -57,6 +60,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
|
||||
}
|
||||
|
||||
override fun getItems() = listOfNotNull(
|
||||
util.createActionItem(
|
||||
text = R.string.menu_agenda_config,
|
||||
icon = CommunityMaterial.Icon.cmd_calendar_outline
|
||||
) {
|
||||
AgendaConfigDialog(activity, reloadOnDismiss = false)
|
||||
},
|
||||
|
||||
util.createActionItem(
|
||||
text = R.string.menu_grades_config,
|
||||
icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline
|
||||
@ -64,6 +74,13 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
|
||||
GradesConfigDialog(activity, reloadOnDismiss = false)
|
||||
},
|
||||
|
||||
util.createActionItem(
|
||||
text = R.string.menu_messages_config,
|
||||
icon = CommunityMaterial.Icon.cmd_calendar_outline
|
||||
) {
|
||||
MessagesConfigDialog(activity, reloadOnDismiss = false)
|
||||
},
|
||||
|
||||
util.createActionItem(
|
||||
text = R.string.menu_attendance_config,
|
||||
icon = CommunityMaterial.Icon.cmd_calendar_remove_outline
|
||||
@ -141,12 +158,15 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
|
||||
else
|
||||
null,
|
||||
|
||||
util.createPropertyItem(
|
||||
text = R.string.settings_register_hide_sticks_from_old,
|
||||
icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline,
|
||||
value = configProfile.grades.hideSticksFromOld
|
||||
) { _, it ->
|
||||
configProfile.grades.hideSticksFromOld = it
|
||||
}
|
||||
if (App.devMode)
|
||||
util.createPropertyItem(
|
||||
text = R.string.settings_register_hide_sticks_from_old,
|
||||
icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline,
|
||||
value = configProfile.grades.hideSticksFromOld
|
||||
) { _, it ->
|
||||
configProfile.grades.hideSticksFromOld = it
|
||||
}
|
||||
else
|
||||
null
|
||||
)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.settings.MiniMenuConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ThemeChooserDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
|
||||
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
|
||||
import pl.szczodrzynski.edziennik.utils.BigNightUtil
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
|
||||
@ -36,6 +37,18 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) {
|
||||
}
|
||||
else null,
|
||||
|
||||
if (BigNightUtil().isDataWielkanocyNearDzisiaj()) // cool klasa for utility to dzień wielkanocy
|
||||
util.createPropertyItem(
|
||||
text = R.string.settings_theme_eggfall_text,
|
||||
subText = R.string.settings_theme_eggfall_subtext,
|
||||
icon = CommunityMaterial.Icon.cmd_egg_easter,
|
||||
value = configGlobal.ui.eggfall
|
||||
) { _, it ->
|
||||
configGlobal.ui.eggfall = it
|
||||
activity.recreate()
|
||||
}
|
||||
else null,
|
||||
|
||||
util.createActionItem(
|
||||
text = R.string.settings_theme_theme_text,
|
||||
subText = Themes.getThemeNameRes(),
|
||||
|
@ -5,16 +5,16 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.timetable
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginTop
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.linkedin.android.tachyon.DayView
|
||||
import com.linkedin.android.tachyon.DayViewConfig
|
||||
import kotlinx.coroutines.*
|
||||
@ -24,14 +24,15 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
|
||||
import pl.szczodrzynski.edziennik.data.db.full.EventFull
|
||||
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
|
||||
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
|
||||
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
|
||||
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR
|
||||
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR
|
||||
import pl.szczodrzynski.edziennik.utils.ListenerScrollView
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import java.util.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.math.min
|
||||
@ -44,75 +45,66 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
private lateinit var app: App
|
||||
private lateinit var activity: MainActivity
|
||||
private lateinit var inflater: AsyncLayoutInflater
|
||||
private lateinit var b: TimetableDayFragmentBinding
|
||||
|
||||
private val job: Job = Job()
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private var timeIndicatorJob: Job? = null
|
||||
|
||||
private lateinit var date: Date
|
||||
private var startHour = DEFAULT_START_HOUR
|
||||
private var endHour = DEFAULT_END_HOUR
|
||||
private var firstEventMinute = 24 * 60
|
||||
private var paddingTop = 0
|
||||
|
||||
private val manager by lazy { app.timetableManager }
|
||||
private val manager
|
||||
get() = app.timetableManager
|
||||
|
||||
// find SwipeRefreshLayout in the hierarchy
|
||||
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
|
||||
// the day ScrollView
|
||||
private val dayScrollDelegate = lazy {
|
||||
val dayScroll = ListenerScrollView(context!!)
|
||||
dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
dayScroll.setOnRefreshLayoutEnabledListener { enabled ->
|
||||
refreshLayout?.isEnabled = enabled
|
||||
}
|
||||
dayScroll
|
||||
}
|
||||
private val dayScroll by dayScrollDelegate
|
||||
// the lesson DayView
|
||||
|
||||
private val dayView by lazy {
|
||||
val dayView = DayView(context!!, DayViewConfig(
|
||||
startHour = startHour,
|
||||
endHour = endHour,
|
||||
dividerHeight = 1.dp,
|
||||
halfHourHeight = 60.dp,
|
||||
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
|
||||
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
|
||||
hourLabelWidth = 40.dp,
|
||||
hourLabelMarginEnd = 10.dp,
|
||||
eventMargin = 2.dp
|
||||
val dayView = DayView(activity, DayViewConfig(
|
||||
startHour = startHour,
|
||||
endHour = endHour,
|
||||
dividerHeight = 1.dp,
|
||||
halfHourHeight = 60.dp,
|
||||
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
|
||||
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
|
||||
hourLabelWidth = 40.dp,
|
||||
hourLabelMarginEnd = 10.dp,
|
||||
eventMargin = 2.dp
|
||||
), true)
|
||||
dayView.setPadding(10.dp)
|
||||
dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
dayScroll.addView(dayView)
|
||||
dayView
|
||||
return@lazy dayView
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
activity = (getActivity() as MainActivity?) ?: return null
|
||||
context ?: return null
|
||||
app = activity.application as App
|
||||
this.inflater = AsyncLayoutInflater(context!!)
|
||||
this.inflater = AsyncLayoutInflater(requireContext())
|
||||
|
||||
date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday()
|
||||
startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR
|
||||
endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR
|
||||
return FrameLayout(activity).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
addView(ProgressBar(activity).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
|
||||
})
|
||||
}
|
||||
|
||||
b = TimetableDayFragmentBinding.inflate(inflater, null, false)
|
||||
return b.root
|
||||
}
|
||||
|
||||
override fun onPageCreated(): Boolean {
|
||||
// observe lesson database
|
||||
app.db.timetableDao().getAllForDate(App.profileId, date).observe(this, Observer { lessons ->
|
||||
app.db.timetableDao().getAllForDate(App.profileId, date).observe(this) { lessons ->
|
||||
launch {
|
||||
val events = withContext(Dispatchers.Default) {
|
||||
app.db.eventDao().getAllByDateNow(App.profileId, date)
|
||||
}
|
||||
processLessonList(lessons, events)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -120,9 +112,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
private fun processLessonList(lessons: List<LessonFull>, events: List<EventFull>) {
|
||||
// no lessons - timetable not downloaded yet
|
||||
if (lessons.isEmpty()) {
|
||||
inflater.inflate(R.layout.timetable_no_timetable, view as FrameLayout?) { view, _, parent ->
|
||||
parent?.removeAllViews()
|
||||
parent?.addView(view)
|
||||
inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ ->
|
||||
b.root.removeAllViews()
|
||||
b.root.addView(view)
|
||||
|
||||
val b = TimetableNoTimetableBinding.bind(view)
|
||||
val weekStart = date.weekStart.stringY_m_d
|
||||
b.noTimetableSync.onClick {
|
||||
@ -143,9 +136,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
}
|
||||
// one lesson indicating a day without lessons
|
||||
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
|
||||
inflater.inflate(R.layout.timetable_no_lessons, view as FrameLayout?) { view, _, parent ->
|
||||
parent?.removeAllViews()
|
||||
parent?.addView(view)
|
||||
inflater.inflate(R.layout.timetable_no_lessons, b.root) { view, _, _ ->
|
||||
b.root.removeAllViews()
|
||||
b.root.addView(view)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -157,12 +150,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
return
|
||||
}
|
||||
|
||||
// clear the root view and add the ScrollView
|
||||
(view as FrameLayout?)?.removeAllViews()
|
||||
(view as FrameLayout?)?.addView(dayScroll)
|
||||
b.scrollView.isVisible = true
|
||||
b.dayFrame.removeView(dayView)
|
||||
b.dayFrame.addView(dayView, 0)
|
||||
|
||||
// Inflate a label view for each hour the day view will display
|
||||
val hourLabelViews = ArrayList<View>()
|
||||
val hourLabelViews = mutableListOf<View>()
|
||||
for (i in dayView.startHour..dayView.endHour) {
|
||||
if (!isAdded)
|
||||
continue
|
||||
@ -171,6 +164,11 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
hourLabelViews.add(hourLabelView)
|
||||
}
|
||||
dayView.setHourLabelViews(hourLabelViews)
|
||||
// measure dayView top padding needed for the timeIndicator
|
||||
hourLabelViews.getOrNull(0)?.let {
|
||||
it.measure(0, 0)
|
||||
paddingTop = it.measuredHeight / 2 + dayView.paddingTop
|
||||
}
|
||||
|
||||
lessons.forEach { it.showAsUnseen = !it.seen }
|
||||
|
||||
@ -201,8 +199,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
|
||||
// Try to recycle an existing event view if there are enough left, otherwise inflate
|
||||
// a new one
|
||||
val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, dayView, false))
|
||||
?: continue
|
||||
val eventView =
|
||||
(if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(
|
||||
R.layout.timetable_lesson,
|
||||
dayView,
|
||||
false
|
||||
)) ?: continue
|
||||
val lb = TimetableLessonBinding.bind(eventView)
|
||||
eventViews += eventView
|
||||
|
||||
@ -290,16 +292,50 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
|
||||
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
|
||||
}
|
||||
|
||||
updateTimeIndicator()
|
||||
|
||||
dayView.setEventViews(eventViews, eventTimeRanges)
|
||||
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
|
||||
dayScroll.scrollTo(0, firstEventTop.toInt())
|
||||
b.scrollView.scrollTo(0, firstEventTop.toInt())
|
||||
|
||||
b.progressBar.isVisible = false
|
||||
}
|
||||
|
||||
private fun updateTimeIndicator() {
|
||||
val time = Time.getNow()
|
||||
val isTimeInView =
|
||||
date == Date.getToday() && time.hour in dayView.startHour..dayView.endHour
|
||||
|
||||
b.timeIndicator.isVisible = isTimeInView
|
||||
b.timeIndicatorMarker.isVisible = isTimeInView
|
||||
if (isTimeInView) {
|
||||
val startTime = Time(dayView.startHour, 0, 0)
|
||||
val seconds = time.inSeconds - startTime.inSeconds * 1f
|
||||
b.timeIndicator.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
topMargin = (seconds * dayView.minuteHeight / 60f).toInt() + paddingTop
|
||||
}
|
||||
b.timeIndicatorMarker.updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
topMargin = b.timeIndicator.marginTop - (16.dp / 2) + (1.dp / 2)
|
||||
}
|
||||
}
|
||||
|
||||
if (timeIndicatorJob == null) {
|
||||
timeIndicatorJob = startCoroutineTimer(repeatMillis = 30000) {
|
||||
updateTimeIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (dayScrollDelegate.isInitialized()) {
|
||||
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
|
||||
dayScroll.scrollTo(0, firstEventTop.toInt())
|
||||
}
|
||||
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
|
||||
b.scrollView.scrollTo(0, firstEventTop.toInt())
|
||||
updateTimeIndicator()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
timeIndicatorJob?.cancel()
|
||||
timeIndicatorJob = null
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ class DateDropdown : TextInputDropDown {
|
||||
}
|
||||
}
|
||||
|
||||
fun pickerDialog() {
|
||||
private fun pickerDialog() {
|
||||
val date = getSelected() as? Date ?: Date.getToday()
|
||||
|
||||
MaterialDatePicker.Builder.datePicker()
|
||||
|
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) Kuba Szczodrzyński 2021-4-14.
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.ui.modules.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import pl.szczodrzynski.edziennik.data.db.AppDb
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.EventType
|
||||
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
|
||||
|
||||
class EventTypeDropdown : TextInputDropDown {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
lateinit var db: AppDb
|
||||
var profileId: Int = 0
|
||||
var onTypeSelected: ((eventType: EventType) -> Unit)? = null
|
||||
|
||||
override fun create(context: Context) {
|
||||
super.create(context)
|
||||
isEnabled = false
|
||||
}
|
||||
|
||||
suspend fun loadItems() {
|
||||
val types = withContext(Dispatchers.Default) {
|
||||
val list = mutableListOf<Item>()
|
||||
|
||||
var types = db.eventTypeDao().getAllNow(profileId)
|
||||
|
||||
if (types.none { it.id in -1L..10L }) {
|
||||
types = db.eventTypeDao().addDefaultTypes(context, profileId)
|
||||
}
|
||||
|
||||
list += types.map {
|
||||
Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply {
|
||||
icon = CommunityMaterial.Icon.cmd_circle
|
||||
sizeDp = 24
|
||||
colorInt = it.color
|
||||
})
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
clear().append(types)
|
||||
isEnabled = true
|
||||
|
||||
setOnChangeListener {
|
||||
when (it.tag) {
|
||||
is EventType -> {
|
||||
// selected an event type
|
||||
onTypeSelected?.invoke(it.tag)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an event type by the [typeId].
|
||||
*/
|
||||
fun selectType(typeId: Long) = select(typeId)
|
||||
|
||||
/**
|
||||
* Select an event type by the [typeId] **if it's not selected yet**.
|
||||
*/
|
||||
fun selectDefault(typeId: Long?) {
|
||||
if (typeId == null || selected != null)
|
||||
return
|
||||
selectType(typeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected event type.
|
||||
* ### Returns:
|
||||
* - null if no valid type is selected
|
||||
* - [EventType] - the selected event type
|
||||
*/
|
||||
fun getSelected(): EventType? {
|
||||
return when (selected?.tag) {
|
||||
is EventType -> selected?.tag as EventType
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.crc16
|
||||
import pl.szczodrzynski.edziennik.data.db.AppDb
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Subject
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.input
|
||||
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
|
||||
|
||||
@ -40,7 +41,7 @@ class SubjectDropdown : TextInputDropDown {
|
||||
var showNoSubject = true
|
||||
var showCustomSubject = false
|
||||
var customSubjectName = ""
|
||||
var onSubjectSelected: ((subjectId: Long?) -> Unit)? = null
|
||||
var onSubjectSelected: ((subject: Subject?) -> Unit)? = null
|
||||
var onCustomSubjectSelected: ((subjectName: String) -> Unit)? = null
|
||||
|
||||
override fun create(context: Context) {
|
||||
@ -73,7 +74,7 @@ class SubjectDropdown : TextInputDropDown {
|
||||
list += subjects.map { Item(
|
||||
it.id,
|
||||
it.longName,
|
||||
tag = it.id
|
||||
tag = it
|
||||
) }
|
||||
|
||||
list
|
||||
@ -91,10 +92,11 @@ class SubjectDropdown : TextInputDropDown {
|
||||
}
|
||||
-1L -> {
|
||||
// no subject
|
||||
deselect()
|
||||
onSubjectSelected?.invoke(null)
|
||||
true
|
||||
false
|
||||
}
|
||||
is Long -> {
|
||||
is Subject -> {
|
||||
// selected a subject
|
||||
onSubjectSelected?.invoke(it.tag)
|
||||
true
|
||||
@ -104,7 +106,7 @@ class SubjectDropdown : TextInputDropDown {
|
||||
}
|
||||
}
|
||||
|
||||
fun customNameDialog() {
|
||||
private fun customNameDialog() {
|
||||
activity ?: return
|
||||
MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle("Własny przedmiot")
|
||||
@ -127,32 +129,37 @@ class SubjectDropdown : TextInputDropDown {
|
||||
.show()
|
||||
}
|
||||
|
||||
fun selectSubject(subjectId: Long) {
|
||||
if (select(subjectId) == null)
|
||||
select(Item(
|
||||
subjectId,
|
||||
"nieznany przedmiot ($subjectId)",
|
||||
tag = subjectId
|
||||
))
|
||||
/**
|
||||
* Select a subject by the [subjectId].
|
||||
*/
|
||||
fun selectSubject(subjectId: Long): Item? {
|
||||
if (subjectId == -1L) {
|
||||
deselect()
|
||||
return null
|
||||
}
|
||||
return select(subjectId)
|
||||
}
|
||||
|
||||
fun selectDefault(subjectId: Long?) {
|
||||
/**
|
||||
* Select a subject by the [subjectId] **if it's not selected yet**.
|
||||
*/
|
||||
fun selectDefault(subjectId: Long?): Item? {
|
||||
if (subjectId == null || selected != null)
|
||||
return
|
||||
selectSubject(subjectId)
|
||||
return null
|
||||
return selectSubject(subjectId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected subject.
|
||||
* ### Returns:
|
||||
* - null if no valid subject is selected
|
||||
* - [Long] - the selected subject's ID
|
||||
* - [Subject] - the selected subject
|
||||
* - [String] - a custom subject name entered, if [showCustomSubject] == true
|
||||
*/
|
||||
fun getSelected(): Any? {
|
||||
return when (selected?.tag) {
|
||||
-1L -> null
|
||||
is Long -> selected?.tag as Long
|
||||
is Subject -> selected?.tag as Subject
|
||||
is String -> selected?.tag as String
|
||||
else -> null
|
||||
}
|
||||
|
@ -5,13 +5,12 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
import pl.szczodrzynski.edziennik.data.db.AppDb
|
||||
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
||||
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
|
||||
|
||||
class TeacherDropdown : TextInputDropDown {
|
||||
@ -19,22 +18,10 @@ class TeacherDropdown : TextInputDropDown {
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private val activity: AppCompatActivity?
|
||||
get() {
|
||||
var context: Context? = context ?: return null
|
||||
if (context is AppCompatActivity) return context
|
||||
while (context is ContextWrapper) {
|
||||
if (context is AppCompatActivity)
|
||||
return context
|
||||
context = context.baseContext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
lateinit var db: AppDb
|
||||
var profileId: Int = 0
|
||||
var showNoTeacher = true
|
||||
var onTeacherSelected: ((teacherId: Long?) -> Unit)? = null
|
||||
var onTeacherSelected: ((teacher: Teacher?) -> Unit)? = null
|
||||
|
||||
override fun create(context: Context) {
|
||||
super.create(context)
|
||||
@ -58,7 +45,7 @@ class TeacherDropdown : TextInputDropDown {
|
||||
list += teachers.map { Item(
|
||||
it.id,
|
||||
it.fullName,
|
||||
tag = it.id
|
||||
tag = it
|
||||
) }
|
||||
|
||||
list
|
||||
@ -71,10 +58,11 @@ class TeacherDropdown : TextInputDropDown {
|
||||
when (it.tag) {
|
||||
-1L -> {
|
||||
// no teacher
|
||||
deselect()
|
||||
onTeacherSelected?.invoke(null)
|
||||
true
|
||||
false
|
||||
}
|
||||
is Long -> {
|
||||
is Teacher -> {
|
||||
// selected a teacher
|
||||
onTeacherSelected?.invoke(it.tag)
|
||||
true
|
||||
@ -84,31 +72,36 @@ class TeacherDropdown : TextInputDropDown {
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTeacher(teacherId: Long) {
|
||||
if (select(teacherId) == null)
|
||||
select(Item(
|
||||
teacherId,
|
||||
"nieznany nauczyciel ($teacherId)",
|
||||
tag = teacherId
|
||||
))
|
||||
/**
|
||||
* Select a teacher by the [teacherId].
|
||||
*/
|
||||
fun selectTeacher(teacherId: Long): Item? {
|
||||
if (teacherId == -1L) {
|
||||
deselect()
|
||||
return null
|
||||
}
|
||||
return select(teacherId)
|
||||
}
|
||||
|
||||
fun selectDefault(teacherId: Long?) {
|
||||
/**
|
||||
* Select a teacher by the [teacherId] **if it's not selected yet**.
|
||||
*/
|
||||
fun selectDefault(teacherId: Long?): Item? {
|
||||
if (teacherId == null || selected != null)
|
||||
return
|
||||
selectTeacher(teacherId)
|
||||
return null
|
||||
return selectTeacher(teacherId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected teacher.
|
||||
* ### Returns:
|
||||
* - null if no valid teacher is selected
|
||||
* - [Long] - the selected teacher's ID
|
||||
* - [Teacher] - the selected teacher
|
||||
*/
|
||||
fun getSelected(): Long? {
|
||||
fun getSelected(): Teacher? {
|
||||
return when (selected?.tag) {
|
||||
-1L -> null
|
||||
is Long -> selected?.tag as Long
|
||||
is Teacher -> selected?.tag as Teacher
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,7 @@
|
||||
package pl.szczodrzynski.edziennik.ui.modules.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import pl.szczodrzynski.edziennik.R
|
||||
@ -20,22 +18,10 @@ class TeamDropdown : TextInputDropDown {
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
private val activity: AppCompatActivity?
|
||||
get() {
|
||||
var context: Context? = context ?: return null
|
||||
if (context is AppCompatActivity) return context
|
||||
while (context is ContextWrapper) {
|
||||
if (context is AppCompatActivity)
|
||||
return context
|
||||
context = context.baseContext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
lateinit var db: AppDb
|
||||
var profileId: Int = 0
|
||||
var showNoTeam = true
|
||||
var onTeamSelected: ((teamId: Long?) -> Unit)? = null
|
||||
var onTeamSelected: ((team: Team?) -> Unit)? = null
|
||||
|
||||
override fun create(context: Context) {
|
||||
super.create(context)
|
||||
@ -59,7 +45,7 @@ class TeamDropdown : TextInputDropDown {
|
||||
list += teams.map { Item(
|
||||
it.id,
|
||||
it.name,
|
||||
tag = it.id
|
||||
tag = it
|
||||
) }
|
||||
|
||||
list
|
||||
@ -72,10 +58,11 @@ class TeamDropdown : TextInputDropDown {
|
||||
when (it.tag) {
|
||||
-1L -> {
|
||||
// no team
|
||||
deselect()
|
||||
onTeamSelected?.invoke(null)
|
||||
true
|
||||
false
|
||||
}
|
||||
is Long -> {
|
||||
is Team -> {
|
||||
// selected a team
|
||||
onTeamSelected?.invoke(it.tag)
|
||||
true
|
||||
@ -85,21 +72,29 @@ class TeamDropdown : TextInputDropDown {
|
||||
}
|
||||
}
|
||||
|
||||
fun selectTeam(teamId: Long) {
|
||||
if (select(teamId) == null)
|
||||
select(Item(
|
||||
teamId,
|
||||
"nieznana grupa ($teamId)",
|
||||
tag = teamId
|
||||
))
|
||||
/**
|
||||
* Select a teacher by the [teamId].
|
||||
*/
|
||||
fun selectTeam(teamId: Long): Item? {
|
||||
if (teamId == -1L) {
|
||||
deselect()
|
||||
return null
|
||||
}
|
||||
return select(teamId)
|
||||
}
|
||||
|
||||
fun selectDefault(teamId: Long?) {
|
||||
/**
|
||||
* Select a team by the [teamId] **if it's not selected yet**.
|
||||
*/
|
||||
fun selectDefault(teamId: Long?): Item? {
|
||||
if (teamId == null || selected != null)
|
||||
return
|
||||
selectTeam(teamId)
|
||||
return null
|
||||
return selectTeam(teamId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a team of the [Team.TYPE_CLASS] type.
|
||||
*/
|
||||
fun selectTeamClass() {
|
||||
select(items.singleOrNull {
|
||||
it.tag is Team && it.tag.type == Team.TYPE_CLASS
|
||||
@ -110,12 +105,12 @@ class TeamDropdown : TextInputDropDown {
|
||||
* Get the currently selected team.
|
||||
* ### Returns:
|
||||
* - null if no valid team is selected
|
||||
* - [Long] - the team's ID
|
||||
* - [Team] - the selected team
|
||||
*/
|
||||
fun getSelected(): Any? {
|
||||
fun getSelected(): Team? {
|
||||
return when (selected?.tag) {
|
||||
-1L -> null
|
||||
is Long -> selected?.tag as Long
|
||||
is Team -> selected?.tag as Team
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ class TimeDropdown : TextInputDropDown {
|
||||
return !noTimetable
|
||||
}
|
||||
|
||||
fun pickerDialog() {
|
||||
private fun pickerDialog() {
|
||||
val time = (getSelected() as? Pair<*, *>)?.first as? Time ?: Time.getNow()
|
||||
|
||||
MaterialTimePicker.Builder()
|
||||
|
@ -45,6 +45,7 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot
|
||||
getLong("id") ?: 0,
|
||||
getString("title") ?: "",
|
||||
getString("text") ?: "",
|
||||
getString("textLong"),
|
||||
getInt("type") ?: 0,
|
||||
getInt("profileId"),
|
||||
getString("profileName"),
|
||||
@ -74,4 +75,4 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot
|
||||
override fun hasStableIds() = true
|
||||
override fun getViewTypeCount() = 1
|
||||
override fun onDestroy() = cursor?.close() ?: Unit
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user