Compare commits

...

34 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
5a217aca01 [4.8.2] Update build.gradle, signing and changelog. 2021-06-15 18:13:18 +02:00
4bed62aa6f [UI] Fix timetable crash when syncing (#54)
* Fix removeView

* Use removeView() instead of removeAllViews()

* Remove dayView from layout file
2021-06-11 22:06:07 +02:00
a4d604e146 [API/Librus] Update JST Client ID (#53)
@6Arin9
2021-06-11 00:05:54 +02:00
106 changed files with 1525 additions and 366 deletions

View File

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

View File

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

View File

@ -43,10 +43,12 @@ jobs:
androidHome: ${{ env.ANDROID_HOME }} androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps: steps:
- name: Setup JDK 1.8 - name: Setup JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v2
with: with:
java-version: 1.8 distribution: 'zulu'
java-version: '11'
cache: 'gradle'
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v2 uses: android-actions/setup-android@v2
- name: Clean build artifacts - 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 # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,gradle,java,kotlin
signatures/ 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: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.crashlytics'
@ -35,6 +36,9 @@ android {
buildTypes { buildTypes {
debug { debug {
minifyEnabled = false minifyEnabled = false
manifestPlaceholders = [
buildTimestamp: 0
]
} }
release { release {
minifyEnabled = true minifyEnabled = true
@ -120,25 +124,25 @@ dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
// Android Jetpack // 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.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.4" implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.room:room-runtime:2.2.6" implementation "androidx.room:room-runtime:2.3.0"
implementation "androidx.work:work-runtime-ktx:2.5.0" implementation "androidx.work:work-runtime-ktx:2.6.0"
kapt "androidx.room:room-compiler:2.2.6" kapt "androidx.room:room-compiler:2.3.0"
// Google design libs // 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" implementation "com.google.android:flexbox:2.0.1"
// Play Services/Firebase // Play Services/Firebase
implementation "com.google.android.gms:play-services-wearable:17.0.0" implementation "com.google.android.gms:play-services-wearable:17.1.0"
implementation "com.google.firebase:firebase-core:18.0.2" implementation "com.google.firebase:firebase-core:19.0.1"
implementation "com.google.firebase:firebase-crashlytics:17.4.0" implementation "com.google.firebase:firebase-crashlytics:18.2.1"
implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } } implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } }
// OkHttp, Retrofit, Gson, Jsoup // OkHttp, Retrofit, Gson, Jsoup
@ -153,7 +157,7 @@ dependencies {
// Szkolny.eu libraries/forks // Szkolny.eu libraries/forks
implementation "eu.szkolny:android-snowfall:1ca9ea2da3" implementation "eu.szkolny:android-snowfall:1ca9ea2da3"
implementation "eu.szkolny:agendacalendarview:5431f03098" implementation "eu.szkolny:agendacalendarview:ac0f3dcf42"
implementation "eu.szkolny:cafebar:5bf0c618de" implementation "eu.szkolny:cafebar:5bf0c618de"
implementation "eu.szkolny.fslogin:lib:2.0.0" implementation "eu.szkolny.fslogin:lib:2.0.0"
implementation "eu.szkolny:material-about-library:1d5ebaf47c" implementation "eu.szkolny:material-about-library:1d5ebaf47c"
@ -168,10 +172,10 @@ dependencies {
kapt "eu.szkolny.selective-dao:codegen:27f8f3f194" kapt "eu.szkolny.selective-dao:codegen:27f8f3f194"
// Iconics & related // Iconics & related
implementation "com.mikepenz:iconics-core:5.3.0-b01" implementation "com.mikepenz:iconics-core:5.3.1"
implementation "com.mikepenz:iconics-views:5.3.0-b01" implementation "com.mikepenz:iconics-views:5.3.1"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar" 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 // Other dependencies
implementation "cat.ereza:customactivityoncrash:2.3.0" implementation "cat.ereza:customactivityoncrash:2.3.0"

View File

@ -146,6 +146,7 @@
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
android:theme="@style/Base.Theme.AppCompat" /> android:theme="@style/Base.Theme.AppCompat" />
<activity android:name=".ui.modules.base.BuildInvalidActivity" /> <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.1, 2021-06-06</h3> <h3>Wersja 4.10, 2021-09-22</h3>
<ul> <ul>
<li>Poprawiono funkcje logowania. @BxOxSxS</li> <li>Dodano wyświetlanie informacji o frekwencji w planie lekcji. @Antoni-Czaplicki</li>
<li>MobiDziennik: naprawiono wysyłanie wiadomości (błąd "nie znaleziono wiadomości").</li>
</ul> </ul>
<br> <br>
<br> <br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0x90, 0x44, 0x08, 0xb8, 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); 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 val profileId
get() = profile.id get() = profile.id
var enableChucker = false
var debugMode = false var debugMode = false
var devMode = false var devMode = false
} }
@ -70,6 +71,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val permissionManager by lazy { PermissionManager(this) } val permissionManager by lazy { PermissionManager(this) }
val attendanceManager by lazy { AttendanceManager(this) } val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) } val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) }
val db val db
get() = App.db get() = App.db
@ -115,10 +117,12 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
HyperLog.initialize(this) HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE) HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this)) HyperLog.setLogFormat(DebugLogFormat(this))
if (enableChucker) {
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
builder.addInterceptor(chuckerInterceptor) builder.addInterceptor(chuckerInterceptor)
} }
}
http = builder.build() http = builder.build()
@ -171,7 +175,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
App.config = Config(App.db) App.config = Config(App.db)
App.profile = Profile(0, 0, 0, "") App.profile = Profile(0, 0, 0, "")
debugMode = BuildConfig.DEBUG debugMode = BuildConfig.DEBUG
devMode = config.debugMode || debugMode devMode = config.devMode ?: debugMode
enableChucker = config.enableChucker ?: devMode
if (!profileLoadById(config.lastProfileId)) { if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) } db.profileDao().firstId?.let { profileLoadById(it) }

View File

@ -14,6 +14,7 @@ import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.text.* import android.text.*
import android.text.style.CharacterStyle import android.text.style.CharacterStyle
import android.text.style.ForegroundColorSpan 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 Short -> putShort(property.first, property.second as Short)
is Double -> putDouble(property.first, property.second as Double) is Double -> putDouble(property.first, property.second as Double)
is Boolean -> putBoolean(property.first, property.second as Boolean) 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.*
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx 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.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget import pl.szczodrzynski.edziennik.utils.models.NavTarget
import pl.szczodrzynski.navlib.* import pl.szczodrzynski.navlib.*
@ -634,45 +636,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
return return
} }
app.profile.registerName?.let { registerName -> val error = withContext(Dispatchers.IO) {
var status = app.config.sync.registerAvailability[registerName] app.availabilityManager.check(app.profile)
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 (error?.type) {
}) Type.NOT_AVAILABLE -> {
}
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) {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
loadTarget(DRAWER_ITEM_HOME) loadTarget(DRAWER_ITEM_HOME)
if (status != null) RegisterUnavailableDialog(this, error.status!!)
RegisterUnavailableDialog(this, status)
return 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 swipeRefreshLayout.isRefreshing = true
@ -699,10 +679,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event) EventBus.getDefault().removeStickyEvent(event)
app.profile.registerName?.let { registerName -> val error = app.availabilityManager.check(app.profile, cacheOnly = true)
event.data[registerName]?.let { if (error != null) {
RegisterUnavailableDialog(this, it) RegisterUnavailableDialog(this, error.status!!)
}
} }
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)

View File

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

View File

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

View File

@ -43,7 +43,7 @@ const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token"
const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST" const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST"
const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE=" const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE="
const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398" const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398"
const val LIBRUS_API_CLIENT_ID_JST = "49" const val LIBRUS_API_CLIENT_ID_JST = "59"
//const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42" //const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42"
const val LIBRUS_JST_DEMO_CODE = "68656A21" const val LIBRUS_JST_DEMO_CODE = "68656A21"

View File

@ -195,6 +195,7 @@ const val ERROR_VULCAN_HEBE_FIREBASE_ERROR = 362
const val ERROR_VULCAN_HEBE_CERTIFICATE_GONE = 363 const val ERROR_VULCAN_HEBE_CERTIFICATE_GONE = 363
const val ERROR_VULCAN_HEBE_SERVER_ERROR = 364 const val ERROR_VULCAN_HEBE_SERVER_ERROR = 364
const val ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND = 365 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_VULCAN_API_DEPRECATED = 390
const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501 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 { val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
"""<input type="hidden".+?name="([A-z0-9_]+)?".+?value="([A-z0-9_+-/=]+)?".+?>""".toRegex(DOT_MATCHES_ALL) """<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.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError 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.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile 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.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d 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) { open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object { companion object {
@ -90,35 +90,21 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
return return
} }
profile.registerName?.also { registerName -> val error = app.availabilityManager.check(profile)
var status = app.config.sync.registerAvailability[registerName] when (error?.type) {
if (status == null || status.nextCheckAt < currentTimeUnix()) { Type.NOT_AVAILABLE -> {
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) {
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) {
EventBus.getDefault().postSticky( EventBus.getDefault().postSticky(RegisterAvailabilityEvent())
RegisterAvailabilityEvent(app.config.sync.registerAvailability)
)
} }
cancel() cancel()
taskCallback.onCompleted() taskCallback.onCompleted()
return 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 val courseStudentEndpoint: String
get() = "Course/$studentId/" 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 { fun getEventType(longId: String, name: String): EventType {
val id = longId.crc16().toLong() val id = longId.crc16().toLong()
return eventTypes.singleOrNull { it.id == id } ?: run { 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) val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEach ?: return@forEach
val subjectName = subjectElement.text().trim() 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() val dateString = examElement.child(2).text().trim()
if (dateString.isBlank()) return@forEach if (dateString.isBlank()) return@forEach

View File

@ -53,7 +53,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
val subjectId = subjectElement.id().trim() val subjectId = subjectElement.id().trim()
val subjectName = subjectElement.child(0).text().trim() val subjectName = subjectElement.child(0).text().trim()
val subject = data.getSubject(subjectId, subjectName) val subject = data.getSubject(subjectId.crc32(), subjectName)
val gradeType = when { val gradeType = when {
subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM 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) val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEach ?: return@forEach
val subjectName = subjectElement.text() 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 lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date)
val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime 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 { EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach {
val id = it[1].trim() val id = it[1].trim()
val name = it[2].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 package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web
import org.jsoup.Jsoup 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_SUBJECT_ID
import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik 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) val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1)
?: return@forEachIndexed ?: return@forEachIndexed
val subjectName = subjectElement.text().trim() val subjectName = subjectElement.text().trim()
val subject = data.getSubject(subjectId, subjectName) val subject = data.getSubject(subjectId.crc32(), subjectName)
/* Getting teacher */ /* 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_MANUALS = 2100
const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200 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_HOMEWORK = 2300 // not used as an endpoint
const val ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE = 2400
const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000 const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000
val MobidziennikFeatures = listOf( 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. * 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) data.startProgress(R.string.edziennik_progress_endpoint_lucky_number)
MobidziennikWebManuals(data, lastSync, onSuccess) MobidziennikWebManuals(data, lastSync, onSuccess)
}*/ }*/
ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE-> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
MobidziennikWebTimetable(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId) else -> onSuccess(endpointId)
} }
} }

View File

@ -48,7 +48,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik,
//syncWeeks.clear() //syncWeeks.clear()
//syncWeeks += Date.fromY_m_d("2019-12-19") //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)) 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? val loginShort: String?
get() = studentLogin?.split('@')?.get(0) 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 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 val addedDate = if (profile.empty) profile.getSemesterStart(semester).inMillis
else System.currentTimeMillis() 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 teacher = data.getTeacher(teacherFirstName, teacherLastName)
val subjectName = grade.getString("SchoolSubject") ?: return@forEach 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 } val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis }
?: System.currentTimeMillis() ?: System.currentTimeMillis()

View File

@ -22,7 +22,13 @@ class PodlasieApiMain(override val data: DataPodlasie,
init { init {
apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json -> 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.getInt("LuckyNumber")?.let { PodlasieApiLuckyNumber(data, it) }
json.getJsonArray("Teacher")?.asJsonObjectList()?.let { PodlasieApiTeachers(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) } val startTime = lesson.getString("TimeFrom")?.let { Time.fromH_m_s(it) }
?: return@forEach ?: return@forEach
val endTime = lesson.getString("TimeTo")?.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 ?: return@forEach
val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach
val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach
val teacher = data.getTeacher(teacherFirstName, teacherLastName) 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") val classroom = lesson.getString("Room")
Lesson(data.profileId, -1).also { 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 } get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value } 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? val apiUrl: String?
get() { get() {
val url = when (apiToken[symbol]?.substring(0, 3)) { val url = when (apiToken[symbol]?.substring(0, 3)) {

View File

@ -38,7 +38,7 @@ class VulcanHebeAttendance(
lastSync = lastSync lastSync = lastSync
) { list, _ -> ) { list, _ ->
list.forEach { attendance -> 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 type = attendance.getJsonObject("PresenceType") ?: return@forEach
val baseType = getBaseType(type) val baseType = getBaseType(type)
val typeName = type.getString("Name") ?: return@forEach val typeName = type.getString("Name") ?: return@forEach

View File

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

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import com.google.gson.JsonObject import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* 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.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe
@ -27,6 +28,22 @@ class VulcanHebeSendMessage(
} }
init { 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() val recipientsArray = JsonArray()
recipients.forEach { teacher -> recipients.forEach { teacher ->
recipientsArray += JsonObject( recipientsArray += JsonObject(
@ -40,10 +57,10 @@ class VulcanHebeSendMessage(
val senderName = (profile?.accountName ?: profile?.studentNameLong) val senderName = (profile?.accountName ?: profile?.studentNameLong)
?.swapFirstLastName() ?: "" ?.swapFirstLastName() ?: ""
val sender = JsonObject( val sender = JsonObject(
"Address" to senderName, "Address" to data.senderAddressName,
"LoginId" to data.studentLoginId.toString(), "LoginId" to data.studentLoginId.toString(),
"Initials" to senderName.getNameInitials(), "Initials" to senderName.getNameInitials(),
"AddressHash" to senderName.sha1Hex() "AddressHash" to data.senderAddressHash
) )
apiPost( 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.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE 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.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
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE
@ -47,7 +48,7 @@ class VulcanHebeTimetable(
?: previousWeekStart ?: previousWeekStart
val dateTo = dateFrom.clone().stepForward(0, 0, 13) val dateTo = dateFrom.clone().stepForward(0, 0, 13)
val lastSync = null val lastSync = 0L
apiGetList( apiGetList(
TAG, TAG,
@ -106,6 +107,8 @@ class VulcanHebeTimetable(
"Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}" "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.lessonList.addAll(lessonList)
data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS) data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS)

View File

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

View File

@ -2,6 +2,7 @@ package pl.szczodrzynski.edziennik.data.api.models
import android.util.LongSparseArray import android.util.LongSparseArray
import android.util.SparseArray import android.util.SparseArray
import androidx.core.util.set
import androidx.core.util.size import androidx.core.util.size
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import com.google.gson.JsonObject 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) { fun startProgress(stringRes: Int) {
callback.onStartProgress(stringRes) 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.SignatureInterceptor
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.data.api.szkolny.request.* 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.*
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.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Notification
@ -373,6 +370,15 @@ class SzkolnyApi(val app: App) : CoroutineScope {
throw SzkolnyApiException(null) 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) @Throws(Exception::class)
fun getFirebaseToken(registerName: String): String { fun getFirebaseToken(registerName: String): String {
val response = api.firebaseToken(registerName).execute() val response = api.firebaseToken(registerName).execute()

View File

@ -27,6 +27,9 @@ interface SzkolnyService {
@POST("appUser") @POST("appUser")
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Unit>> fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Unit>>
@GET("contributors/android")
fun contributors(): Call<ApiResponse<ContributorsResponse>>
@GET("updates/app") @GET("updates/app")
fun updates(@Query("channel") channel: String = "release"): Call<ApiResponse<List<Update>>> 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 provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDOlSskDmW===.$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") getRawNow("$QUERY WHERE notified = 0 $ORDER_BY")
fun getNotNotifiedNow(profileId: Int) = fun getNotNotifiedNow(profileId: Int) =
getRawNow("$QUERY WHERE attendances.profileId = $profileId AND notified = 0 $ORDER_BY") 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 // GET ONE - NOW
fun getByIdNow(profileId: Int, id: Long) = fun getByIdNow(profileId: Int, id: Long) =

View File

@ -140,7 +140,7 @@ open class Profile(
LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik" LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik"
LOGIN_TYPE_PODLASIE -> "podlasie" LOGIN_TYPE_PODLASIE -> "podlasie"
LOGIN_TYPE_EDUDZIENNIK -> "edudziennik" LOGIN_TYPE_EDUDZIENNIK -> "edudziennik"
else -> null else -> "unknown"
} }
override fun getImageDrawable(context: Context): Drawable { 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 ) ?: return@launch
app.config.sync.registerAvailability = data app.config.sync.registerAvailability = data
if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { 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 { init { run {
if (activity.isFinishing) if (activity.isFinishing)
return@run return@run
if (status.available && status.minVersionCode <= BuildConfig.VERSION_CODE)
return@run
onShowListener?.invoke(TAG) onShowListener?.invoke(TAG)
app = activity.applicationContext as App app = activity.applicationContext as App

View File

@ -4,11 +4,14 @@
package pl.szczodrzynski.edziennik.ui.dialogs.timetable package pl.szczodrzynski.edziennik.ui.dialogs.timetable
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.graphics.* import android.graphics.*
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.View.MeasureSpec import android.view.View.MeasureSpec
@ -373,25 +376,31 @@ class GenerateBlockTimetableDialog(
val today = Date.getToday().stringY_m_d val today = Date.getToday().stringY_m_d
val now = Time.getNow().stringH_M_S 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() } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png") 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 { try {
val fos = FileOutputStream(outputFile) val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) resolver.openOutputStream(uri).use {
fos.close() bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
uri
} catch (e: Exception) { } catch (e: Exception) {
Log.e("SAVE_IMAGE", e.message, e) Log.e("SAVE_IMAGE", e.message, e)
return@withContext null 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() progressDialog.dismiss()

View File

@ -8,15 +8,20 @@ import android.content.Intent
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Lesson 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.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
import pl.szczodrzynski.edziennik.onClick 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.EventDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog 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.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
@ -34,6 +40,7 @@ import kotlin.coroutines.CoroutineContext
class LessonDetailsDialog( class LessonDetailsDialog(
val activity: AppCompatActivity, val activity: AppCompatActivity,
val lesson: LessonFull, val lesson: LessonFull,
val attendance: AttendanceFull? = null,
val onShowListener: ((tag: String) -> Unit)? = null, val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope { ) : CoroutineScope {
@ -52,6 +59,8 @@ class LessonDetailsDialog(
private lateinit var adapter: EventListAdapter private lateinit var adapter: EventListAdapter
private val manager private val manager
get() = app.timetableManager get() = app.timetableManager
private val attendanceManager
get() = app.attendanceManager
init { run { init { run {
if (activity.isFinishing) if (activity.isFinishing)
@ -170,6 +179,27 @@ class LessonDetailsDialog(
b.teamName = lesson.teamName 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( adapter = EventListAdapter(
activity, activity,
showWeekDay = false, showWeekDay = false,

View File

@ -21,6 +21,7 @@ import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding 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 { private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch {
if (!isAdded) if (!isAdded)
return@launch return@launch
checkEventTypes()
delay(500) delay(500)
agendaDefault = AgendaFragmentDefault(activity, app, b) agendaDefault = AgendaFragmentDefault(activity, app, b)
@ -146,6 +160,7 @@ class AgendaFragment : Fragment(), CoroutineScope {
}}} }}}
private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch { private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch {
checkEventTypes()
delay(300) delay(300)
val dayList = mutableListOf<EventDay>() val dayList = mutableListOf<EventDay>()

View File

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

View File

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

View File

@ -5,15 +5,18 @@
package pl.szczodrzynski.edziennik.ui.modules.debug package pl.szczodrzynski.edziennik.ui.modules.debug
import android.os.Bundle import android.os.Bundle
import android.os.Process
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SimpleSQLiteQuery
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileRemoveDialog 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.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.fslogin.decode import pl.szczodrzynski.fslogin.decode
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
class LabPageFragment : LazyFragment(), CoroutineScope { class LabPageFragment : LazyFragment(), CoroutineScope {
companion object { companion object {
@ -75,12 +79,51 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}") app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
} }
b.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 { b.unarchive.onClick {
app.profile.archived = false app.profile.archived = false
app.profile.archiveId = null app.profile.archiveId = null
app.profileSave() app.profileSave()
} }
b.resetCert.onClick {
app.config.apiInvalidCert = null
}
b.rebuildConfig.onClick {
App.config = Config(App.db)
}
val profiles = app.db.profileDao().allNow val profiles = app.db.profileDao().allNow
b.profile.clear() b.profile.clear()
b.profile += profiles.map { TextInputDropDown.Item(it.id.toLong(), "${it.id} ${it.name} archived ${it.archived}", tag = it) } 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", app.gson.toJsonTree(app.profile))
json.add("App.profile.studentData", app.profile.studentData) json.add("App.profile.studentData", app.profile.studentData)
json.add("App.profile.loginStore", loginStore?.data ?: JsonObject()) 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.items = LabJsonAdapter.expand(json, 0)
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()

View File

@ -163,10 +163,9 @@ class HomeFragment : Fragment(), CoroutineScope {
if (app.profile.archived) if (app.profile.archived)
items.add(0, HomeArchiveCard(101, app, activity, this, app.profile)) 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 val update = app.config.update
if (update != null && update.versionCode > BuildConfig.VERSION_CODE if (update != null && update.versionCode > BuildConfig.VERSION_CODE || status?.userMessage != null) {
|| status != null && (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)) {
items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile)) items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile))
} }

View File

@ -50,7 +50,8 @@ class HomeAvailabilityCard(
} }
holder.root += b.root 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 val update = app.config.update
if (update == null && status == null) if (update == null && status == null)
@ -58,7 +59,8 @@ class HomeAvailabilityCard(
var onInfoClick = { _: View -> } 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.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.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY)
b.homeAvailabilityUpdate.isVisible = false b.homeAvailabilityUpdate.isVisible = false
@ -69,6 +71,7 @@ class HomeAvailabilityCard(
RegisterUnavailableDialog(activity, status) RegisterUnavailableDialog(activity, status)
} }
} }
// show "update available" when available OR version too old for the register
else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) { else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) {
b.homeAvailabilityTitle.setText(R.string.home_availability_title) b.homeAvailabilityTitle.setText(R.string.home_availability_title)
b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName) b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName)
@ -78,6 +81,9 @@ class HomeAvailabilityCard(
UpdateAvailableDialog(activity, update) UpdateAvailableDialog(activity, update)
} }
} }
else {
b.root.isVisible = false
}
b.homeAvailabilityUpdate.onClick { b.homeAvailabilityUpdate.onClick {
if (update == null) if (update == null)

View File

@ -26,13 +26,12 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.* 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.databinding.LoginChooserFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity
import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -269,52 +268,23 @@ class LoginChooserFragment : Fragment(), CoroutineScope {
} }
private suspend fun checkAvailability(loginType: Int): Boolean { private suspend fun checkAvailability(loginType: Int): Boolean {
when (loginType) { val error = withContext(Dispatchers.IO) {
LOGIN_TYPE_LIBRUS -> "librus" app.availabilityManager.check(loginType)
LOGIN_TYPE_VULCAN -> "vulcan" } ?: return true
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
})
}
when (result) { return when (error.type) {
false -> { Type.NOT_AVAILABLE -> {
RegisterUnavailableDialog(activity, error.status!!)
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() Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show()
return@let true
}
is Throwable -> {
activity.errorSnackbar.addError(result.toApiError(TAG)).show()
return false
}
is RegisterAvailabilityStatus -> {
status = result
} }
} }
} }
if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) {
if (status != null)
RegisterUnavailableDialog(activity, status)
return false
}
}
return true
}
} }

View File

@ -85,6 +85,9 @@ class LoginFormFragment : Fragment(), CoroutineScope {
if (credential is LoginInfo.FormField) { if (credential is LoginInfo.FormField) {
val b = LoginFormFieldItemBinding.inflate(layoutInflater) val b = LoginFormFieldItemBinding.inflate(layoutInflater)
b.textLayout.hint = app.getString(credential.name) b.textLayout.hint = app.getString(credential.name)
if (credential.isNumber) {
b.textEdit.inputType = InputType.TYPE_CLASS_NUMBER
}
if (credential.hideText) { if (credential.hideText) {
b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD
b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE 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 ERROR_LOGIN_VULCAN_INVALID_PIN_2_REMAINING to R.string.error_312_reason
), ),
isRequired = true, isRequired = true,
isNumber = true,
validationRegex = "[0-9]+", validationRegex = "[0-9]+",
caseMode = FormField.CaseMode.LOWER_CASE caseMode = FormField.CaseMode.LOWER_CASE
) )
@ -401,6 +402,7 @@ object LoginInfo {
val validationRegex: String, val validationRegex: String,
val caseMode: CaseMode = CaseMode.UNCHANGED, val caseMode: CaseMode = CaseMode.UNCHANGED,
val hideText: Boolean = false, val hideText: Boolean = false,
val isNumber: Boolean = false,
val stripTextRegex: String? = null val stripTextRegex: String? = null
) : BaseCredential(keyName, name, errorCodes) { ) : BaseCredential(keyName, name, errorCodes) {
enum class CaseMode { UNCHANGED, UPPER_CASE, LOWER_CASE } enum class CaseMode { UNCHANGED, UPPER_CASE, LOWER_CASE }

View File

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

View File

@ -76,7 +76,7 @@ class LoginProgressFragment : Fragment(), CoroutineScope {
val maxProfileId = max( val maxProfileId = max(
app.db.profileDao().lastId ?: 0, 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 loginType = args.getInt("loginType", -1)
val loginMode = args.getInt("loginMode", 0) val loginMode = args.getInt("loginMode", 0)

View File

@ -503,10 +503,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
text.toString() 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) { if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
textHtml = textHtml textHtml = textHtml
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "span") .replace("</p><br>", "</p>")
.replace("</p>", "</span>")
.replace("<b>", "<strong>") .replace("<b>", "<strong>")
.replace("</b>", "</strong>") .replace("</b>", "</strong>")
.replace("<i>", "<em>") .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.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsLicenseActivity import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsLicenseActivity
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
import pl.szczodrzynski.edziennik.ui.modules.settings.contributors.ContributorsActivity
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -90,6 +91,14 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope
it.subText = BuildConfig.VERSION_NAME + ", " + BuildConfig.BUILD_TYPE 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.createMoreItem(card, items = listOf(
util.createActionItem( util.createActionItem(
text = R.string.settings_about_changelog_text, 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, text = R.string.settings_theme_drawer_header_text,
icon = CommunityMaterial.Icon2.cmd_image_outline icon = CommunityMaterial.Icon2.cmd_image_outline
) { ) {
if (app.config.ui.appBackground == null) { if (app.config.ui.headerBackground == null) {
setHeaderBackground() setHeaderBackground()
return@createActionItem 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.asynclayoutinflater.view.AsyncLayoutInflater import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.core.view.isVisible import androidx.core.view.*
import androidx.core.view.marginTop
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import com.linkedin.android.tachyon.DayView import com.linkedin.android.tachyon.DayView
import com.linkedin.android.tachyon.DayViewConfig 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 kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask 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.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.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
@ -61,6 +66,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
private val manager private val manager
get() = app.timetableManager get() = app.timetableManager
private val attendanceManager
get() = app.attendanceManager
// find SwipeRefreshLayout in the hierarchy // find SwipeRefreshLayout in the hierarchy
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
@ -102,14 +109,17 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val events = withContext(Dispatchers.Default) { val events = withContext(Dispatchers.Default) {
app.db.eventDao().getAllByDateNow(App.profileId, date) 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 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 // no lessons - timetable not downloaded yet
if (lessons.isEmpty()) { if (lessons.isEmpty()) {
inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ -> inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ ->
@ -151,7 +161,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
} }
b.scrollView.isVisible = true b.scrollView.isVisible = true
b.dayFrame.removeView(b.dayView) b.dayFrame.removeView(dayView)
b.dayFrame.addView(dayView, 0) b.dayFrame.addView(dayView, 0)
// Inflate a label view for each hour the day view will display // Inflate a label view for each hour the day view will display
@ -172,10 +182,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
lessons.forEach { it.showAsUnseen = !it.seen } 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) if (!isAdded)
return return
@ -192,6 +202,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
for (lesson in lessons) { for (lesson in lessons) {
val attendance = attendanceList.find { it.startTime == lesson.startTime }
val startTime = lesson.displayStartTime ?: continue val startTime = lesson.displayStartTime ?: continue
val endTime = lesson.displayEndTime ?: continue val endTime = lesson.displayEndTime ?: continue
@ -208,11 +219,17 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
val lb = TimetableLessonBinding.bind(eventView) val lb = TimetableLessonBinding.bind(eventView)
eventViews += eventView eventViews += eventView
eventView.tag = lesson eventView.tag = lesson to attendance
eventView.setOnClickListener { eventView.setOnClickListener {
if (isAdded && it.tag is LessonFull) if (isAdded && it.tag is Pair<*, *>) {
LessonDetailsDialog(activity, it.tag as LessonFull) 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) 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.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet)
lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).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 lb.unread = lesson.type != Lesson.TYPE_NORMAL && lesson.showAsUnseen
if (!lesson.seen) { if (!lesson.seen) {
manager.markAsSeen(lesson) manager.markAsSeen(lesson)
@ -283,6 +312,12 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
//lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD) //lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
lb.annotationVisible = manager.getAnnotation(activity, lesson, lb.annotation) 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, // The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here // so calculate those here

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@
package pl.szczodrzynski.edziennik.utils.managers 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -63,6 +66,17 @@ class AttendanceManager(val app: App) : CoroutineScope {
else getAttendanceColor(attendance.baseType) 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:layout_gravity="center"
android:drawablePadding="16dp" android:drawablePadding="16dp"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/attendances_no_data" android:text="@string/attendances_no_data"
android:textSize="24sp" android:textSize="24sp"
android:visibility="gone" android:visibility="gone"

View File

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

View File

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

View File

@ -24,6 +24,7 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_margin="16dp" android:layout_margin="16dp"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/card_grades_no_data" android:text="@string/card_grades_no_data"
android:textSize="16sp" /> 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:layout_marginTop="8dp"
android:baselineAligned="false" android:baselineAligned="false"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:visibility="gone">
<TextView <TextView
android:id="@+id/shiftedText" android:id="@+id/shiftedText"
@ -290,6 +291,60 @@
</LinearLayout> </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 <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"

View File

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

View File

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

View File

@ -24,6 +24,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:drawablePadding="16dp" android:drawablePadding="16dp"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:gravity="center"
android:text="@string/homework_no_data" android:text="@string/homework_no_data"
android:textSize="24sp" android:textSize="24sp"
android:visibility="gone" 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. ~ Copyright (c) Kuba Szczodrzyński 2020-4-3.
--> -->
<layout xmlns:tools="http://schemas.android.com/tools" <layout xmlns:android="http://schemas.android.com/apk/res/android"
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"> tools:ignore="HardcodedText">
<data> <data>
<variable name="app" type="pl.szczodrzynski.edziennik.App"/>
<variable
name="app"
type="pl.szczodrzynski.edziennik.App" />
</data> </data>
<ScrollView <ScrollView
@ -38,6 +41,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />--> 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 <Button
android:id="@+id/last10unseen" android:id="@+id/last10unseen"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -92,15 +101,46 @@
<pl.szczodrzynski.edziennik.utils.TextInputDropDown <pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/profile" android:id="@+id/profile"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" />
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="@={app.config.archiverEnabled}" android:checked="@={app.config.archiverEnabled}"
android:text="Archiver enabled" /> 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> </LinearLayout>
</ScrollView> </ScrollView>
</layout> </layout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -856,7 +856,7 @@
<string name="settings_about_licenses_text">Open-Source-Lizenzen</string> <string name="settings_about_licenses_text">Open-Source-Lizenzen</string>
<string name="settings_about_privacy_policy_text">Datenschutzrichtlinie</string> <string name="settings_about_privacy_policy_text">Datenschutzrichtlinie</string>
<string name="settings_card_register_title">E-Klassenbuch</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_subtext">Klicken Sie hier, um nach Aktualisierungen zu suchen</string>
<string name="settings_about_update_text">Aktualisierung</string> <string name="settings_about_update_text">Aktualisierung</string>
<string name="settings_about_version_text">Version</string> <string name="settings_about_version_text">Version</string>
@ -1234,4 +1234,6 @@
<string name="you_are_offline_title">Netzwerkverbindung</string> <string name="you_are_offline_title">Netzwerkverbindung</string>
<string name="login_summary_account_child">(Kind)</string> <string name="login_summary_account_child">(Kind)</string>
<string name="login_summary_account_parent">(Elternteil)</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> </resources>

View File

@ -55,4 +55,13 @@
<item quantity="one">%1$s - %2$d unread</item> <item quantity="one">%1$s - %2$d unread</item>
<item quantity="other">%1$s - %2$d unread</item> <item quantity="other">%1$s - %2$d unread</item>
</plurals> </plurals>
<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> </resources>

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