Merge branch 'develop'

This commit is contained in:
Kuba Szczodrzyński 2021-09-11 00:31:21 +02:00
commit d60e622626
No known key found for this signature in database
GPG Key ID: 70CB8A85BA1633CB
87 changed files with 1154 additions and 196 deletions

View File

@ -51,10 +51,12 @@ jobs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 1.8
uses: actions/setup-java@v1
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
java-version: 1.8
distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Clean build artifacts

View File

@ -43,10 +43,12 @@ jobs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 1.8
uses: actions/setup-java@v1
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
java-version: 1.8
distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Clean build artifacts

View File

@ -43,10 +43,12 @@ jobs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 1.8
uses: actions/setup-java@v1
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
java-version: 1.8
distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Clean build artifacts

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
@ -35,6 +36,9 @@ android {
buildTypes {
debug {
minifyEnabled = false
manifestPlaceholders = [
buildTimestamp: 0
]
}
release {
minifyEnabled = true
@ -120,25 +124,25 @@ dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
// Android Jetpack
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.4"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.room:room-runtime:2.2.6"
implementation "androidx.work:work-runtime-ktx:2.5.0"
kapt "androidx.room:room-compiler:2.2.6"
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.room:room-runtime:2.3.0"
implementation "androidx.work:work-runtime-ktx:2.6.0"
kapt "androidx.room:room-compiler:2.3.0"
// Google design libs
implementation "com.google.android.material:material:1.3.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.google.android:flexbox:2.0.1"
// Play Services/Firebase
implementation "com.google.android.gms:play-services-wearable:17.0.0"
implementation "com.google.firebase:firebase-core:18.0.2"
implementation "com.google.firebase:firebase-crashlytics:17.4.0"
implementation "com.google.android.gms:play-services-wearable:17.1.0"
implementation "com.google.firebase:firebase-core:19.0.1"
implementation "com.google.firebase:firebase-crashlytics:18.2.1"
implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } }
// OkHttp, Retrofit, Gson, Jsoup
@ -153,7 +157,7 @@ dependencies {
// Szkolny.eu libraries/forks
implementation "eu.szkolny:android-snowfall:1ca9ea2da3"
implementation "eu.szkolny:agendacalendarview:5431f03098"
implementation "eu.szkolny:agendacalendarview:ac0f3dcf42"
implementation "eu.szkolny:cafebar:5bf0c618de"
implementation "eu.szkolny.fslogin:lib:2.0.0"
implementation "eu.szkolny:material-about-library:1d5ebaf47c"

View File

@ -146,6 +146,7 @@
android:configChanges="orientation|keyboardHidden"
android:theme="@style/Base.Theme.AppCompat" />
<activity android:name=".ui.modules.base.BuildInvalidActivity" />
<activity android:name=".ui.modules.settings.contributors.ContributorsActivity" />
<!-- _____ _
| __ \ (_)

View File

@ -1,13 +1,13 @@
<h3>Wersja 4.8, 2021-05-26</h3>
<h3>Wersja 4.9, 2021-09-11</h3>
<ul>
<li>Dodano ikony dla powiadomień. @Luncenok</li>
<li>Terminarz: opcje konfiguracji, widok kompaktowy, grupowanie wydarzeń, znaczki nieprzeczytanych, nowe ikony i wiele innych usprawnień.</li>
<li>Wiadomości: usprawiono wyszukiwanie - zapisywanie szukanego tekstu po wejściu w wiadomość.</li>
<li>Wiadomości: dodano opcję konfiguracji podpisu przy wysyłaniu wiadomości.</li>
<li>Plan lekcji: dodano znacznik aktualnej pory dnia w planie lekcji.</li>
<li>Powiadomienia: dodano szczegółowy opis po rozwinięciu.</li>
<li>Wydarzenia: nowy rodzaj "lekcja online".</li>
<li>Naprawiono odbieranie nagrody w easter egg'u.</li>
<li>Vulcan: naprawiono brakujące lekcje w planie. @Antoni-Czaplicki</li>
<li>Vulcan: naprawiono wysyłanie wiadomości. @Antoni-Czaplicki</li>
<li>Vulcan: naprawiono brak frekwencji.</li>
<li>Naprawiono eksportowanie planu lekcji oraz pobieranie załączników. @doteq</li>
<li>Mobidziennik: naprawiono możliwość pobierania przyszłego planu lekcji.</li>
<li>Mobidziennik: poprawiono brak nowych linii w wysłanej wiadomości.</li>
<li>Dodano ekran "Twórcy aplikacji" w Ustawieniach. @Pengwius</li>
<li>Zmieniono domyślne tło nagłówka menu. 😋</li>
</ul>
<br>
<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0x71, 0xcf, 0xdf, 0x13, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0x36, 0x60, 0xb0, 0x4b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -58,6 +58,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val profileId
get() = profile.id
var enableChucker = false
var debugMode = false
var devMode = false
}
@ -115,9 +116,11 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
builder.addInterceptor(chuckerInterceptor)
if (enableChucker) {
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
builder.addInterceptor(chuckerInterceptor)
}
}
http = builder.build()
@ -172,6 +175,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
App.profile = Profile(0, 0, 0, "")
debugMode = BuildConfig.DEBUG
devMode = config.debugMode || debugMode
enableChucker = config.enableChucker || devMode
if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) }

View File

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

View File

@ -80,6 +80,11 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mDebugMode = mDebugMode ?: values.get("debugMode", false); return mDebugMode ?: false }
set(value) { set("debugMode", value); mDebugMode = value }
private var mEnableChucker: Boolean? = null
var enableChucker: Boolean
get() { mEnableChucker = mEnableChucker ?: values.get("enableChucker", false); return mEnableChucker ?: false }
set(value) { set("enableChucker", value); mEnableChucker = value }
private var mDevModePassword: String? = null
var devModePassword: String?
get() { mDevModePassword = mDevModePassword ?: values.get("devModePassword", null as String?); return mDevModePassword }

View File

@ -24,7 +24,7 @@ const val FAKE_LIBRUS_ACCOUNTS = "/synergia_accounts.php"
val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp"
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
const val LIBRUS_CLIENT_ID = "0RbsDOkV9tyKEQYzlLv5hs3DM1ukrynFI4p6C1Yc"
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
const val LIBRUS_REDIRECT_URL = "app://librus"
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code"
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"
@ -43,7 +43,7 @@ const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token"
const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST"
const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE="
const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398"
const val LIBRUS_API_CLIENT_ID_JST = "49"
const val LIBRUS_API_CLIENT_ID_JST = "59"
//const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42"
const val LIBRUS_JST_DEMO_CODE = "68656A21"

View File

@ -195,6 +195,7 @@ const val ERROR_VULCAN_HEBE_FIREBASE_ERROR = 362
const val ERROR_VULCAN_HEBE_CERTIFICATE_GONE = 363
const val ERROR_VULCAN_HEBE_SERVER_ERROR = 364
const val ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND = 365
const val ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY = 366
const val ERROR_VULCAN_API_DEPRECATED = 390
const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501

View File

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

View File

@ -111,37 +111,6 @@ class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Dat
val courseStudentEndpoint: String
get() = "Course/$studentId/"
fun getSubject(longId: String, name: String): Subject {
val id = longId.crc32()
return subjectList.singleOrNull { it.id == id } ?: run {
val subject = Subject(profileId, id, name, name)
subjectList.put(id, subject)
subject
}
}
fun getTeacher(firstName: String, lastName: String, longId: String? = null): Teacher {
val name = "$firstName $lastName".fixName()
val id = name.crc32()
return teacherList.singleOrNull { it.id == id }?.also {
if (longId != null && it.loginId == null) it.loginId = longId
} ?: run {
val teacher = Teacher(profileId, id, firstName, lastName, longId)
teacherList.put(id, teacher)
teacher
}
}
fun getTeacherByFirstLast(nameFirstLast: String, longId: String? = null): Teacher {
val nameParts = nameFirstLast.split(" ")
return getTeacher(nameParts[0], nameParts[1], longId)
}
fun getTeacherByLastFirst(nameLastFirst: String, longId: String? = null): Teacher {
val nameParts = nameLastFirst.split(" ")
return getTeacher(nameParts[1], nameParts[0], longId)
}
fun getEventType(longId: String, name: String): EventType {
val id = longId.crc16().toLong()
return eventTypes.singleOrNull { it.id == id } ?: run {

View File

@ -40,7 +40,7 @@ class EdudziennikWebExams(override val data: DataEdudziennik,
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEach
val subjectName = subjectElement.text().trim()
val subject = data.getSubject(subjectId, subjectName)
val subject = data.getSubject(subjectId.crc32(), subjectName)
val dateString = examElement.child(2).text().trim()
if (dateString.isBlank()) return@forEach

View File

@ -53,7 +53,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
val subjectId = subjectElement.id().trim()
val subjectName = subjectElement.child(0).text().trim()
val subject = data.getSubject(subjectId, subjectName)
val subject = data.getSubject(subjectId.crc32(), subjectName)
val gradeType = when {
subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM

View File

@ -41,7 +41,7 @@ class EdudziennikWebHomework(override val data: DataEdudziennik,
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEach
val subjectName = subjectElement.text()
val subject = data.getSubject(subjectId, subjectName)
val subject = data.getSubject(subjectId.crc32(), subjectName)
val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date)
val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime

View File

@ -73,7 +73,7 @@ class EdudziennikWebStart(override val data: DataEdudziennik,
EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach {
val id = it[1].trim()
val name = it[2].trim()
data.getSubject(id, name)
data.getSubject(id.crc32(), name)
}
}
}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.crc32
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
@ -89,7 +90,7 @@ class EdudziennikWebTimetable(override val data: DataEdudziennik,
val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEachIndexed
val subjectName = subjectElement.text().trim()
val subject = data.getSubject(subjectId, subjectName)
val subject = data.getSubject(subjectId.crc32(), subjectName)
/* Getting teacher */

View File

@ -18,6 +18,7 @@ const val ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE = 2050
const val ENDPOINT_MOBIDZIENNIK_WEB_MANUALS = 2100
const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200
const val ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK = 2300 // not used as an endpoint
const val ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE = 2400
const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000
val MobidziennikFeatures = listOf(
@ -38,6 +39,12 @@ val MobidziennikFeatures = listOf(
/**
* Timetable - web scraping - does nothing if the API_MAIN timetable is enough.
*/
Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_TIMETABLE, listOf(
ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_MOBIDZIENNIK_WEB
), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)),
/**
* Agenda - "API" + web scraping.
*/

View File

@ -84,6 +84,10 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
MobidziennikWebManuals(data, lastSync, onSuccess)
}*/
ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE-> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
MobidziennikWebTimetable(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId)
}
}

View File

@ -48,7 +48,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
//syncWeeks.clear()
//syncWeeks += Date.fromY_m_d("2019-12-19")
syncWeeks.minBy { it.value }?.let {
syncWeeks.minByOrNull { it.value }?.let {
data.toRemove.add(DataRemoveModel.Attendance.from(it))
}

View File

@ -0,0 +1,340 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-9-8.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web
import android.annotation.SuppressLint
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
import kotlin.collections.set
import kotlin.text.replace
class MobidziennikWebTimetable(
override val data: DataMobidziennik,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit
) : MobidziennikWeb(data, lastSync) {
companion object {
private const val TAG = "MobidziennikWebTimetable"
}
private val rangesH = mutableMapOf<ClosedFloatingPointRange<Float>, Date>()
private val hoursV = mutableMapOf<Int, Pair<Time, Int?>>()
private var startDate: Date
private fun parseCss(css: String): Map<String, String> {
return css.split(";").mapNotNull {
val spl = it.split(":")
if (spl.size != 2)
return@mapNotNull null
return@mapNotNull spl[0].trim() to spl[1].trim()
}.toMap()
}
private fun getRangeH(h: Float): Date? {
return rangesH.entries.firstOrNull {
h in it.key
}?.value
}
private fun stringToDate(date: String): Date? {
val items = date.split(" ")
val day = items.getOrNull(0)?.toIntOrNull() ?: return null
val year = items.getOrNull(2)?.toIntOrNull() ?: return null
val month = when (items.getOrNull(1)) {
"stycznia" -> 1
"lutego" -> 2
"marca" -> 3
"kwietnia" -> 4
"maja" -> 5
"czerwca" -> 6
"lipca" -> 7
"sierpnia" -> 8
"września" -> 9
"października" -> 10
"listopada" -> 11
"grudnia" -> 12
else -> return null
}
return Date(year, month, day)
}
init {
val currentWeekStart = Week.getWeekStart()
val nextWeekEnd = Week.getWeekEnd().stepForward(0, 0, 7)
if (Date.getToday().weekDay > 4) {
currentWeekStart.stepForward(0, 0, 7)
}
startDate = data.arguments?.getString("weekStart")?.let {
Date.fromY_m_d(it)
} ?: currentWeekStart
val syncFutureDate = startDate > nextWeekEnd
// TODO: 2021-09-09 make DataRemoveModel keep extra lessons
val syncExtraLessons = false && System.currentTimeMillis() - (lastSync ?: 0) > 2 * DAY * MS
if (!syncFutureDate && !syncExtraLessons) {
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE)
}
else {
val types = when {
syncFutureDate -> mutableListOf("podstawowy")//, "pozalekcyjny")
syncExtraLessons -> mutableListOf("pozalekcyjny")
else -> mutableListOf()
}
syncTypes(types, startDate) {
// set as synced now only when not syncing future date
// (to avoid waiting 2 days for normal sync after future sync)
if (syncExtraLessons)
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS)
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE)
}
}
}
private fun syncTypes(types: MutableList<String>, startDate: Date, onSuccess: () -> Unit) {
if (types.isEmpty()) {
onSuccess()
return
}
val type = types.removeAt(0)
webGet(TAG, "/dziennik/planlekcji?typ=$type&tydzien=${startDate.stringY_m_d}") { html ->
MobidziennikLuckyNumberExtractor(data, html)
readRangesH(html)
readRangesV(html)
readLessons(html)
syncTypes(types, startDate, onSuccess)
}
}
private fun readRangesH(html: String) {
val htmlH = Regexes.MOBIDZIENNIK_TIMETABLE_TOP.find(html) ?: return
val docH = Jsoup.parse(htmlH.value)
var posH = 0f
for (el in docH.select("div > div")) {
val css = parseCss(el.attr("style"))
val width = css["width"]
?.trimEnd('%')
?.toFloatOrNull()
?: continue
val value = stringToDate(el.attr("title"))
?: continue
val range = posH.rangeTo(posH + width)
posH += width
rangesH[range] = value
}
}
private fun readRangesV(html: String) {
val htmlV = Regexes.MOBIDZIENNIK_TIMETABLE_LEFT.find(html) ?: return
val docV = Jsoup.parse(htmlV.value)
for (el in docV.select("div > div")) {
val css = parseCss(el.attr("style"))
val top = css["top"]
?.trimEnd('%')
?.toFloatOrNull()
?: continue
val values = el.text().split(" ")
val time = values.getOrNull(0)?.let {
Time.fromH_m(it)
} ?: continue
val num = values.getOrNull(1)?.toIntOrNull()
hoursV[(top * 100).toInt()] = time to num
}
}
private val whitespaceRegex = "\\s+".toRegex()
private val classroomRegex = "\\((.*)\\)".toRegex()
private fun cleanup(str: String): List<String> {
return str
.replace(whitespaceRegex, " ")
.replace("\n", "")
.replace("&lt;small&gt;", "$")
.replace("&lt;/small&gt;", "$")
.replace("&lt;br /&gt;", "\n")
.replace("&lt;br/&gt;", "\n")
.replace("&lt;br&gt;", "\n")
.replace("<br />", "\n")
.replace("<br/>", "\n")
.replace("<br>", "\n")
.replace("<b>", "%")
.replace("</b>", "%")
.replace("<span>", "")
.replace("</span>", "")
.split("\n")
.map { it.trim() }
}
@SuppressLint("LongLogTag", "LogNotTimber")
private fun readLessons(html: String) {
val matches = Regexes.MOBIDZIENNIK_TIMETABLE_CELL.findAll(html)
val noLessonDays = mutableListOf<Date>()
for (i in 0..6) {
noLessonDays.add(startDate.clone().stepForward(0, 0, i))
}
for (match in matches) {
val css = parseCss("${match[1]};${match[2]}")
val left = css["left"]?.trimEnd('%')?.toFloatOrNull() ?: continue
val top = css["top"]?.trimEnd('%')?.toFloatOrNull() ?: continue
val width = css["width"]?.trimEnd('%')?.toFloatOrNull() ?: continue
val height = css["height"]?.trimEnd('%')?.toFloatOrNull() ?: continue
val posH = left + width / 2f
val topInt = (top * 100).toInt()
val bottomInt = ((top + height) * 100).toInt()
val lessonDate = getRangeH(posH) ?: continue
val (startTime, lessonNumber) = hoursV[topInt] ?: continue
val endTime = hoursV[bottomInt]?.first ?: continue
noLessonDays.remove(lessonDate)
var typeName: String? = null
var subjectName: String? = null
var teacherName: String? = null
var classroomName: String? = null
var teamName: String? = null
val items = (cleanup(match[3]) + cleanup(match[4])).toMutableList()
var length = 0
while (items.isNotEmpty() && length != items.size) {
length = items.size
val toRemove = mutableListOf<String?>()
items.forEachIndexed { i, item ->
when {
item.isEmpty() ->
toRemove.add(item)
item.contains(":") && item.contains(" - ") ->
toRemove.add(item)
item.startsWith("%") -> {
subjectName = item.trim('%')
// I have no idea what's going on here
// ok now seriously.. the subject (long or short) item
// may NOT be 0th, as the HH:MM - HH:MM item may be before
// or even the typeName item. As these are always **before**,
// they are removed in previous iterations, so the first not removed
// item should be the long/short subjectName needing to be removed now.
toRemove.add(items[toRemove.size])
// ...and this has to be added later
toRemove.add(item)
}
item.startsWith("&") -> {
typeName = item.trim('&')
toRemove.add(item)
}
typeName != null && (item.contains(typeName!!) || item.contains("</small>")) -> {
toRemove.add(item)
}
item.contains("(") && item.contains(")") -> {
classroomName = classroomRegex.find(item)?.get(1)
items[i] = item.replace("($classroomName)", "").trim()
}
classroomName != null && item.contains(classroomName!!) -> {
items[i] = item.replace("($classroomName)", "").trim()
}
item.contains("class=\"wyjatek tooltip\"") ->
toRemove.add(item)
}
}
items.removeAll(toRemove)
}
if (items.size == 2 && items[0].contains(" - ")) {
val parts = items[0].split(" - ")
teamName = parts[0]
teacherName = parts[1]
}
else if (items.size == 2 && typeName?.contains("odwołana") == true) {
teamName = items[0]
}
else if (items.size == 4) {
teamName = items[0]
teacherName = items[1]
}
val type = when (typeName) {
"zastępstwo" -> Lesson.TYPE_CHANGE
"lekcja odwołana", "odwołana" -> Lesson.TYPE_CANCELLED
else -> Lesson.TYPE_NORMAL
}
val subject = subjectName?.let { data.getSubject(null, it) }
val teacher = teacherName?.let { data.getTeacherByLastFirst(it) }
val team = teamName?.let { data.getTeam(
id = null,
name = it,
schoolCode = data.loginServerName ?: return@let null,
isTeamClass = false
) }
Lesson(data.profileId, -1).also {
it.type = type
if (type == Lesson.TYPE_CANCELLED) {
it.oldDate = lessonDate
it.oldLessonNumber = lessonNumber
it.oldStartTime = startTime
it.oldEndTime = endTime
it.oldSubjectId = subject?.id ?: -1
it.oldTeamId = team?.id ?: -1
}
else {
it.date = lessonDate
it.lessonNumber = lessonNumber
it.startTime = startTime
it.endTime = endTime
it.subjectId = subject?.id ?: -1
it.teacherId = teacher?.id ?: -1
it.teamId = team?.id ?: -1
it.classroom = classroomName
}
it.id = it.buildId()
val seen = profile?.empty == false || lessonDate < Date.getToday()
if (it.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LESSON_CHANGE,
it.id,
seen,
seen
)
)
}
data.lessonList += it
}
}
for (date in noLessonDays) {
data.lessonList += Lesson(data.profileId, date.value.toLong()).also {
it.type = Lesson.TYPE_NO_LESSONS
it.date = date
}
}
}
}

View File

@ -81,39 +81,4 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
val loginShort: String?
get() = studentLogin?.split('@')?.get(0)
fun getSubject(name: String): Subject {
val id = name.crc32()
return subjectList.singleOrNull { it.id == id } ?: run {
val subject = Subject(profileId, id, name, name)
subjectList.put(id, subject)
subject
}
}
fun getTeacher(firstName: String, lastName: String): Teacher {
val name = "$firstName $lastName".fixName()
return teacherList.singleOrNull { it.fullName == name } ?: run {
val id = name.crc32()
val teacher = Teacher(profileId, id, firstName, lastName)
teacherList.put(id, teacher)
teacher
}
}
fun getTeam(name: String? = null): Team {
if (name == "cała klasa" || name == null) return teamClass ?: run {
val id = className!!.crc32()
val teamCode = "$schoolShortName:$className"
val team = Team(profileId, id, className, Team.TYPE_CLASS, teamCode, -1)
teamList.put(id, team)
return team
} else {
val id = name.crc32()
val teamCode = "$schoolShortName:$name"
val team = Team(profileId, id, name, Team.TYPE_VIRTUAL, teamCode, -1)
teamList.put(id, team)
return team
}
}
}

View File

@ -36,7 +36,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List<JsonObject>)
}
val subjectName = grade.getString("SchoolSubject") ?: return@forEach
val subject = data.getSubject(subjectName)
val subject = data.getSubject(null, subjectName)
val addedDate = if (profile.empty) profile.getSemesterStart(semester).inMillis
else System.currentTimeMillis()

View File

@ -34,7 +34,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
val subjectName = grade.getString("SchoolSubject") ?: return@forEach
val subject = data.getSubject(subjectName)
val subject = data.getSubject(null, subjectName)
val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis }
?: System.currentTimeMillis()

View File

@ -22,7 +22,13 @@ class PodlasieApiMain(override val data: DataPodlasie,
init {
apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json ->
data.getTeam() // Save the class team when it doesn't exist.
// Save the class team when it doesn't exist.
data.getTeam(
id = null,
name = data.className ?: "",
schoolCode = data.schoolShortName ?: "",
isTeamClass = true
)
json.getInt("LuckyNumber")?.let { PodlasieApiLuckyNumber(data, it) }
json.getJsonArray("Teacher")?.asJsonObjectList()?.let { PodlasieApiTeachers(data, it) }

View File

@ -43,14 +43,21 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List<JsonObject>) {
val startTime = lesson.getString("TimeFrom")?.let { Time.fromH_m_s(it) }
?: return@forEach
val endTime = lesson.getString("TimeTo")?.let { Time.fromH_m_s(it) } ?: return@forEach
val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(it) }
val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(null, it) }
?: return@forEach
val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach
val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach
val teacher = data.getTeacher(teacherFirstName, teacherLastName)
val team = lesson.getString("Group")?.let { data.getTeam(it) } ?: return@forEach
val team = lesson.getString("Group")?.let {
data.getTeam(
id = null,
name = it,
schoolCode = data.schoolShortName ?: "",
isTeamClass = it == "cała klasa"
)
} ?: return@forEach
val classroom = lesson.getString("Room")
Lesson(data.profileId, -1).also {

View File

@ -222,6 +222,16 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value }
private var mSenderAddressHash: String? = null
var senderAddressHash: String?
get() { mSenderAddressHash = mSenderAddressHash ?: profile?.getStudentData("senderAddressHash", null); return mSenderAddressHash }
set(value) { profile?.putStudentData("senderAddressHash", value) ?: return; mSenderAddressHash = value }
private var mSenderAddressName: String? = null
var senderAddressName: String?
get() { mSenderAddressName = mSenderAddressName ?: profile?.getStudentData("senderAddressName", null); return mSenderAddressName }
set(value) { profile?.putStudentData("senderAddressName", value) ?: return; mSenderAddressName = value }
val apiUrl: String?
get() {
val url = when (apiToken[symbol]?.substring(0, 3)) {

View File

@ -38,7 +38,7 @@ class VulcanHebeAttendance(
lastSync = lastSync
) { list, _ ->
list.forEach { attendance ->
val id = attendance.getLong("AuxPresenceId") ?: return@forEach
val id = attendance.getLong("Id") ?: return@forEach
val type = attendance.getJsonObject("PresenceType") ?: return@forEach
val baseType = getBaseType(type)
val typeName = type.getString("Name") ?: return@forEach

View File

@ -97,6 +97,10 @@ class VulcanHebeMain(
val studentSemesterId = period.getInt("Id") ?: return@forEach
val studentSemesterNumber = period.getInt("Number") ?: return@forEach
val senderEntry = student.getJsonObject("SenderEntry")
val senderAddressName = senderEntry.getString("Address")
val senderAddressHash = senderEntry.getString("AddressHash")
val hebeContext = student.getString("Context")
val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true)
@ -143,6 +147,8 @@ class VulcanHebeMain(
studentData["schoolSymbol"] = schoolSymbol
studentData["schoolShort"] = schoolShort
studentData["schoolName"] = schoolCode
studentData["senderAddressName"] = senderAddressName
studentData["senderAddressHash"] = senderAddressHash
studentData["hebeContext"] = hebeContext
}
dateSemester1Start?.let {

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
@ -27,6 +28,22 @@ class VulcanHebeSendMessage(
}
init {
if (data.senderAddressName == null || data.senderAddressHash == null) {
VulcanHebeMain(data).getStudents(data.profile, null) {
if (data.senderAddressName == null || data.senderAddressHash == null) {
data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY)
}
else {
sendMessage()
}
}
}
else {
sendMessage()
}
}
private fun sendMessage() {
val recipientsArray = JsonArray()
recipients.forEach { teacher ->
recipientsArray += JsonObject(
@ -40,10 +57,10 @@ class VulcanHebeSendMessage(
val senderName = (profile?.accountName ?: profile?.studentNameLong)
?.swapFirstLastName() ?: ""
val sender = JsonObject(
"Address" to senderName,
"Address" to data.senderAddressName,
"LoginId" to data.studentLoginId.toString(),
"Initials" to senderName.getNameInitials(),
"AddressHash" to senderName.sha1Hex()
"AddressHash" to data.senderAddressHash
)
apiPost(

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE
@ -47,7 +48,7 @@ class VulcanHebeTimetable(
?: previousWeekStart
val dateTo = dateFrom.clone().stepForward(0, 0, 13)
val lastSync = null
val lastSync = 0L
apiGetList(
TAG,
@ -106,6 +107,8 @@ class VulcanHebeTimetable(
"Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}"
)
data.toRemove.add(DataRemoveModel.Timetable.between(dateFrom, dateTo))
data.lessonList.addAll(lessonList)
data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS)

View File

@ -2,6 +2,7 @@ package pl.szczodrzynski.edziennik.data.api.models
import android.util.LongSparseArray
import android.util.SparseArray
import androidx.core.util.set
import androidx.core.util.size
import androidx.room.OnConflictStrategy
import com.google.gson.JsonObject
@ -376,4 +377,108 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
fun startProgress(stringRes: Int) {
callback.onStartProgress(stringRes)
}
/* _ _ _ _ _
| | | | | (_) |
| | | | |_ _| |___
| | | | __| | / __|
| |__| | |_| | \__ \
\____/ \__|_|_|__*/
fun getSubject(id: Long?, name: String, shortName: String = name): Subject {
var subject = subjectList.singleOrNull { it.id == id }
if (subject == null)
subject = subjectList.singleOrNull { it.longName == name }
if (subject == null)
subject = subjectList.singleOrNull { it.shortName == name }
if (subject == null) {
subject = Subject(
profileId,
id ?: name.crc32(),
name,
shortName
)
subjectList[subject.id] = subject
}
return subject
}
fun getTeam(id: Long?, name: String, schoolCode: String, isTeamClass: Boolean = false): Team {
if (isTeamClass && teamClass != null)
return teamClass as Team
var team = teamList.singleOrNull { it.id == id }
val namePlain = name.replace(" ", "")
if (team == null)
team = teamList.singleOrNull { it.name.replace(" ", "") == namePlain }
if (team == null) {
team = Team(
profileId,
id ?: name.crc32(),
name,
if (isTeamClass) Team.TYPE_CLASS else Team.TYPE_VIRTUAL,
"$schoolCode:$name",
-1
)
teamList[team.id] = team
}
return team
}
fun getTeacher(firstName: String, lastName: String, loginId: String? = null): Teacher {
val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" }
return validateTeacher(teacher, firstName, lastName, loginId)
}
fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher {
val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" }
return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId)
}
fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher {
val nameParts = nameLastFirst.split(" ")
return if (nameParts.size == 1)
getTeacher(nameParts[0], "", loginId)
else
getTeacher(nameParts[1], nameParts[0], loginId)
}
fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher {
val nameParts = nameFirstLast.split(" ")
return if (nameParts.size == 1)
getTeacher(nameParts[0], "", loginId)
else
getTeacher(nameParts[0], nameParts[1], loginId)
}
fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher {
val nameParts = nameFDotLast.split(".")
return if (nameParts.size == 1)
getTeacher(nameParts[0], "", loginId)
else
getTeacher(nameParts[0][0], nameParts[1], loginId)
}
fun getTeacherByFDotSpaceLast(nameFDotSpaceLast: String, loginId: String? = null): Teacher {
val nameParts = nameFDotSpaceLast.split(".")
return if (nameParts.size == 1)
getTeacher(nameParts[0], "", loginId)
else
getTeacher(nameParts[0][0], nameParts[1], loginId)
}
private fun validateTeacher(teacher: Teacher?, firstName: String, lastName: String, loginId: String?): Teacher {
val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).apply {
id = fullName.crc32()
teacherList[id] = this
}
return obj.also {
if (loginId != null && it.loginId != null)
it.loginId = loginId
if (firstName.length > 1)
it.name = firstName
it.surname = lastName
}
}
}

View File

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

View File

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

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDZ/2nExVD===.$param2".sha256()
return "$param1.MTIzNDU2Nzg5MDkdkClKMQ===.$param2".sha256()
}
}

View File

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

View File

@ -4,11 +4,14 @@
package pl.szczodrzynski.edziennik.ui.dialogs.timetable
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.graphics.*
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.view.View.MeasureSpec
@ -373,25 +376,31 @@ class GenerateBlockTimetableDialog(
val today = Date.getToday().stringY_m_d
val now = Time.getNow().stringH_M_S
val filename = "plan_lekcji_${app.profile.name}_${today}_${now}.png"
val resolver: ContentResolver = activity.applicationContext.contentResolver
val values = ContentValues()
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() }
val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
values.put(MediaStore.MediaColumns.RELATIVE_PATH, File(Environment.DIRECTORY_PICTURES, "Szkolny.eu").path)
} else {
val picturesDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Szkolny.eu")
picturesDirectory.mkdirs()
values.put(MediaStore.MediaColumns.DATA, File(picturesDirectory, filename).path)
}
try {
val fos = FileOutputStream(outputFile)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.close()
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null
resolver.openOutputStream(uri).use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
uri
} catch (e: Exception) {
Log.e("SAVE_IMAGE", e.message, e)
return@withContext null
}
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile)
} else {
Uri.parse("file://" + outputFile.absolutePath)
}
uri
}
progressDialog.dismiss()

View File

@ -21,6 +21,7 @@ import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding
@ -136,9 +137,22 @@ class AgendaFragment : Fragment(), CoroutineScope {
}
}
private suspend fun checkEventTypes() {
withContext(Dispatchers.Default) {
val eventTypes = app.db.eventTypeDao().getAllNow(app.profileId).map {
it.id
}
val defaultEventTypes = EventType.getTypeColorMap().keys
if (!eventTypes.containsAll(defaultEventTypes)) {
app.db.eventTypeDao().addDefaultTypes(activity, app.profileId)
}
}
}
private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch {
if (!isAdded)
return@launch
checkEventTypes()
delay(500)
agendaDefault = AgendaFragmentDefault(activity, app, b)
@ -146,6 +160,7 @@ class AgendaFragment : Fragment(), CoroutineScope {
}}}
private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch {
checkEventTypes()
delay(300)
val dayList = mutableListOf<EventDay>()

View File

@ -9,7 +9,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
class FragmentLazyPagerAdapter(
fragmentManager: FragmentManager,
swipeRefreshLayout: SwipeRefreshLayout,
swipeRefreshLayout: SwipeRefreshLayout? = null,
val fragments: List<Pair<LazyFragment, CharSequence>>
) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) {
override fun getPage(position: Int) = fragments[position].first

View File

@ -5,10 +5,12 @@
package pl.szczodrzynski.edziennik.ui.modules.debug
import android.os.Bundle
import android.os.Process
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.sqlite.db.SimpleSQLiteQuery
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -21,6 +23,7 @@ import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.fslogin.decode
import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
class LabPageFragment : LazyFragment(), CoroutineScope {
companion object {
@ -75,6 +78,37 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
}
b.chucker.isChecked = app.config.enableChucker
b.chucker.onChange { _, isChecked ->
app.config.enableChucker = isChecked
MaterialAlertDialogBuilder(activity)
.setTitle("Restart")
.setMessage("Wymagany restart aplikacji")
.setPositiveButton(R.string.ok) { _, _ ->
Process.killProcess(Process.myPid())
Runtime.getRuntime().exit(0)
exitProcess(0)
}
.setCancelable(false)
.show()
}
b.disableDebug.onClick {
app.config.debugMode = false
MaterialAlertDialogBuilder(activity)
.setTitle("Restart")
.setMessage("Wymagany restart aplikacji")
.setPositiveButton(R.string.ok) { _, _ ->
Process.killProcess(Process.myPid())
Runtime.getRuntime().exit(0)
exitProcess(0)
}
.setCancelable(false)
.show()
}
b.unarchive.onClick {
app.profile.archived = false
app.profile.archiveId = null

View File

@ -166,7 +166,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
json.add("App.profile", app.gson.toJsonTree(app.profile))
json.add("App.profile.studentData", app.profile.studentData)
json.add("App.profile.loginStore", loginStore?.data ?: JsonObject())
json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values)))
json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values.toSortedMap())))
}
adapter.items = LabJsonAdapter.expand(json, 0)
adapter.notifyDataSetChanged()

View File

@ -85,6 +85,9 @@ class LoginFormFragment : Fragment(), CoroutineScope {
if (credential is LoginInfo.FormField) {
val b = LoginFormFieldItemBinding.inflate(layoutInflater)
b.textLayout.hint = app.getString(credential.name)
if (credential.isNumber) {
b.textEdit.inputType = InputType.TYPE_CLASS_NUMBER
}
if (credential.hideText) {
b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE

View File

@ -179,6 +179,7 @@ object LoginInfo {
ERROR_LOGIN_VULCAN_INVALID_PIN_2_REMAINING to R.string.error_312_reason
),
isRequired = true,
isNumber = true,
validationRegex = "[0-9]+",
caseMode = FormField.CaseMode.LOWER_CASE
)
@ -401,6 +402,7 @@ object LoginInfo {
val validationRegex: String,
val caseMode: CaseMode = CaseMode.UNCHANGED,
val hideText: Boolean = false,
val isNumber: Boolean = false,
val stripTextRegex: String? = null
) : BaseCredential(keyName, name, errorCodes) {
enum class CaseMode { UNCHANGED, UPPER_CASE, LOWER_CASE }

View File

@ -76,7 +76,7 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
val maxProfileId = max(
app.db.profileDao().lastId ?: 0,
activity.profiles.maxBy { it.profile.id }?.profile?.id ?: 0
activity.profiles.maxByOrNull { it.profile.id }?.profile?.id ?: 0
)
val loginType = args.getInt("loginType", -1)
val loginMode = args.getInt("loginMode", 0)

View File

@ -503,10 +503,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
text.toString()
}
textHtml = textHtml
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
textHtml = textHtml
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "span")
.replace("</p>", "</span>")
.replace("</p><br>", "</p>")
.replace("<b>", "<strong>")
.replace("</b>", "</strong>")
.replace("<i>", "<em>")

View File

@ -24,6 +24,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsLicenseActivity
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
import pl.szczodrzynski.edziennik.ui.modules.settings.contributors.ContributorsActivity
import pl.szczodrzynski.edziennik.utils.Utils
import kotlin.coroutines.CoroutineContext
@ -90,6 +91,14 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope
it.subText = BuildConfig.VERSION_NAME + ", " + BuildConfig.BUILD_TYPE
},
util.createActionItem(
text = R.string.settings_about_contributors_text,
subText = R.string.settings_about_contributors_subtext,
icon = CommunityMaterial.Icon.cmd_account_group_outline
) {
activity.startActivity(Intent(activity, ContributorsActivity::class.java))
},
util.createMoreItem(card, items = listOf(
util.createActionItem(
text = R.string.settings_about_changelog_text,

View File

@ -88,7 +88,7 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) {
text = R.string.settings_theme_drawer_header_text,
icon = CommunityMaterial.Icon2.cmd_image_outline
) {
if (app.config.ui.appBackground == null) {
if (app.config.ui.headerBackground == null) {
setHeaderBackground()
return@createActionItem
}

View File

@ -0,0 +1,83 @@
package pl.szczodrzynski.edziennik.ui.modules.settings.contributors
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.Bundle
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse
import pl.szczodrzynski.edziennik.databinding.ContributorsActivityBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import kotlin.coroutines.CoroutineContext
class ContributorsActivity : AppCompatActivity(), CoroutineScope {
companion object {
private const val TAG = "ContributorsActivity"
private var contributors: ContributorsResponse? = null
}
private lateinit var app: App
private lateinit var b: ContributorsActivityBinding
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
private val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
app = application as App
b = ContributorsActivityBinding.inflate(layoutInflater)
setContentView(b.root)
b.progressBar.isVisible = true
b.tabLayout.isVisible = false
b.viewPager.isVisible = false
launch {
contributors = contributors ?: SzkolnyApi(app).runCatching(errorSnackbar) {
getContributors()
} ?: return@launch
val pagerAdapter = FragmentLazyPagerAdapter(
supportFragmentManager,
fragments = listOf(
ContributorsFragment().apply {
arguments = Bundle(
"items" to contributors!!.contributors.toTypedArray(),
"quantityPluralRes" to R.plurals.contributions_quantity,
)
} to getString(R.string.contributors),
ContributorsFragment().apply {
arguments = Bundle(
"items" to contributors!!.translators.toTypedArray(),
"quantityPluralRes" to R.plurals.translations_quantity,
)
} to getString(R.string.translators),
)
)
b.viewPager.apply {
offscreenPageLimit = 1
adapter = pagerAdapter
b.tabLayout.setupWithViewPager(this)
}
b.progressBar.isVisible = false
b.tabLayout.isVisible = true
b.viewPager.isVisible = true
}
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-9-7.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings.contributors
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.PluralsRes
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse
import pl.szczodrzynski.edziennik.databinding.ContributorsListItemBinding
import pl.szczodrzynski.edziennik.plural
import pl.szczodrzynski.edziennik.setText
import pl.szczodrzynski.edziennik.utils.Utils
class ContributorsAdapter(
val activity: AppCompatActivity,
val items: List<ContributorsResponse.Item>,
@PluralsRes
val quantityPluralRes: Int
) : RecyclerView.Adapter<ContributorsAdapter.ViewHolder>() {
companion object {
private const val TAG = "ContributorsAdapter"
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = ContributorsListItemBinding.inflate(inflater, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
val b = holder.b
b.text.text = item.name ?: item.login
b.subtext.setText(
R.string.contributors_subtext_format,
item.login,
activity.plural(
quantityPluralRes,
item.contributions ?: 0
)
)
b.image.load(item.avatarUrl) {
transformations(CircleCropTransformation())
}
b.root.setOnClickListener {
Utils.openUrl(activity, item.itemUrl)
}
}
override fun getItemCount() = items.size
class ViewHolder(val b: ContributorsListItemBinding) : RecyclerView.ViewHolder(b.root)
}

View File

@ -0,0 +1,47 @@
package pl.szczodrzynski.edziennik.ui.modules.settings.contributors
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse
import pl.szczodrzynski.edziennik.databinding.ContributorsListFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
class ContributorsFragment : LazyFragment() {
companion object {
private const val TAG = "ContributorsFragment"
}
private lateinit var app: App
private lateinit var activity: ContributorsActivity
private lateinit var b: ContributorsListFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as ContributorsActivity?) ?: return null
context ?: return null
app = activity.application as App
b = ContributorsListFragmentBinding.inflate(inflater)
return b.root
}
override fun onPageCreated(): Boolean {
val contributorsArray = requireArguments().getParcelableArray("items") as Array<ContributorsResponse.Item>
val contributors = contributorsArray.toList()
val quantityPluralRes = requireArguments().getInt("quantityPluralRes")
val adapter = ContributorsAdapter(activity, contributors, quantityPluralRes)
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
addOnScrollListener(onScrollListener)
}
return true
}
}

View File

@ -151,7 +151,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
}
b.scrollView.isVisible = true
b.dayFrame.removeView(b.dayView)
b.dayFrame.removeView(dayView)
b.dayFrame.addView(dayView, 0)
// Inflate a label view for each hour the day view will display

View File

@ -120,8 +120,8 @@ class TimetableFragment : Fragment(), CoroutineScope {
}
val lessonRanges = app.db.lessonRangeDao().getAllNow(App.profileId)
startHour = lessonRanges.map { it.startTime.hour }.min() ?: DEFAULT_START_HOUR
endHour = lessonRanges.map { it.endTime.hour }.max()?.plus(1) ?: DEFAULT_END_HOUR
startHour = lessonRanges.map { it.startTime.hour }.minOrNull() ?: DEFAULT_START_HOUR
endHour = lessonRanges.map { it.endTime.hour }.maxOrNull()?.plus(1) ?: DEFAULT_END_HOUR
}
deferred.await()
if (!isAdded)

View File

@ -37,9 +37,7 @@ class AttachmentsView @JvmOverloads constructor(
}
private val storageDir by lazy {
val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu")
storageDir.mkdirs()
storageDir
Utils.getStorageDir()
}
fun init(arguments: Bundle, owner: Any) {
@ -82,6 +80,7 @@ class AttachmentsView @JvmOverloads constructor(
list.adapter = adapter
list.apply {
setHasFixedSize(false)
isNestedScrollingEnabled = false
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
}

View File

@ -776,7 +776,8 @@ public class Utils {
public static File getStorageDir() {
if (storageDir != null)
return storageDir;
storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu");
storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
storageDir = new File(storageDir, "Szkolny.eu");
storageDir.mkdirs();
return storageDir;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -24,6 +24,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/attendances_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -152,6 +152,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/attendances_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -22,6 +22,7 @@
android:layout_gravity="center_horizontal"
android:layout_margin="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/card_grades_no_data"
android:textSize="16sp" />

View File

@ -24,6 +24,7 @@
android:layout_gravity="center_horizontal"
android:layout_margin="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/card_grades_no_data"
android:textSize="16sp" />

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
tools:visibility="gone" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?actionBarSize">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="?actionBarSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="?actionBarSize"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.7">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:scaleType="center"
android:scaleX="0.8"
android:scaleY="0.8"
android:src="@mipmap/ic_splash" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="64dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/app_name"
android:textAppearance="@style/NavView.TextView.Large"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="64dp"
android:layout_marginBottom="48dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/contributors_headline"
android:textAppearance="@style/NavView.TextView.Medium" />
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_gravity="bottom"
android:background="?android:colorBackground"
android:foreground="@color/colorSurface_2dp"
android:minHeight="?actionBarSize"
android:visibility="gone"
app:tabIndicatorColor="?colorPrimary"
app:tabMode="auto"
app:tabSelectedTextColor="?colorPrimary"
app:tabTextColor="?android:textColorPrimary"
tools:visibility="visible" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/contributors_list_item" />
</layout>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/image"
android:layout_width="72dp"
android:layout_height="72dp"
android:padding="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_account_circle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical"
android:paddingHorizontal="8dp"
android:paddingVertical="16dp">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/NavView.TextView.Subtitle"
tools:text="der Librüsch" />
<TextView
android:id="@+id/subtext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/NavView.TextView.Helper"
tools:text="[at]librüsch - ∞ contributions" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -32,6 +32,7 @@
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/grades_stats_no_data"
android:textSize="16sp"
android:visibility="gone"

View File

@ -29,6 +29,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -24,6 +24,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/homework_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -3,7 +3,8 @@
~ Copyright (c) Kuba Szczodrzyński 2020-4-3.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
tools:ignore="HardcodedText">
@ -38,6 +39,12 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />-->
<Switch
android:id="@+id/chucker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Chucker" />
<Button
android:id="@+id/last10unseen"
android:layout_width="match_parent"
@ -101,6 +108,14 @@
android:layout_height="wrap_content"
android:checked="@={app.config.archiverEnabled}"
android:text="Archiver enabled" />
<Button
android:id="@+id/disableDebug"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Disable Dev Mode"
android:textAllCaps="false"
app:backgroundTint="@color/windowBackgroundRed" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -57,7 +57,7 @@
android:visibility="visible"
tools:visibility="gone"/>
<ScrollView
<androidx.core.widget.NestedScrollView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="0dp"
@ -306,6 +306,6 @@
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</layout>

View File

@ -24,6 +24,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/messages_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -24,6 +24,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/notifications_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -29,6 +29,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -24,6 +24,7 @@
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"

View File

@ -36,12 +36,6 @@
android:layout_marginHorizontal="8dp"
android:background="@color/md_red_500"
tools:layout_marginTop="100dp" />
<com.linkedin.android.tachyon.DayView
android:id="@+id/dayView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_height="match_parent" />
</FrameLayout>
</pl.szczodrzynski.edziennik.utils.ListenerScrollView>

View File

@ -856,7 +856,7 @@
<string name="settings_about_licenses_text">Open-Source-Lizenzen</string>
<string name="settings_about_privacy_policy_text">Datenschutzrichtlinie</string>
<string name="settings_card_register_title">E-Klassenbuch</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - Mai 2021</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - Juni 2021</string>
<string name="settings_about_update_subtext">Klicken Sie hier, um nach Aktualisierungen zu suchen</string>
<string name="settings_about_update_text">Aktualisierung</string>
<string name="settings_about_version_text">Version</string>
@ -1234,4 +1234,6 @@
<string name="you_are_offline_title">Netzwerkverbindung</string>
<string name="login_summary_account_child">(Kind)</string>
<string name="login_summary_account_parent">(Elternteil)</string>
<string name="settings_about_contributors_text">Anwendungsentwickler</string>
<string name="settings_about_contributors_subtext">Liste der Szkolny-Entwickler</string>
</resources>

View File

@ -55,4 +55,13 @@
<item quantity="one">%1$s - %2$d unread</item>
<item quantity="other">%1$s - %2$d unread</item>
</plurals>
</resources>
<plurals name="contributions_quantity">
<item quantity="one">%d contribution</item>
<item quantity="other">%d contributions</item>
</plurals>
<plurals name="translations_quantity">
<item quantity="one">%d translation</item>
<item quantity="other">%d translations</item>
</plurals>
</resources>

View File

@ -858,7 +858,7 @@
<string name="settings_about_licenses_text">Open-source licenses</string>
<string name="settings_about_privacy_policy_text">Privacy policy</string>
<string name="settings_card_register_title">E-register</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - May 2021</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - June 2021</string>
<string name="settings_about_update_subtext">Click to check for updates</string>
<string name="settings_about_update_text">Update</string>
<string name="settings_about_version_text">Version</string>
@ -1237,6 +1237,10 @@
<string name="permissions_attachment">In order to download the file, you have to grant file storage permission for the application.\n\nClick OK to grant the permission.</string>
<string name="permissions_denied">You denied the required permissions for the application.\n\nIn order to grant the permission, open the Permissions screen for Szkolny.eu in phone settings.\n\nClick OK to open app settings now.</string>
<string name="permissions_required">Required permissions</string>
<string name="settings_about_contributors_text">App contributors</string>
<string name="settings_about_contributors_subtext">List of Szkolny.eu contributors</string>
<string name="contributors">Contributors</string>
<string name="translators">Translators</string>
<string name="settings_register_hide_sticks_from_old">Hide sticks from old</string>
<string name="build_official">Official build</string>
<string name="build_platform_play">Google Play</string>

View File

@ -165,6 +165,7 @@
<string name="error_363" translatable="false">ERROR_VULCAN_HEBE_CERTIFICATE_GONE</string>
<string name="error_364" translatable="false">ERROR_VULCAN_HEBE_SERVER_ERROR</string>
<string name="error_365" translatable="false">ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND</string>
<string name="error_366" translatable="false">ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY</string>
<string name="error_390" translatable="false">ERROR_VULCAN_API_DEPRECATED</string>
<string name="error_501" translatable="false">ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN</string>
@ -363,6 +364,7 @@
<string name="error_363_reason">VULCAN®: urządzenie usunięte. Zaloguj się ponownie do dziennika.</string>
<string name="error_364_reason">VULCAN®: błąd serwera. Dziennik może być przeciążony.</string>
<string name="error_365_reason">VULCAN®: nie znaleziono bytu</string>
<string name="error_366_reason">Błąd wysyłania wiadomości - brak informacji o nadawcy.</string>
<string name="error_390_reason">W związku z wygaszeniem aplikacji Dzienniczek+ przez firmę Vulcan, należy zalogować się ponownie.\n\nAby móc dalej korzystać z aplikacji Szkolny.eu, otwórz Ustawienia i wybierz opcję Dodaj nowego ucznia.\nNastępnie zaloguj się do dziennika Vulcan zgodnie z instrukcją.\n\nPrzepraszamy za niedogodności.</string>
<string name="error_501_reason">Błędny email lub hasło</string>

View File

@ -159,4 +159,15 @@
<item quantity="few">%d oceny</item> <!-- 2, 3, 4, 32, 33, 34 -->
<item quantity="other">%d ocen</item> <!-- 5, 10, 12, 13, 21, 25 -->
</plurals>
<plurals name="contributions_quantity">
<item quantity="one">%d commit</item>
<item quantity="few">%d commity</item>
<item quantity="other">%d commit\'ów</item>
</plurals>
<plurals name="translations_quantity">
<item quantity="one">%d tłumaczenie</item>
<item quantity="few">%d tłumaczenia</item>
<item quantity="other">%d tłumaczeń</item>
</plurals>
</resources>

View File

@ -921,7 +921,7 @@
<string name="settings_about_licenses_text">Licencje open-source</string>
<string name="settings_about_privacy_policy_text">Polityka prywatności</string>
<string name="settings_card_register_title">E-dziennik</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nwrzesień 2018 - maj 2021</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nwrzesień 2018 - czerwiec 2021</string>
<string name="settings_about_update_subtext">Kliknij, aby sprawdzić aktualizacje</string>
<string name="settings_about_update_text">Aktualizacja</string>
<string name="settings_about_version_text">Wersja</string>
@ -1391,6 +1391,10 @@
<string name="see_also">Zobacz także</string>
<string name="settings_about_homepage_text">Wejdź na stronę aplikacji</string>
<string name="settings_about_homepage_subtext">Uzyskaj pomoc lub wesprzyj autorów</string>
<string name="settings_about_contributors_text">Twórcy aplikacji</string>
<string name="settings_about_contributors_subtext">Lista twórców Szkolnego</string>
<string name="contributors">Współtwórcy</string>
<string name="translators">Tłumacze</string>
<string name="settings_about_github_text">Kod źródłowy</string>
<string name="settings_about_github_subtext">Pomóż w rozwoju aplikacji na GitHubie</string>
<string name="profile_config_name_hint">Nazwa profilu</string>
@ -1457,4 +1461,6 @@
<string name="notification_grade_long_format">Ocena: %s (waga %s)\nPrzedmiot: %s\nKategoria: %s\nOpis: %s\nNauczyciel: %s</string>
<string name="notification_notice_long_format">Rodzaj: %s\nNauczyciel: %s\nTreść: %s</string>
<string name="notification_attendance_long_format">Rodzaj: %s\nTermin: %s, %s\nNr lekcji: %s\nPrzedmiot: %s\nNauczyciel: %s\nTemat lekcji: %s</string>
<string name="contributors_subtext_format" translatable="false">\@%s - %s</string>
<string name="contributors_headline">Najłatwiejszy sposób na korzystanie z e-dziennika.</string>
</resources>

View File

@ -2,11 +2,11 @@
buildscript {
ext {
kotlin_version = '1.4.31'
kotlin_version = '1.5.20'
release = [
versionName: "4.8",
versionCode: 4080099
versionName: "4.9",
versionCode: 4090099
]
setup = [
@ -21,10 +21,10 @@ buildscript {
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.0-beta06"
classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.5'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
}
}

View File

@ -1,6 +1,6 @@
#Wed Feb 17 14:04:38 CET 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME