Compare commits

...

31 Commits

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

* Optimize code and UX

* Update some code

* Update attendance layout

* Fix timetable view

* Bump iconics version

* Change umbrella icon

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

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

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

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

* Update lesson cell margins

* Add attendance info in lesson dialog

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-09-22 00:03:31 +02:00
3700a71c39 Merge pull request #75 from szkolny-eu/feature/availability-refactor
[API] Refactor register availability checking module.
2021-09-18 21:13:01 +02:00
60f0628f5e [Lab] Fix disabling Chucker and Dev Mode. Add new Lab options. 2021-09-18 16:37:24 +02:00
80dcd9aa69 [API] Refactor register availability checking module. 2021-09-18 16:36:17 +02:00
91b685576b [4.9] Update build.gradle, signing and changelog. 2021-09-10 23:54:09 +02:00
2e3e3dcf3c [Lab] Add button to disable devmode and Chucker toggle. (#70)
* Add option to disable/enable chucker and option to disable dev mode from lab page

* Change "chucker" to "enableChucker"

* Update App.kt
2021-09-10 23:47:46 +02:00
118f5e1794 [API/Vulcan] Fix missing attendance. (#72) 2021-09-10 23:45:29 +02:00
e902352a4b Merge pull request #69 from szkolny-eu/hotfix/message-attachments
[Messages] Fix downloading and displaying attachment on API 30+.
2021-09-10 23:44:26 +02:00
2f7fcb6dc3 [API/Mobidziennik] Fix Web timetable scrapper. 2021-09-10 17:41:39 +02:00
21ddb9d706 [Git] Update .gitignore for .idea. 2021-09-10 17:20:12 +02:00
efa63452e7 [App] Fix Apply Changes not working due to manifest changes. 2021-09-10 17:17:31 +02:00
83f84de019 [UI] Fix attachments view cut off on API 30+. 2021-09-10 17:11:21 +02:00
b9aca981e5 [App] Change app-wide storage dir to Download subfolder. 2021-09-10 16:56:52 +02:00
5913707519 [UI] Use number keyboard in the PIN field in Vulcan. (#68) 2021-09-10 06:49:17 +02:00
dd6a2c0979 [API/Mobidziennik] Add Web timetable scrapper. (#66)
* [API] Add utils for getting teacher, subject and team by name.

* [API/Mobidziennik] Add Web timetable scrapper.

* [API/Mobidziennik] Add missing Regexes.
2021-09-09 23:14:24 +02:00
9fdee6e0c7 [UI] Fix restoring header background dialog. (#65) 2021-09-09 23:14:07 +02:00
b31bf5c1ab [API/Vulcan] Fix missing timetable entries. (#67) 2021-09-09 23:13:39 +02:00
cf4906f2f4 [API/Vulcan] Fix sending messages. (#64)
* Fix sending message

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

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-09-09 18:52:04 +02:00
680a5dfea3 [Mobidziennik] Fix missing linebreaks when sending messages. (#63) 2021-09-08 22:49:14 +02:00
c1062cd7ed [UI] Update drawer header background. (#62) 2021-09-08 22:49:00 +02:00
8edc581f0b [UI] Fix multiplicated day dialog in Agenda. (#61) 2021-09-08 22:48:48 +02:00
ea9d801d08 [DB] Workaround missing event types after profile archiving. (#60) 2021-09-08 22:48:35 +02:00
8f72e11d0c [UI] Center "no data" text view. (#59) 2021-09-08 22:48:21 +02:00
452271e8c0 [UI] Add list of contributors in Settings. (#15)
* Contributors item in settings

* Move contributors activity to settings package && actualize branch

* Update AndroidManifest.xml

* Getting contributors from github api

* Cleaning code

* Fetching data from szkolny api, displaying content, a lot of changes :D

* Strings

* Remove androidx legacy library

* Revert manifest changes

* Remove logging in SzkolnyApi

* Fix app name spelling

* Revert changes to dimens.xml

* Refactor contributors code

* Revert changes to dimens.xml

Again

* Revert changes to build.gradle

* Revert changes to gradle-wrapper.properties

* Revert changes to gradle.properties

* Make user name nullable

* Add caching, refactor plurals, add progress bar

* Update contributors UI

* Shorten activity name in manifest

* Remove unneeded line break

* Remove fragment_translators.xml

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-09-08 19:11:14 +02:00
7b4effe889 [UI] Fix block timetable export. (#57)
* Use MediaStore on SDK level >= 29 for timetable export

* Fix imports

* Use subdirectory in the Photos folder

* Use MediaStore for all API levels

* Remove not-null assertion

* Remove unnecessary outputStream close.

* Use File constructor for directory path

* Use File(File, String) constructor

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2021-09-07 22:11:29 +02:00
e2bf48d1b6 [Actions] Use JDK 11. 2021-09-02 18:27:51 +02:00
c88056ddb9 [Gradle] Update library dependencies. 2021-09-02 17:49:01 +02:00
96dbb0a057 [Gradle] Update Kotlin to v1.5.20. 2021-09-02 17:48:02 +02:00
288c80ea26 [Gradle] Update wrapper and AGP to match AS 2020.3.1. 2021-09-02 17:40:34 +02:00
104 changed files with 1523 additions and 358 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

9
.idea/discord.xml generated
View File

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

6
.idea/kotlinc.xml generated
View File

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

View File

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

View File

@ -1,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"
@ -168,10 +172,10 @@ dependencies {
kapt "eu.szkolny.selective-dao:codegen:27f8f3f194"
// Iconics & related
implementation "com.mikepenz:iconics-core:5.3.0-b01"
implementation "com.mikepenz:iconics-views:5.3.0-b01"
implementation "com.mikepenz:iconics-core:5.3.1"
implementation "com.mikepenz:iconics-views:5.3.1"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
implementation "eu.szkolny:szkolny-font:1.3"
implementation "eu.szkolny:szkolny-font:77e33acc2a"
// Other dependencies
implementation "cat.ereza:customactivityoncrash:2.3.0"

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,7 +1,6 @@
<h3>Wersja 4.8.2, 2021-06-15</h3>
<h3>Wersja 4.10, 2021-09-22</h3>
<ul>
<li>Poprawiono funkcje logowania. @6Arin9</li>
<li>Naprawiono zatrzymanie aplikacji na ekranie planu lekcji. @doteq</li>
<li>Dodano wyświetlanie informacji o frekwencji w planie lekcji. @Antoni-Czaplicki</li>
</ul>
<br>
<br>

View File

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

View File

@ -58,6 +58,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val profileId
get() = profile.id
var enableChucker = false
var debugMode = false
var devMode = false
}
@ -70,6 +71,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val permissionManager by lazy { PermissionManager(this) }
val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) }
val db
get() = App.db
@ -115,9 +117,11 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
builder.addInterceptor(chuckerInterceptor)
if (enableChucker) {
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
builder.addInterceptor(chuckerInterceptor)
}
}
http = builder.build()
@ -171,7 +175,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
App.config = Config(App.db)
App.profile = Profile(0, 0, 0, "")
debugMode = BuildConfig.DEBUG
devMode = config.debugMode || debugMode
devMode = config.devMode ?: debugMode
enableChucker = config.enableChucker ?: devMode
if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) }

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,15 +8,20 @@ import android.content.Intent
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
import pl.szczodrzynski.edziennik.onClick
@ -24,6 +29,7 @@ import pl.szczodrzynski.edziennik.setText
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
@ -34,6 +40,7 @@ import kotlin.coroutines.CoroutineContext
class LessonDetailsDialog(
val activity: AppCompatActivity,
val lesson: LessonFull,
val attendance: AttendanceFull? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
@ -52,6 +59,8 @@ class LessonDetailsDialog(
private lateinit var adapter: EventListAdapter
private val manager
get() = app.timetableManager
private val attendanceManager
get() = app.attendanceManager
init { run {
if (activity.isFinishing)
@ -170,6 +179,27 @@ class LessonDetailsDialog(
b.teamName = lesson.teamName
}
b.attendanceDivider.isVisible = attendance != null
b.attendanceLayout.isVisible = attendance != null
if (attendance != null) {
b.attendanceView.setAttendance(attendance, app.attendanceManager, bigView = true)
b.attendanceType.text = attendance.typeName
b.attendanceIcon.isVisible = attendance.let {
val icon = attendanceManager.getAttendanceIcon(it) ?: return@let false
val color = attendanceManager.getAttendanceColor(it)
b.attendanceIcon.setImageDrawable(
IconicsDrawable(activity, icon).apply {
colorInt = color
sizeDp = 24
}
)
true
}
b.attendanceDetails.onClick {
AttendanceDetailsDialog(activity, attendance, onShowListener, onDismissListener)
}
}
adapter = EventListAdapter(
activity,
showWeekDay = false,

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

@ -41,6 +41,7 @@ class LabFragment : Fragment(), CoroutineScope {
app = activity.application as App
b = TemplateFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
b.refreshLayout.isEnabled = false
return b.root
}

View File

@ -5,15 +5,18 @@
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
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileRemoveDialog
@ -21,6 +24,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,12 +79,51 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
}
b.chucker.isChecked = App.enableChucker
b.chucker.onChange { _, isChecked ->
app.config.enableChucker = isChecked
App.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.devMode = false
App.devMode = 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
app.profileSave()
}
b.resetCert.onClick {
app.config.apiInvalidCert = null
}
b.rebuildConfig.onClick {
App.config = Config(App.db)
}
val profiles = app.db.profileDao().allNow
b.profile.clear()
b.profile += profiles.map { TextInputDropDown.Item(it.id.toLong(), "${it.id} ${it.name} archived ${it.archived}", tag = it) }

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

@ -163,10 +163,9 @@ class HomeFragment : Fragment(), CoroutineScope {
if (app.profile.archived)
items.add(0, HomeArchiveCard(101, app, activity, this, app.profile))
val status = app.config.sync.registerAvailability[app.profile.registerName]
val status = app.availabilityManager.check(app.profile, cacheOnly = true)?.status
val update = app.config.update
if (update != null && update.versionCode > BuildConfig.VERSION_CODE
|| status != null && (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)) {
if (update != null && update.versionCode > BuildConfig.VERSION_CODE || status?.userMessage != null) {
items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile))
}

View File

@ -50,7 +50,8 @@ class HomeAvailabilityCard(
}
holder.root += b.root
val status = app.config.sync.registerAvailability[profile.registerName]
val error = app.availabilityManager.check(profile, cacheOnly = true)
val status = error?.status
val update = app.config.update
if (update == null && status == null)
@ -58,7 +59,8 @@ class HomeAvailabilityCard(
var onInfoClick = { _: View -> }
if (status != null && !status.available && status.userMessage != null) {
// show "register unavailable" only when disabled
if (status?.userMessage != null) {
b.homeAvailabilityTitle.text = HtmlCompat.fromHtml(status.userMessage.title, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityUpdate.isVisible = false
@ -69,6 +71,7 @@ class HomeAvailabilityCard(
RegisterUnavailableDialog(activity, status)
}
}
// show "update available" when available OR version too old for the register
else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) {
b.homeAvailabilityTitle.setText(R.string.home_availability_title)
b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName)
@ -78,6 +81,9 @@ class HomeAvailabilityCard(
UpdateAvailableDialog(activity, update)
}
}
else {
b.root.isVisible = false
}
b.homeAvailabilityUpdate.onClick {
if (update == null)

View File

@ -26,13 +26,12 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
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.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
@ -269,52 +268,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
}
private suspend fun checkAvailability(loginType: Int): Boolean {
when (loginType) {
LOGIN_TYPE_LIBRUS -> "librus"
LOGIN_TYPE_VULCAN -> "vulcan"
LOGIN_TYPE_IDZIENNIK -> "idziennik"
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null
}?.let { registerName ->
var status = app.config.sync.registerAvailability[registerName]
if (status == null || status.nextCheckAt < currentTimeUnix()) {
val api = SzkolnyApi(app)
val result = withContext(Dispatchers.IO) {
return@withContext api.runCatching({
val availability = getRegisterAvailability()
app.config.sync.registerAvailability = availability
availability[registerName]
}, onError = {
if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) {
return@withContext false
}
return@withContext it
})
}
val error = withContext(Dispatchers.IO) {
app.availabilityManager.check(loginType)
} ?: return true
when (result) {
false -> {
Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
return@let
}
is Throwable -> {
activity.errorSnackbar.addError(result.toApiError(TAG)).show()
return false
}
is RegisterAvailabilityStatus -> {
status = result
}
}
return when (error.type) {
Type.NOT_AVAILABLE -> {
RegisterUnavailableDialog(activity, error.status!!)
false
}
if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) {
if (status != null)
RegisterUnavailableDialog(activity, status)
return false
Type.API_ERROR -> {
activity.errorSnackbar.addError(error.apiError!!).show()
false
}
Type.NO_API_ACCESS -> {
Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
true
}
}
return true
}
}

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

@ -53,7 +53,7 @@ class LoginPrizeFragment : Fragment(), CoroutineScope {
.setTitle(R.string.are_you_sure)
.setMessage(R.string.dev_mode_enable_warning)
.setPositiveButton(R.string.yes) { _, _ ->
app.config.debugMode = true
app.config.devMode = true
App.devMode = true
MaterialAlertDialogBuilder(activity)
.setTitle("Restart")
@ -67,8 +67,8 @@ class LoginPrizeFragment : Fragment(), CoroutineScope {
.show()
}
.setNegativeButton(R.string.no) { _, _ ->
app.config.debugMode = false
App.devMode = false
app.config.devMode = App.debugMode
App.devMode = App.debugMode
activity.finish()
}
.show()

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

@ -9,19 +9,24 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
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.core.view.updateLayoutParams
import androidx.core.view.*
import com.linkedin.android.tachyon.DayView
import com.linkedin.android.tachyon.DayViewConfig
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 eu.szkolny.font.SzkolnyFont
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
@ -61,6 +66,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
private val manager
get() = app.timetableManager
private val attendanceManager
get() = app.attendanceManager
// find SwipeRefreshLayout in the hierarchy
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
@ -102,14 +109,17 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val events = withContext(Dispatchers.Default) {
app.db.eventDao().getAllByDateNow(App.profileId, date)
}
processLessonList(lessons, events)
val attendanceList = withContext(Dispatchers.Default) {
app.db.attendanceDao().getAllByDateNow(App.profileId, date)
}
processLessonList(lessons, events, attendanceList)
}
}
return true
}
private fun processLessonList(lessons: List<LessonFull>, events: List<EventFull>) {
private fun processLessonList(lessons: List<LessonFull>, events: List<EventFull>, attendanceList: List<AttendanceFull>) {
// no lessons - timetable not downloaded yet
if (lessons.isEmpty()) {
inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ ->
@ -172,10 +182,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
lessons.forEach { it.showAsUnseen = !it.seen }
buildLessonViews(lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }, events)
buildLessonViews(lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }, events, attendanceList)
}
private fun buildLessonViews(lessons: List<LessonFull>, events: List<EventFull>) {
private fun buildLessonViews(lessons: List<LessonFull>, events: List<EventFull>, attendanceList: List<AttendanceFull>) {
if (!isAdded)
return
@ -192,6 +202,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
for (lesson in lessons) {
val attendance = attendanceList.find { it.startTime == lesson.startTime }
val startTime = lesson.displayStartTime ?: continue
val endTime = lesson.displayEndTime ?: continue
@ -208,11 +219,17 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val lb = TimetableLessonBinding.bind(eventView)
eventViews += eventView
eventView.tag = lesson
eventView.tag = lesson to attendance
eventView.setOnClickListener {
if (isAdded && it.tag is LessonFull)
LessonDetailsDialog(activity, it.tag as LessonFull)
if (isAdded && it.tag is Pair<*, *>) {
val (lessonObj, attendanceObj) = it.tag as Pair<*, *>
LessonDetailsDialog(
activity = activity,
lesson = lessonObj as LessonFull,
attendance = attendanceObj as AttendanceFull?
)
}
}
val eventList = events.filter { it.time != null && it.time == lesson.displayStartTime }.take(3)
@ -276,6 +293,18 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
lb.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet)
lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet)
lb.attendanceIcon.isVisible = attendance?.let {
val icon = attendanceManager.getAttendanceIcon(it) ?: return@let false
val color = attendanceManager.getAttendanceColor(it)
lb.attendanceIcon.setImageDrawable(
IconicsDrawable(activity, icon).apply {
colorInt = color
sizeDp = 24
}
)
true
} ?: false
lb.unread = lesson.type != Lesson.TYPE_NORMAL && lesson.showAsUnseen
if (!lesson.seen) {
manager.markAsSeen(lesson)
@ -283,6 +312,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
//lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
lb.annotationVisible = manager.getAnnotation(activity, lesson, lb.annotation)
val lessonNumberMargin =
if (lb.annotationVisible) (-8).dp
else 0
lb.lessonNumberText.updateLayoutParams<LinearLayout.LayoutParams> {
updateMargins(top = lessonNumberMargin, bottom = lessonNumberMargin)
}
// The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here

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

View File

@ -4,6 +4,9 @@
package pl.szczodrzynski.edziennik.utils.managers
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import eu.szkolny.font.SzkolnyFont
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -63,6 +66,17 @@ class AttendanceManager(val app: App) : CoroutineScope {
else getAttendanceColor(attendance.baseType)
}
fun getAttendanceIcon(attendance: Attendance): IIcon? = when (attendance.baseType) {
Attendance.TYPE_PRESENT, Attendance.TYPE_PRESENT_CUSTOM -> CommunityMaterial.Icon.cmd_check
Attendance.TYPE_ABSENT -> CommunityMaterial.Icon.cmd_close
Attendance.TYPE_ABSENT_EXCUSED -> CommunityMaterial.Icon3.cmd_progress_close
Attendance.TYPE_RELEASED -> CommunityMaterial.Icon.cmd_account_arrow_right_outline
Attendance.TYPE_BELATED -> CommunityMaterial.Icon.cmd_clock_alert_outline
Attendance.TYPE_BELATED_EXCUSED -> CommunityMaterial.Icon.cmd_clock_check_outline
Attendance.TYPE_DAY_FREE -> SzkolnyFont.Icon.szf_umbrella_beach_outline
else -> null
}
/* _ _ _____ _____ _ __ _
| | | |_ _| / ____| (_)/ _(_)
| | | | | | | (___ _ __ ___ ___ _| |_ _ ___

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-9-18.
*/
package pl.szczodrzynski.edziennik.utils.managers
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.toApiError
class AvailabilityManager(val app: App) {
companion object {
private const val TAG = "AvailabilityManager"
}
private val api = SzkolnyApi(app)
data class Error(
val type: Type,
val status: RegisterAvailabilityStatus?,
val apiError: ApiError?
) {
companion object {
fun notAvailable(status: RegisterAvailabilityStatus) =
Error(Type.NOT_AVAILABLE, status, null)
fun apiError(apiError: ApiError) =
Error(Type.API_ERROR, null, apiError)
fun noApiAccess() =
Error(Type.NO_API_ACCESS, null, null)
}
enum class Type {
NOT_AVAILABLE,
API_ERROR,
NO_API_ACCESS,
}
}
fun check(profile: Profile, cacheOnly: Boolean = false): Error? {
return check(profile.registerName, cacheOnly)
}
fun check(loginType: Int, cacheOnly: Boolean = false): Error? {
val registerName = when (loginType) {
LOGIN_TYPE_LIBRUS -> "librus"
LOGIN_TYPE_VULCAN -> "vulcan"
LOGIN_TYPE_IDZIENNIK -> "idziennik"
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> "unknown"
}
return check(registerName, cacheOnly)
}
fun check(registerName: String, cacheOnly: Boolean = false): Error? {
if (!app.config.apiAvailabilityCheck)
return null
val status = app.config.sync.registerAvailability[registerName]
if (status != null && status.nextCheckAt > currentTimeUnix()) {
return reportStatus(status)
}
if (cacheOnly) {
return reportStatus(status)
}
return try {
val availability = api.getRegisterAvailability()
app.config.sync.registerAvailability = availability
reportStatus(availability[registerName])
} catch (e: Throwable) {
reportApiError(e)
}
}
private fun reportStatus(status: RegisterAvailabilityStatus?): Error? {
if (status == null)
return null
if (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)
return Error.notAvailable(status)
return null
}
private fun reportApiError(throwable: Throwable): Error {
val apiError = throwable.toApiError(TAG)
if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) {
app.config.sync.registerAvailability = mapOf()
return Error.noApiAccess()
}
return Error.apiError(apiError)
}
}

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

@ -134,7 +134,8 @@
android:layout_marginTop="8dp"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/shiftedText"
@ -290,6 +291,60 @@
</LinearLayout>
<View
android:id="@+id/attendanceDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="@drawable/divider"/>
<LinearLayout
android:id="@+id/attendanceLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceView
android:id="@+id/attendanceView"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
tools:background="@drawable/bg_rounded_8dp"
tools:backgroundTint="#f44336"
tools:gravity="center"
tools:text="nb"
tools:textSize="22sp" />
<TextView
android:id="@+id/attendanceType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:maxLines="2"
android:ellipsize="end"
android:textSize="16sp"
tools:text="nieobecność usprawiedliweniowsza1234324" />
<ImageView
android:id="@+id/attendanceIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginHorizontal="8dp"
tools:srcCompat="@sample/check" />
<Button
android:id="@+id/attendanceDetails"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_lesson_attendance_details" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"

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

@ -1,14 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) Kuba Szczodrzyński 2020-4-3.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
<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"
tools:ignore="HardcodedText">
<data>
<variable name="app" type="pl.szczodrzynski.edziennik.App"/>
<variable
name="app"
type="pl.szczodrzynski.edziennik.App" />
</data>
<ScrollView
@ -38,6 +41,12 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />-->
<com.google.android.material.checkbox.MaterialCheckBox
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"
@ -80,7 +89,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
tools:text="Cookies:\n\nsynergia.librus.pl\n DZIENNIKSID=L01~1234567890abcdef"/>
tools:text="Cookies:\n\nsynergia.librus.pl\n DZIENNIKSID=L01~1234567890abcdef" />
<Button
android:id="@+id/unarchive"
@ -92,15 +101,46 @@
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/profile"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
android:layout_height="wrap_content" />
<com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={app.config.archiverEnabled}"
android:text="Archiver enabled" />
<com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="@={app.config.apiAvailabilityCheck}"
android:text="Availability check enabled" />
<Button
android:id="@+id/resetCert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Reset API signature"
android:textAllCaps="false" />
<Button
android:id="@+id/disableDebug"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Disable Dev Mode"
android:textAllCaps="false"
app:backgroundTint="@color/windowBackgroundRed" />
<Button
android:id="@+id/rebuildConfig"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Rebuild App.config"
android:textAllCaps="false" />
</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

@ -48,16 +48,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingHorizontal="8dp"
android:baselineAligned="false"
android:orientation="horizontal"
android:baselineAligned="false">
android:paddingHorizontal="8dp"
android:paddingVertical="4dp">
<!--tools:background="@drawable/timetable_subject_color_rounded"-->
<TextView
android:id="@+id/subjectName"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginVertical="4dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
@ -75,8 +75,8 @@
android:layout_height="12dp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="8dp"
android:visibility="@{unread ? View.VISIBLE : View.GONE}"
android:background="@drawable/unread_red_circle" />
android:background="@drawable/unread_red_circle"
android:visibility="@{unread ? View.VISIBLE : View.GONE}" />
<ImageView
android:id="@+id/attendanceIcon"
@ -87,29 +87,18 @@
tools:srcCompat="@sample/check"
tools:visibility="visible" />
<ImageView
android:id="@+id/imageView4"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_weight="0"
app:srcCompat="@drawable/bg_circle"
android:visibility="gone" />
<TextView
android:id="@+id/textView6"
android:id="@+id/lessonNumberText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:fontFamily="sans-serif-condensed-light"
android:includeFontPadding="false"
android:layout_marginBottom="-4dp"
android:paddingHorizontal="4dp"
android:text="@{Integer.toString(lessonNumber)}"
android:textSize="28sp"
android:visibility="@{lessonNumber != null ? View.VISIBLE : View.GONE}"
tools:text="3"/>
<!--android:layout_marginTop="@{annotationVisible ? `-4dp` : `4dp`}"
android:layout_marginBottom="@{annotationVisible ? `-4dp` : `0dp`}"-->
tools:text="3" />
</LinearLayout>
@ -149,7 +138,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="end|bottom"
android:orientation="horizontal">
android:orientation="horizontal"
android:paddingBottom="2dp">
<View
android:id="@+id/event3"

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 - Juni 2021</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - 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 - June 2021</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - 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>

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