Compare commits

...

81 Commits

Author SHA1 Message Date
30b6ac2a06 [4.0-rc.2] Update build.gradle, singing and changelog. 2020-03-26 20:46:03 +01:00
a7fa7cb5e4 [API/Librus] Fix a typo. 2020-03-26 20:45:46 +01:00
f3e87f9016 [API/Librus] Fix missing login data error. 2020-03-26 20:42:58 +01:00
a983af6c28 [4.0-rc.1] Update build.gradle, singing and changelog. 2020-03-26 20:40:00 +01:00
114c841f0c [API/Liburs] Fix Librus API push config endpoint. 2020-03-26 20:34:59 +01:00
e271048577 [UI] Clarify some errors. Fix deselecting the mini drawer. 2020-03-26 18:48:54 +01:00
b8c5925e82 [API/Librus] Fix attendance NPE. 2020-03-26 17:55:39 +01:00
9bda6c8869 [API/Librus] Disable push config with no premium. Disable API homework. Enable Synergia homework. 2020-03-26 16:31:11 +01:00
d1608d308c [API/Librus] Disable login with credentials in Messages. 2020-03-26 15:57:13 +01:00
b8e1e1d33a [Event/Manual] Fix dropdowns not showing any data. 2020-03-25 17:36:22 +01:00
8099a037e7 [Proguard] Update proguard rules to fix BetterLink and MiniDrawer. 2020-03-24 21:03:19 +01:00
af23c932a6 [API] Change the Cookie jar to fix most cookie problems. 2020-03-24 20:43:41 +01:00
4edabbb186 [Messages/Compose] Enable HTML messages for Idziennik. 2020-03-24 16:31:46 +01:00
37a5bea79b [Libraries] Update gradle, NavLib and Firebase. 2020-03-24 16:29:45 +01:00
40cdc7d713 [4.0-beta.14] Update build.gradle, signing and changelog. 2020-03-24 12:54:42 +01:00
49825aca48 [API/Librus] Fix message attachment downloading. 2020-03-24 12:51:49 +01:00
1d57c4e705 [Settings] Replace hardcoded Discord invite link with a redirect. 2020-03-21 16:57:01 +01:00
87ae5787ee [UX] Fix app quiting in home when back button opens drawer function active. 2020-03-20 21:39:38 +01:00
20f16c25a3 [API/Liburs] Fix getting wrong homework description in Synergia. 2020-03-20 15:47:12 +01:00
6f1ec79d9b [UX] Fix back button opens drawer function always opening the drawer. 2020-03-20 14:21:13 +01:00
18c7eea89c [API/Mobidziennik] Fix messages exception when no table found. Handle server problem maintenance error. 2020-03-19 23:25:01 +01:00
f73060aeb6 [API/Idziennik] Fix announcements error. 2020-03-19 23:04:23 +01:00
2f653b83b6 [Errors] Clarify some HTTP error explanations. 2020-03-19 23:00:39 +01:00
445dec907d [Home] Change card swipe direction to left. Add config dialog to bottom sheet. 2020-03-19 22:29:42 +01:00
927316d24b [Firebase] Disable sync by firebase when profile is excluded from auto sync. 2020-03-19 20:18:52 +01:00
3957453ed6 [UI] Fix displaying lists for correct profile in event manual dialog. 2020-03-19 20:09:35 +01:00
0296c704cb [UI] Update dialog NoDisplay theme. 2020-03-19 20:02:50 +01:00
1e7fe972de [API/Librus] Fix setting messages as read. 2020-03-19 19:49:55 +01:00
c95bc656ea [UI] Add context menus in messages and events to quickly run an action. 2020-03-19 17:55:12 +01:00
a082d95b04 [Events/Manual] Remove saving progress toasts. 2020-03-18 14:39:21 +01:00
6866dd4801 [Widgets/Timetable] Fix "no lessons" and "no timetable" texts overlapping. 2020-03-18 12:48:04 +01:00
2186da416e [Timetable] Fix displaying "no lessons" when all lessons in next 7 days are cancelled. 2020-03-18 12:45:53 +01:00
22d859fcde [UI] Set main snackbar dismiss timeout to 7 seconds. 2020-03-18 12:44:57 +01:00
39514b69b3 [API/Vulcan] Fix API url slash issue when migrating from 3.x. 2020-03-18 12:44:28 +01:00
c384736840 [Grades] Fix counting average without weight. 2020-03-17 16:06:22 +01:00
507657f273 [UI/Messages] Improve HTML lists presentation. 2020-03-17 16:05:21 +01:00
60641742ed [4.0-beta.13] Update build.gradle, signing and changelog. 2020-03-15 23:05:03 +01:00
0fc6f07986 [Homework] Fix homework list sorting. 2020-03-15 22:20:35 +01:00
1b2bdc0580 [Login] Add QR scanner to Vulcan & Librus JST login. Implement incorrect token error in Librus JST. 2020-03-15 21:46:46 +01:00
9bac239f77 [API/Librus] Add handling message not found error. Fix for duplicated errors and exceptions. 2020-03-15 20:15:10 +01:00
371acb2d2a [Events] Disable shared notification for past events. 2020-03-15 20:01:23 +01:00
454f82e139 [Events] Disable shared notification with registration disabled. Add registration enable prompt when sharing events. 2020-03-15 19:59:48 +01:00
e8da249353 [UI] Fix date dropdown selecting wrong month. Refactor event dialogs a bit. 2020-03-15 14:54:26 +01:00
c7950c53da [API/Vulcan] Fix adding unknown subject in timetable. Fix selecting correct TeamClass in timetable. 2020-03-15 12:12:00 +01:00
b5502478e4 [Dialog/EventManual] Add process dialog and fix some things. 2020-03-14 23:27:16 +01:00
4480a7e486 [API/Librus] Fix indicating parent account during first login. 2020-03-13 16:37:30 +01:00
7c7dff743b [API] Optimize App Sync a bit. 2020-03-13 16:22:43 +01:00
c568cd3f2e [Messages] Replace hardcoded message colors with brightened/darkened versions instead of white/black. 2020-03-12 18:46:16 +01:00
6ec2bc6f21 [API/Mobidziennik] Fix duplicated line breaks when getting message. 2020-03-12 13:55:02 +01:00
af3b6f3a97 [UI] Replace material date pickers with the DatePickerDialog. Add time picker to time dropdown. 2020-03-11 21:11:35 +01:00
d855118610 [Attendance] Revert changing attendance item font. 2020-03-11 19:58:56 +01:00
c9992d9fe8 [UI] Make fragments disable pull to refresh when not scrolled to the top. 2020-03-11 19:18:24 +01:00
85fe2636cc [Home] Disable pull to refresh while swiping a card. 2020-03-11 18:41:37 +01:00
35f4a31a76 [Home] Implement dismissing, adding and removing cards. Remove debug card. 2020-03-11 18:25:28 +01:00
1e494ebb70 [Feedback] Implement feedback fragment in feedback activity. 2020-03-11 17:36:41 +01:00
ed93627505 [Grades] Implement not counting selected grades to average. 2020-03-11 16:57:12 +01:00
b9b4b0036f [Grades] Update fonts and colors a bit. 2020-03-11 16:25:54 +01:00
4aa31424d6 [4.0-beta.12] Update build.gradle, signing and changelog. 2020-03-10 23:50:24 +01:00
8a825227cb [Timetable] Disable setting metadata for normal type lessons. 2020-03-10 23:45:40 +01:00
cc1b581d7e [Grades] Show custom plus/minus value annotation in GradeDetailsDialog. 2020-03-10 23:44:02 +01:00
9936d90ae2 [Dropdown/Date] Clarify strings a bit (makes sense during weekends). 2020-03-10 22:04:13 +01:00
df1a241b2b [Timetable] Fix showing "no timetable" when all nearest lessons are cancelled. Fix a crash in timetable fragment. 2020-03-10 22:01:30 +01:00
ae89b33fb7 [Events/Manual] Implement syncing timetable when no lessons for the selected date. 2020-03-10 21:49:02 +01:00
e05b483f5c [Grades] Disable counting grade value when custom values not specified. 2020-03-10 21:45:38 +01:00
715f536b23 [MainActivity] Fix some critical errors. 2020-03-10 20:57:04 +01:00
930813fb8a [Agenda] Try to fix agenda fragment not attached crashes. 2020-03-10 20:25:14 +01:00
acd5e9b998 [Timetable] Implement lazy day loading. Introduce TimetableManager class. 2020-03-10 19:27:18 +01:00
06011bf4ae [Grades] Add grades config and mark as read menu items. 2020-03-10 18:38:28 +01:00
30e15b813c [HotFix/Timers] Change timers intervals from 1s to 500ms. 2020-03-09 22:03:44 +01:00
fcd7a7f349 [Grades] Make home card use GradeView. Update GradeDetailsDialog text color. Remove deprecated items. 2020-03-09 20:39:48 +01:00
42ef40439e [Grades] Implement showing unseen badges and marking as seen. Change default "hide improved" config value. 2020-03-09 20:18:11 +01:00
098beb14fe [Timetable/Generate] Add automatic timetable sync when no timetable for the selected week. 2020-03-09 14:57:14 +01:00
0b186a754a [API/Librus] Implement behaviour grades with types. Use optional "Phrase" in text grades. 2020-03-08 20:12:37 +01:00
d00963b53d [Grades] Implement getting correct grade colors. 2020-03-08 19:39:23 +01:00
e282af0e80 [Grades] Add option to hide improved grades. Make counting average without weight configurable. 2020-03-08 17:57:44 +01:00
630361849c [Notifications] Implement Quiet hours. Add missing timetable manual strings. 2020-03-08 17:22:14 +01:00
88a1de50ca [Changelog] Update the changelog a bit. 2020-03-07 20:14:05 +01:00
d8263d0b6a [Timetable/Manual] Add database migration to implement new model. 2020-03-07 20:09:22 +01:00
611ab0f100 [Events/Manual] Create custom views for dropdowns. Simplify dialog code. Fix wrong start time saving. 2020-03-07 20:03:47 +01:00
70c307b796 [UI/Grades] Change some fonts. 2020-03-07 11:54:47 +01:00
054a233ad6 [API/Librus] Handle some more maintenance cases. 2020-03-07 09:45:45 +01:00
174 changed files with 4609 additions and 2259 deletions

View File

@ -128,6 +128,7 @@ dependencies {
implementation "com.mikepenz:iconics-core:${versions.iconics}"
implementation "com.mikepenz:iconics-views:${versions.iconics}"
implementation "com.mikepenz:community-material-typeface:${versions.font_cmd}@aar"
implementation "com.mikepenz:materialize:1.2.1"
implementation "com.github.kuba2k2:NavLib:${versions.navlib}"

View File

@ -31,6 +31,12 @@
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
-keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); }
-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; }
-keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); }
-keepclassmembernames class com.mikepenz.materialdrawer.widget.MiniDrawerSliderView { private *; }
-keep class .R
-keep class **.R$* {
<fields>;

View File

@ -62,7 +62,7 @@
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/AppTheme.NoDisplay">
android:theme="@style/AppTheme.Dark.NoDisplay">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
@ -84,7 +84,7 @@
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/AppTheme.NoDisplay" />
android:theme="@style/AppTheme.Dark.NoDisplay" />
<!-- NOTIFICATIONS -->
<receiver android:name=".ui.widgets.notifications.WidgetNotificationsProvider"
android:label="@string/widget_notifications_title">

View File

@ -1,35 +1,29 @@
<h3>Wersja 4.0-beta.11, 2020-03-06</h3>
<h3>Wersja 4.0-rc.2, 2020-03-26</h3>
<ul>
<li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkosć oraz poprawność pobieranych danych.</li>
<li><b><u>Wysyłanie wiadomości</u></b> - funkcja, na którą czekał każdy. Od teraz w Szkolnym można wysyłać oraz odpowiadać na wiadomości do nauczycieli &#x1F44F;</li>
<li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość oraz poprawność pobieranych danych</li>
<li>Udoskonalony wygląd Szkolnego - sprawi, że korzystanie z aplikacji będzie jeszcze przyjemniejsze</li>
<li>Nowa <b>Strona główna</b> - ładniejszy wygląd oraz możliwość przestawiania kart na każdym profilu</li>
<li>Nowy <b>Plan lekcji</b> - z doskonałą obsługą lekcji przesuniętych oraz dwóch lekcji o tej samej godzinie</li>
<li>Nowe <b>Oceny</b> - z możliwością zmiany wartości plusów oraz minusów oraz wyłączenia niektórych ocen ze średniej</li>
<li>Opcja wyłączenia wybranych powiadomień z aplikacji</li>
<li>Znaczki nieprzeczytanych informacji na obrazkach profili.</li>
<br>
<br>
<li>Nowe okienka informacji o wydarzeniach oraz lekcjach</li>
<li>Nowe, przyjemniejsze powiadomienia</li>
<li>Łatwiejsze dodawanie własnych wydarzeń</li>
<li>Dużo poprawek w widoku <b>Wiadomości</b> oraz <b>Ogłoszeń</b></li>
<li>Częściowa <b>Obsługa dziennika EduDziennik</b></li>
<li>Librus: opcja logowania w dziennikach <b>Jednostek Samorządu Terytorialnego</b> oraz <b>Oświata w Radomiu</b></li>
<li>Librus: <b>poprawione obliczanie frekwencji</b></li>
<li>Librus: obsługa Zadań domowych bez posiadania Mobilnych dodatków (przez system Synergia)</li>
<li>Lepsze <b>przekazywanie powiadomień na komputer</b> oraz łatwiejsze parowanie</li>
<li>Łatwiejsze dodawanie własnych wydarzeń</li>
<li>Poprawiliśmy synchronizację w tle na niektórych telefonach</li>
<li>Usunąłem denerwujący brak zaznaczenia w lewym menu</li>
<li>Znaczna ilość błędów z poprzednich wersji już nie występuje</li>
<li><strike>Występują natomiast nowe błędy, dlatego proszę o ich zgłaszanie :)</strike></li>
</ul>
<br>
<br>
<br>
<b>Uwaga.</b> Ponieważ to wersja <i>beta</i>, niektóre funkcje mogą nie działać prawidłowo.<br>
Staramy się usuwać takie przypadki, jednak na chwilę obecną mogą występować błędy w:
<ul>
<li>Wysyłanie wiadomości może nie działać w pełni prawidłowo - proszę o zgłaszanie wszystkich błędów na naszym serwerze Discord</li>
<li>Cisza nocna w powiadomieniach jeszcze nie działa.</li>
</ul>
<br>
<br>
<br>
Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; Kuba Szczodrzyński, Kacper Ziubryniewicz 2020</i>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0x00, 0x37, 0x6a, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0x30, 0xfe, 0xe2, 0x8d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -28,9 +28,6 @@ import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import im.wangchao.mhttp.MHttp
import im.wangchao.mhttp.internal.cookie.PersistentCookieJar
import im.wangchao.mhttp.internal.cookie.cache.SetCookieCache
import im.wangchao.mhttp.internal.cookie.persistence.SharedPrefsCookiePersistor
import kotlinx.coroutines.*
import me.leolin.shortcutbadger.ShortcutBadger
import okhttp3.OkHttpClient
@ -41,12 +38,14 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.network.NetworkUtils
import pl.szczodrzynski.edziennik.network.cookie.DumbCookieJar
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager
import pl.szczodrzynski.edziennik.utils.managers.TimetableManager
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
@ -67,6 +66,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val gradesManager by lazy { GradesManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val db
get() = App.db
@ -123,7 +123,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
.followSslRedirects(false)
.build()
}
val cookieJar by lazy { PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(this)) }
val cookieJar by lazy { DumbCookieJar(this) }
/* _____ _ _
/ ____(_) | |

View File

@ -1,21 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-11.
*/
package pl.szczodrzynski.edziennik;
import android.graphics.Paint;
import android.widget.TextView;
import androidx.databinding.BindingAdapter;
public class Binding {
@BindingAdapter("strikeThrough")
public static void strikeThrough(TextView textView, Boolean strikeThrough) {
if (strikeThrough) {
textView.setPaintFlags(textView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
textView.setPaintFlags(textView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-11.
*/
package pl.szczodrzynski.edziennik
import android.graphics.Paint
import android.widget.TextView
import androidx.databinding.BindingAdapter
object Binding {
@JvmStatic
@BindingAdapter("strikeThrough")
fun strikeThrough(textView: TextView, strikeThrough: Boolean) {
if (strikeThrough) {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
textView.paintFlags = textView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
}

View File

@ -2,6 +2,8 @@ package pl.szczodrzynski.edziennik
import android.Manifest
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@ -10,6 +12,7 @@ import android.content.res.Resources
import android.database.Cursor
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
@ -23,6 +26,7 @@ import android.util.Base64
import android.util.Base64.NO_WRAP
import android.util.Base64.encodeToString
import android.view.View
import android.view.WindowManager
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.RadioButton
@ -51,6 +55,7 @@ import okhttp3.RequestBody
import okhttp3.TlsVersion
import okio.Buffer
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.db.entity.Notification
@ -1080,6 +1085,7 @@ fun Throwable.toErrorCode() = when (this) {
private fun ApiResponse.Error.toErrorCode() = when (this.code) {
else -> ERROR_API_EXCEPTION
}
fun Throwable.toApiError(tag: String) = ApiError.fromThrowable(tag, this)
inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? {
if (a != null && b != null) {
@ -1092,3 +1098,71 @@ inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? {
fun Iterable<Int>.averageOrNull() = this.average().let { if (it.isNaN()) null else it }
@kotlin.jvm.JvmName("averageOrNullOfFloat")
fun Iterable<Float>.averageOrNull() = this.average().let { if (it.isNaN()) null else it }
fun String.copyToClipboard(context: Context) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("Tekst", this)
clipboard.primaryClip = clipData
}
fun TextView.getTextPosition(range: IntRange): Rect {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
// Initialize global value
var parentTextViewRect = Rect()
// Initialize values for the computing of clickedText position
//val completeText = parentTextView.text as SpannableString
val textViewLayout = this.layout
val startOffsetOfClickedText = range.first//completeText.getSpanStart(clickedText)
val endOffsetOfClickedText = range.last//completeText.getSpanEnd(clickedText)
var startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(startOffsetOfClickedText)
var endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(endOffsetOfClickedText)
// Get the rectangle of the clicked text
val currentLineStartOffset = textViewLayout.getLineForOffset(startOffsetOfClickedText)
val currentLineEndOffset = textViewLayout.getLineForOffset(endOffsetOfClickedText)
val keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset
textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect)
// Update the rectangle position to his real position on screen
val parentTextViewLocation = intArrayOf(0, 0)
this.getLocationOnScreen(parentTextViewLocation)
val parentTextViewTopAndBottomOffset = (parentTextViewLocation[1] - this.scrollY + this.compoundPaddingTop)
parentTextViewRect.top += parentTextViewTopAndBottomOffset
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset
// In the case of multi line text, we have to choose what rectangle take
if (keywordIsInMultiLine) {
val screenHeight = windowManager.defaultDisplay.height
val dyTop = parentTextViewRect.top
val dyBottom = screenHeight - parentTextViewRect.bottom
val onTop = dyTop > dyBottom
if (onTop) {
endXCoordinatesOfClickedText = textViewLayout.getLineRight(currentLineStartOffset);
} else {
parentTextViewRect = Rect()
textViewLayout.getLineBounds(currentLineEndOffset, parentTextViewRect);
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
startXCoordinatesOfClickedText = textViewLayout.getLineLeft(currentLineEndOffset);
}
}
parentTextViewRect.left += (
parentTextViewLocation[0] +
startXCoordinatesOfClickedText +
this.compoundPaddingLeft -
this.scrollX
).toInt()
parentTextViewRect.right = (
parentTextViewRect.left +
endXCoordinatesOfClickedText -
startXCoordinatesOfClickedText
).toInt()
return parentTextViewRect
}

View File

@ -12,7 +12,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.Toast
@ -32,8 +31,8 @@ import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.model.utils.withIsHiddenInMiniDrawer
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@ -42,7 +41,6 @@ import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
@ -51,6 +49,7 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
@ -91,7 +90,6 @@ import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import pl.szczodrzynski.navlib.drawer.NavDrawer
import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem
import pl.szczodrzynski.navlib.drawer.items.withAppTitle
import java.io.File
import java.io.IOException
import java.util.*
@ -286,8 +284,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
setContentView(b.root)
Log.d(TAG, Signing.appPassword)
mainSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
@ -364,7 +360,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
false
}
drawerProfileLongClickListener = { _, profile, _, view ->
if (profile is ProfileDrawerItem) {
if (view != null && profile is ProfileDrawerItem) {
showProfileContextMenu(profile, view)
true
}
@ -448,6 +444,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// WHAT'S NEW DIALOG
if (app.config.appVersion < BuildConfig.VERSION_CODE) {
// force an AppSync after update
app.config.sync.lastAppSync = 0L
ChangelogDialog(this)
if (app.config.appVersion < 170) {
//Intent intent = new Intent(this, ChangelogIntroActivity.class);
@ -725,6 +723,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
)
true
}
"createManualEvent" -> {
val date = extras.getString("eventDate")?.let { Date.fromY_m_d(it) } ?: Date.getToday()
EventManualDialog(
this,
App.profileId,
defaultDate = date
)
true
}
else -> false
}
if (handled && !navLoading) {
@ -906,9 +913,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
bottomSheet.close()
bottomSheet.removeAllContextual()
bottomSheet.toggleGroupEnabled = false
bottomSheet.onCloseListener = null
drawer.close()
drawer.setSelection(target.id, fireOnClick = false)
if (drawer.getSelection() != target.id)
drawer.setSelection(target.id, fireOnClick = false)
navView.toolbar.setTitle(target.title ?: target.name)
navView.bottomBar.fabEnable = false
navView.bottomBar.fabExtended = false
@ -1056,7 +1063,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val item = DrawerPrimaryItem()
.withIdentifier(target.id.toLong())
.withName(target.name)
.withHiddenInMiniDrawer(!app.config.ui.miniMenuButtons.contains(target.id))
.withIsHiddenInMiniDrawer(!app.config.ui.miniMenuButtons.contains(target.id))
.also { if (target.description != null) it.withDescription(target.description!!) }
.also { if (target.icon != null) it.withIcon(target.icon!!) }
.also { if (target.title != null) it.withAppTitle(getString(target.title!!)) }
@ -1120,7 +1127,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawer.addProfileSettings(*drawerProfiles.toTypedArray())
}
private fun showProfileContextMenu(profile: IProfile<*>, view: View) {
private fun showProfileContextMenu(profile: IProfile, view: View) {
val profileId = profile.identifier.toInt()
val popupMenu = PopupMenu(this, view)
popupMenu.menu.add(0, 1, 1, R.string.profile_menu_open_settings)
@ -1133,7 +1140,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
loadTarget(DRAWER_ITEM_SETTINGS, null)
} else if (item.itemId == 2) {
ProfileRemoveDialog(this, profileId, profile.name?.getText(this)?.toString() ?: "?")
ProfileRemoveDialog(this, profileId, profile.name?.getText(this) ?: "?")
}
true
}
@ -1144,7 +1151,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
private var targetHomeId: Int = -1
override fun onBackPressed() {
if (!b.navView.onBackPressed()) {
if (App.config.ui.openDrawerOnBackPressed) {
if (App.config.ui.openDrawerOnBackPressed && ((navTarget.popTo == null && navTarget.popToHome)
|| navTarget.id == DRAWER_ITEM_HOME)) {
b.navView.drawer.toggle()
} else {
navigateUp()

View File

@ -22,7 +22,7 @@ import kotlin.coroutines.CoroutineContext
class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
companion object {
const val DATA_VERSION = 10
const val DATA_VERSION = 11
}
private val job = Job()
@ -105,11 +105,6 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { set("lastAppSync", value); mLastAppSync = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigSync(private val config: Config) {
private var mDontShowAppManagerDialog: Boolean? = null
@ -19,6 +20,11 @@ class ConfigSync(private val config: Config) {
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true }
set(value) { config.set("syncEnabled", value); mSyncEnabled = value }
private var mWebPushEnabled: Boolean? = null
var webPushEnabled: Boolean
get() { mWebPushEnabled = mWebPushEnabled ?: config.values.get("webPushEnabled", true); return mWebPushEnabled ?: true }
set(value) { config.set("webPushEnabled", value); mWebPushEnabled = value }
private var mSyncOnlyWifi: Boolean? = null
var onlyWifi: Boolean
get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates }
@ -34,20 +40,30 @@ class ConfigSync(private val config: Config) {
get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true }
set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: config.values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { config.set("lastAppSync", value); mLastAppSync = value }
/* ____ _ _ _
/ __ \ (_) | | | |
| | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___
| | | | | | | |/ _ \ __| | '_ \ / _ \| | | | '__/ __|
| |__| | |_| | | __/ |_ | | | | (_) | |_| | | \__ \
\___\_\\__,_|_|\___|\__| |_| |_|\___/ \__,_|_| |__*/
private var mQuietHoursStart: Long? = null
var quietHoursStart: Long
get() { mQuietHoursStart = mQuietHoursStart ?: config.values.get("quietHoursStart", 0L); return mQuietHoursStart ?: 0L }
private var mQuietHoursEnabled: Boolean? = null
var quietHoursEnabled: Boolean
get() { mQuietHoursEnabled = mQuietHoursEnabled ?: config.values.get("quietHoursEnabled", false); return mQuietHoursEnabled ?: false }
set(value) { config.set("quietHoursEnabled", value); mQuietHoursEnabled = value }
private var mQuietHoursStart: Time? = null
var quietHoursStart: Time?
get() { mQuietHoursStart = mQuietHoursStart ?: config.values.get("quietHoursStart", null as Time?); return mQuietHoursStart }
set(value) { config.set("quietHoursStart", value); mQuietHoursStart = value }
private var mQuietHoursEnd: Long? = null
var quietHoursEnd: Long
get() { mQuietHoursEnd = mQuietHoursEnd ?: config.values.get("quietHoursEnd", 0L); return mQuietHoursEnd ?: 0L }
private var mQuietHoursEnd: Time? = null
var quietHoursEnd: Time?
get() { mQuietHoursEnd = mQuietHoursEnd ?: config.values.get("quietHoursEnd", null as Time?); return mQuietHoursEnd }
set(value) { config.set("quietHoursEnd", value); mQuietHoursEnd = value }
private var mQuietDuringLessons: Boolean? = null
@ -90,4 +106,4 @@ class ConfigSync(private val config: Config) {
var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
}
}

View File

@ -21,10 +21,15 @@ class ProfileConfigGrades(private val config: ProfileConfig) {
get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES }
set(value) { config.set("yearAverageMode", value); mYearAverageMode = value }
private var mCountZeroToAvg: Boolean? = null
var countZeroToAvg: Boolean
get() { mCountZeroToAvg = mCountZeroToAvg ?: config.values.get("countZeroToAvg", true); return mCountZeroToAvg ?: true }
set(value) { config.set("countZeroToAvg", value); mCountZeroToAvg = value }
private var mHideImproved: Boolean? = null
var hideImproved: Boolean
get() { mHideImproved = mHideImproved ?: config.values.get("hideImproved", false); return mHideImproved ?: false }
set(value) { config.set("hideImproved", value); mHideImproved = value }
private var mAverageWithoutWeight: Boolean? = null
var averageWithoutWeight: Boolean
get() { mAverageWithoutWeight = mAverageWithoutWeight ?: config.values.get("averageWithoutWeight", true); return mAverageWithoutWeight ?: true }
set(value) { config.set("averageWithoutWeight", value); mAverageWithoutWeight = value }
private var mPlusValue: Float? = null
var plusValue: Float?
@ -35,6 +40,11 @@ class ProfileConfigGrades(private val config: ProfileConfig) {
get() { mMinusValue = mMinusValue ?: config.values.getFloat("minusValue"); return mMinusValue }
set(value) { config.set("minusValue", value); mMinusValue = value }
private var mDontCountEnabled: Boolean? = null
var dontCountEnabled: Boolean
get() { mDontCountEnabled = mDontCountEnabled ?: config.values.get("dontCountEnabled", false); return mDontCountEnabled ?: false }
set(value) { config.set("dontCountEnabled", value); mDontCountEnabled = value }
private var mDontCountGrades: List<String>? = null
var dontCountGrades: List<String>
get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() }

View File

@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.math.abs
class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
init { config.apply {
@ -42,9 +43,7 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
devModePassword = p.getString("$s.devModePassword", null).fix()
sync.tokenApp = p.getString("$s.fcmToken", null).fix()
timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0
sync.quietHoursStart = p.getString("$s.quietHoursStart", null)?.toLongOrNull() ?: 0
appRateSnackbarTime = p.getString("$s.appRateSnackbarTime", null)?.toLongOrNull() ?: 0
sync.quietHoursEnd = p.getString("$s.quietHoursEnd", null)?.toLongOrNull() ?: 0
timetable.countInSeconds = p.getString("$s.countInSeconds", null)?.toBoolean() ?: false
ui.headerBackground = p.getString("$s.headerBackground", null).fix()
ui.appBackground = p.getString("$s.appBackground", null).fix()
@ -59,6 +58,22 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
sync.notifyAboutUpdates = p.getString("$s.notifyAboutUpdates", null)?.toBoolean() ?: true
timetable.bellSyncDiff = p.getString("$s.bellSyncDiff", null)?.let { Gson().fromJson(it, Time::class.java) }
val startMillis = p.getString("$s.quietHoursStart", null)?.toLongOrNull() ?: 0
val endMillis = p.getString("$s.quietHoursEnd", null)?.toLongOrNull() ?: 0
if (startMillis > 0) {
try {
sync.quietHoursStart = Time.fromMillis(abs(startMillis))
sync.quietHoursEnd = Time.fromMillis(abs(endMillis))
sync.quietHoursEnabled = true
}
catch (_: Exception) {}
}
else {
sync.quietHoursEnabled = false
sync.quietHoursStart = null
sync.quietHoursEnd = null
}
sync.tokenMobidziennikList = listOf()
sync.tokenVulcanList = listOf()
sync.tokenLibrusList = listOf()
@ -78,4 +93,4 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
private fun String?.fix(): String? {
return this?.replace("\"", "")?.let { if (it == "null") null else it }
}
}
}

View File

@ -11,6 +11,8 @@ import pl.szczodrzynski.edziennik.HOUR
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.math.abs
class ConfigMigration(app: App, config: Config) {
init { config.apply {
@ -43,8 +45,9 @@ class ConfigMigration(app: App, config: Config) {
sync.interval = 1*HOUR.toInt()
sync.notifyAboutUpdates = true
sync.onlyWifi = false
sync.quietHoursStart = 0
sync.quietHoursEnd = 0
sync.quietHoursEnabled = false
sync.quietHoursStart = null
sync.quietHoursEnd = null
sync.quietDuringLessons = false
sync.tokenApp = null
sync.tokenMobidziennik = null
@ -69,5 +72,25 @@ class ConfigMigration(app: App, config: Config) {
dataVersion = 10
}
if (dataVersion < 11) {
val startMillis = config.values.get("quietHoursStart", 0L)
val endMillis = config.values.get("quietHoursEnd", 0L)
if (startMillis > 0) {
try {
sync.quietHoursStart = Time.fromMillis(abs(startMillis))
sync.quietHoursEnd = Time.fromMillis(abs(endMillis))
sync.quietHoursEnabled = true
}
catch (_: Exception) {}
}
else {
sync.quietHoursEnabled = false
sync.quietHoursStart = null
sync.quietHoursEnd = null
}
dataVersion = 11
}
}}
}

View File

@ -14,7 +14,7 @@ class ProfileConfigMigration(config: ProfileConfig) {
if (dataVersion < 1) {
grades.colorMode = COLOR_MODE_WEIGHTED
grades.countZeroToAvg = true
grades.dontCountEnabled = false
grades.yearAverageMode = YEAR_ALL_GRADES
ui.agendaViewType = AGENDA_DEFAULT

View File

@ -22,6 +22,7 @@ import pl.szczodrzynski.edziennik.data.api.task.ErrorReportTask
import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.toApiError
import pl.szczodrzynski.edziennik.utils.Utils.d
import kotlin.math.min
import kotlin.math.roundToInt
@ -181,7 +182,7 @@ class ApiService : Service() {
is SzkolnyTask -> task.run(taskCallback)
}
} catch (e: Exception) {
taskCallback.onError(ApiError(TAG, EXCEPTION_API_TASK).withThrowable(e))
taskCallback.onError(e.toApiError(TAG))
}
}

View File

@ -124,6 +124,8 @@ const val ERROR_LIBRUS_PORTAL_MAINTENANCE = 182
const val ERROR_LIBRUS_API_NOTICEBOARD_PROBLEM = 183
const val ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED = 184
const val ERROR_LIBRUS_API_DEVICE_REGISTERED = 185
const val ERROR_LIBRUS_MESSAGES_NOT_FOUND = 186
const val ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST = 187
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202
@ -140,6 +142,7 @@ const val ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE = 214
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID = 215
const val ERROR_LOGIN_MOBIDZIENNIK_API2_INVALID_LOGIN = 216
const val ERROR_LOGIN_MOBIDZIENNIK_API2_OTHER = 217
const val ERROR_MOBIDZIENNIK_WEB_SERVER_PROBLEM = 218
const val ERROR_LOGIN_VULCAN_INVALID_SYMBOL = 301
const val ERROR_LOGIN_VULCAN_INVALID_TOKEN = 302

View File

@ -70,13 +70,13 @@ val librusLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_SYNERGIA, LibrusLoginSynergia::class.java)
.withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") }
.withRequiredLoginMethod { profile, _ ->
if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_API else LOGIN_METHOD_NOT_NEEDED
if (profile?.hasStudentData("accountPassword") == false || true) LOGIN_METHOD_LIBRUS_API else LOGIN_METHOD_NOT_NEEDED
},
LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_MESSAGES, LibrusLoginMessages::class.java)
.withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") }
.withRequiredLoginMethod { profile, _ ->
if (profile?.hasStudentData("accountPassword") == false) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED
if (profile?.hasStudentData("accountPassword") == false || true) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED
}
)

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api
import kotlin.text.RegexOption.DOT_MATCHES_ALL
import kotlin.text.RegexOption.IGNORE_CASE
object Regexes {
val STYLE_CSS_COLOR by lazy {
@ -126,7 +127,7 @@ object Regexes {
val LIBRUS_ATTACHMENT_KEY by lazy {
"""singleUseKey=([0-9A-f_]+)""".toRegex()
"""singleUseKey=([0-9A-z_]+)""".toRegex()
}
@ -199,4 +200,20 @@ object Regexes {
val EDUDZIENNIK_TEACHERS by lazy {
"""<div class="teacher">.*?<p>(.+?) (.+?)</p>""".toRegex(DOT_MATCHES_ALL)
}
val LINKIFY_DATE_YMD by lazy {
"""(1\d{3}|20\d{2})[\-./](1[0-2]|0?\d)[\-./]([1-2]\d|3[0-1]|0?\d)""".toRegex()
}
val LINKIFY_DATE_DMY by lazy {
"""(?<![\d\-./])([1-2]\d|3[0-1]|0?\d)[\-./](1[0-2]|0?\d)(?:[\-./](1\d{3}|2?0?\d{2}))?(?![\d\-/])""".toRegex()
}
val LINKIFY_DATE_ABSOLUTE by lazy {
"""([1-3][0-9]|[1-9])\s(sty|lut|mar|kwi|maj|cze|lip|sie|wrz|paź|lis|gru).*?\s(1[0-9]{3}|20[0-9]{2})?""".toRegex(IGNORE_CASE)
}
val LINKIFY_DATE_RELATIVE by lazy {
"""za\s([0-9]+)?\s?(dni|dzień|tydzień|tygodnie)""".toRegex(IGNORE_CASE)
}
}

View File

@ -7,7 +7,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -43,8 +42,8 @@ open class EdudziennikWeb(open val data: DataEdudziennik, open val lastSync: Lon
if (semester == null && url.contains("start")) {
profile?.also { profile ->
val cookies = data.app.cookieJar.getForDomain("dziennikel.appspot.com")
val semesterCookie = cookies.firstOrNull { it.name() == "semester" }?.value()?.toIntOrNull()
val cookies = data.app.cookieJar.getAll("dziennikel.appspot.com")
val semesterCookie = cookies["semester"]?.toIntOrNull()
semesterCookie?.let { data.currentSemester = it }
@ -75,13 +74,7 @@ open class EdudziennikWeb(open val data: DataEdudziennik, open val lastSync: Lon
}
}
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("sessionid")
.value(data.webSessionId!!)
.domain("dziennikel.appspot.com")
.secure().httpOnly().build()
))
data.app.cookieJar.set("dziennikel.appspot.com", "sessionid", data.webSessionId)
Request.builder()
.url(url)

View File

@ -24,7 +24,7 @@ class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit)
onSuccess()
}
else {
data.app.cookieJar.clearForDomain("dziennikel.appspot.com")
data.app.cookieJar.clear("dziennikel.appspot.com")
if (data.loginEmail.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) {
loginWithCredentials()
}
@ -59,8 +59,8 @@ class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit)
}
}
val cookies = data.app.cookieJar.getForDomain("dziennikel.appspot.com")
val sessionId = cookies.firstOrNull { it.name() == "sessionid" }?.value()
val cookies = data.app.cookieJar.getAll("dziennikel.appspot.com")
val sessionId = cookies["sessionid"]
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID)

View File

@ -5,7 +5,6 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik
import androidx.core.util.set
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_IDZIENNIK_API
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_IDZIENNIK_WEB
@ -24,18 +23,8 @@ class DataIdziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(
loginMethods.clear()
if (isWebLoginValid()) {
loginMethods += LOGIN_METHOD_IDZIENNIK_WEB
app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("ASP.NET_SessionId_iDziennik")
.value(webSessionId!!)
.domain("iuczniowie.progman.pl")
.secure().httpOnly().build(),
Cookie.Builder()
.name(".ASPXAUTH")
.value(webAuth!!)
.domain("iuczniowie.progman.pl")
.secure().httpOnly().build()
))
app.cookieJar.set("iuczniowie.progman.pl", "ASP.NET_SessionId_iDziennik", webSessionId)
app.cookieJar.set("iuczniowie.progman.pl", ".ASPXAUTH", webAuth)
}
if (isApiLoginValid())
loginMethods += LOGIN_METHOD_IDZIENNIK_API

View File

@ -16,6 +16,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Announcement
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
class IdziennikWebAnnouncements(override val data: DataIdziennik,
@ -43,11 +45,11 @@ class IdziennikWebAnnouncements(override val data: DataIdziennik,
for (jAnnouncementEl in json.getAsJsonArray("ListK")) {
val jAnnouncement = jAnnouncementEl.asJsonObject
// jAnnouncement
val announcementId = jAnnouncement.get("Id").asLong
val announcementId = jAnnouncement.getLong("Id") ?: -1
val rTeacher = data.getTeacherByFirstLast(jAnnouncement.get("Autor").asString)
val addedDate = java.lang.Long.parseLong(jAnnouncement.get("DataDodania").asString.replace("[^\\d]".toRegex(), ""))
val startDate = Date.fromMillis(java.lang.Long.parseLong(jAnnouncement.get("DataWydarzenia").asString.replace("[^\\d]".toRegex(), "")))
val rTeacher = data.getTeacherByFirstLast(jAnnouncement.getString("Autor") ?: "")
val addedDate = jAnnouncement.getString("DataDodania")?.replace("[^\\d]".toRegex(), "")?.toLongOrNull() ?: System.currentTimeMillis()
val startDate = jAnnouncement.getString("DataWydarzenia")?.replace("[^\\d]".toRegex(), "")?.toLongOrNull()?.let { Date.fromMillis(it) }
val announcementObject = Announcement(
profileId,

View File

@ -7,7 +7,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.DataIdziennik
@ -24,22 +23,12 @@ class IdziennikLoginWeb(val data: DataIdziennik, val onSuccess: () -> Unit) {
init { run {
if (data.isWebLoginValid()) {
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("ASP.NET_SessionId_iDziennik")
.value(data.webSessionId!!)
.domain("iuczniowie.progman.pl")
.secure().httpOnly().build(),
Cookie.Builder()
.name(".ASPXAUTH")
.value(data.webAuth!!)
.domain("iuczniowie.progman.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("iuczniowie.progman.pl", "ASP.NET_SessionId_iDziennik", data.webSessionId)
data.app.cookieJar.set("iuczniowie.progman.pl", ".ASPXAUTH", data.webAuth)
onSuccess()
}
else {
data.app.cookieJar.clearForDomain("iuczniowie.progman.pl")
data.app.cookieJar.clear("iuczniowie.progman.pl")
if (data.webSchoolName != null && data.webUsername != null && data.webPassword != null) {
loginWithCredentials()
}
@ -62,11 +51,11 @@ class IdziennikLoginWeb(val data: DataIdziennik, val onSuccess: () -> Unit) {
// login succeeded: there is a start page
if (text.contains("czyWyswietlicDostepMobilny")) {
val cookies = data.app.cookieJar.getForDomain("iuczniowie.progman.pl")
val cookies = data.app.cookieJar.getAll("iuczniowie.progman.pl")
run {
data.webSessionId = cookies.singleOrNull { it.name() == "ASP.NET_SessionId_iDziennik" }?.value() ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_SESSION
data.webAuth = cookies.singleOrNull { it.name() == ".ASPXAUTH" }?.value() ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_AUTH
data.apiBearer = cookies.singleOrNull { it.name() == "Bearer" }?.value() ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_BEARER
data.webSessionId = cookies["ASP.NET_SessionId_iDziennik"] ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_SESSION
data.webAuth = cookies[".ASPXAUTH"] ?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_AUTH
data.apiBearer = cookies["Bearer"]?: return@run ERROR_LOGIN_IDZIENNIK_WEB_NO_BEARER
data.loginExpiryTime = response.getUnixDate() + 30 * MINUTE /* after about 40 minutes the login didn't work already */
data.apiExpiryTime = response.getUnixDate() + 12 * HOUR /* actually it expires after 24 hours but I'm not sure when does the token refresh. */

View File

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.librus
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_API
@ -31,23 +30,11 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
loginMethods += LOGIN_METHOD_LIBRUS_API
if (isSynergiaLoginValid()) {
loginMethods += LOGIN_METHOD_LIBRUS_SYNERGIA
app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(synergiaSessionId!!)
.domain("synergia.librus.pl")
.secure().httpOnly().build()
))
app.cookieJar.set("synergia.librus.pl", "DZIENNIKSID", synergiaSessionId)
}
if (isMessagesLoginValid()) {
loginMethods += LOGIN_METHOD_LIBRUS_MESSAGES
app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(messagesSessionId!!)
.domain("wiadomosci.librus.pl")
.secure().httpOnly().build()
))
app.cookieJar.set("wiadomosci.librus.pl", "DZIENNIKSID", messagesSessionId)
}
}

View File

@ -64,7 +64,7 @@ val LibrusFeatures = listOf(
Feature(LOGIN_TYPE_LIBRUS, FEATURE_PUSH_CONFIG, listOf(
ENDPOINT_LIBRUS_API_PUSH_CONFIG to LOGIN_METHOD_LIBRUS_API
), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data ->
!data.app.config.sync.tokenLibrusList.contains(data.profileId)
(data as DataLibrus).isPremium && !data.app.config.sync.tokenLibrusList.contains(data.profileId)
},
@ -116,11 +116,11 @@ val LibrusFeatures = listOf(
* Homework - using API.
* Sync only if account has premium access.
*/
Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf(
/*Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf(
ENDPOINT_LIBRUS_API_HOMEWORK to LOGIN_METHOD_LIBRUS_API
), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data ->
(data as DataLibrus).isPremium
},
},*/
/**
* Behaviour - using API.
*/
@ -227,9 +227,9 @@ val LibrusFeatures = listOf(
*/
Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf(
ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK to LOGIN_METHOD_LIBRUS_SYNERGIA
), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)).withShouldSync { data ->
), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA))/*.withShouldSync { data ->
!(data as DataLibrus).isPremium
},
}*/,
/**
* Messages inbox - using messages website.

View File

@ -49,6 +49,7 @@ open class LibrusApi(open val data: DataLibrus, open val lastSync: Long?) {
val error = if (response?.code() == 200) null else
json.getString("Code") ?:
json.getString("Message") ?:
json.getString("Status") ?:
response?.parserErrorBody
error?.let { code ->
when (code) {
@ -67,6 +68,7 @@ open class LibrusApi(open val data: DataLibrus, open val lastSync: Long?) {
"Nieprawidłowy węzeł." -> ERROR_LIBRUS_API_INCORRECT_ENDPOINT
"NoticeboardProblem" -> ERROR_LIBRUS_API_NOTICEBOARD_PROBLEM
"DeviceRegistered" -> ERROR_LIBRUS_API_DEVICE_REGISTERED
"Maintenance" -> ERROR_LIBRUS_API_MAINTENANCE
else -> ERROR_LIBRUS_API_OTHER
}.let { errorCode ->
if (errorCode !in ignoreErrors) {
@ -119,6 +121,7 @@ open class LibrusApi(open val data: DataLibrus, open val lastSync: Long?) {
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_UNAVAILABLE)
.allowErrorCode(HTTP_NOT_FOUND)
.allowErrorCode(503)
.callback(callback)
.build()
.enqueue()

View File

@ -12,7 +12,6 @@ import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.FileCallbackHandler
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import org.json.JSONObject
import org.json.XML
import org.jsoup.Jsoup
@ -55,13 +54,20 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) {
}
when {
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text.contains("<status>error</status>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text.contains("<type>eVarWhitThisNameNotExists</type>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("<error>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN
text.contains("<message>Nie odnaleziono wiadomości.</message>") -> ERROR_LIBRUS_MESSAGES_NOT_FOUND
text.contains("stop.png") -> ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED
text.contains("eAccessDeny") -> ERROR_LIBRUS_MESSAGES_ACCESS_DENIED
text.contains("OffLine") -> ERROR_LIBRUS_MESSAGES_MAINTENANCE
text.contains("<status>error</status>") -> ERROR_LIBRUS_MESSAGES_ERROR
text.contains("<type>eVarWhitThisNameNotExists</type>") -> ERROR_LIBRUS_MESSAGES_ACCESS_DENIED
text.contains("<error>") -> ERROR_LIBRUS_MESSAGES_OTHER
else -> null
}?.let { errorCode ->
data.error(ApiError(tag, errorCode)
.withApiResponse(text)
.withResponse(response))
return
}
try {
@ -82,14 +88,7 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) {
}
}
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(data.messagesSessionId!!)
.domain("wiadomosci.librus.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("wiadomosci.librus.pl", "DZIENNIKSID", data.messagesSessionId)
val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val doc = docBuilder.newDocument()
@ -139,13 +138,20 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) {
}
when {
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text.contains("<status>error</status>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text.contains("<type>eVarWhitThisNameNotExists</type>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("<error>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN
text.contains("<message>Nie odnaleziono wiadomości.</message>") -> ERROR_LIBRUS_MESSAGES_NOT_FOUND
text.contains("stop.png") -> ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED
text.contains("eAccessDeny") -> ERROR_LIBRUS_MESSAGES_ACCESS_DENIED
text.contains("OffLine") -> ERROR_LIBRUS_MESSAGES_MAINTENANCE
text.contains("<status>error</status>") -> ERROR_LIBRUS_MESSAGES_ERROR
text.contains("<type>eVarWhitThisNameNotExists</type>") -> ERROR_LIBRUS_MESSAGES_ACCESS_DENIED
text.contains("<error>") -> ERROR_LIBRUS_MESSAGES_OTHER
else -> null
}?.let { errorCode ->
data.error(ApiError(tag, errorCode)
.withApiResponse(text)
.withResponse(response))
return
}
try {
@ -166,14 +172,7 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) {
}
}
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(data.messagesSessionId!!)
.domain("wiadomosci.librus.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("wiadomosci.librus.pl", "DZIENNIKSID", data.messagesSessionId)
val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val doc = docBuilder.newDocument()

View File

@ -44,6 +44,7 @@ open class LibrusPortal(open val data: DataLibrus) {
"Access token is invalid" -> ERROR_LIBRUS_PORTAL_ACCESS_DENIED
"ApiDisabled" -> ERROR_LIBRUS_PORTAL_API_DISABLED
"Account not found" -> ERROR_LIBRUS_PORTAL_SYNERGIA_NOT_FOUND
"Unable to refresh the account" -> ERROR_LIBRUS_PORTAL_MAINTENANCE
else -> when (json.getString("hint")) {
"Error while decoding to JSON" -> ERROR_LIBRUS_PORTAL_ACCESS_DENIED
else -> ERROR_LIBRUS_PORTAL_OTHER
@ -97,6 +98,7 @@ open class LibrusPortal(open val data: DataLibrus) {
.allowErrorCode(HttpURLConnection.HTTP_UNAUTHORIZED)
.allowErrorCode(HttpURLConnection.HTTP_BAD_REQUEST)
.allowErrorCode(HttpURLConnection.HTTP_GONE)
.allowErrorCode(424)
.callback(callback)
.build()
.enqueue()

View File

@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Attendance
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
class LibrusApiAttendances(override val data: DataLibrus,
override val lastSync: Long?,
@ -42,10 +43,10 @@ class LibrusApiAttendances(override val data: DataLibrus,
val teacherId = attendance.getJsonObject("AddedBy")?.getLong("Id")
val semester = attendance.getInt("Semester") ?: return@forEach
val type = attendance.getJsonObject("Type")?.getLong("Id") ?: return@forEach
val typeObject = data.attendanceTypes.get(type)
val typeObject = data.attendanceTypes[type] ?: null
val topic = typeObject?.name ?: ""
val startTime = data.lessonRanges.get(lessonNo).startTime
val startTime = data.lessonRanges.get(lessonNo)?.startTime
val lesson = if (lessonId != -1L)
data.librusLessons.singleOrNull { it.lessonId == lessonId }
@ -59,14 +60,14 @@ class LibrusApiAttendances(override val data: DataLibrus,
semester,
topic,
lessonDate,
startTime,
typeObject.type
startTime ?: Time(0, 0, 0),
typeObject?.type ?: Attendance.TYPE_CUSTOM
)
val addedDate = Date.fromIso(attendance.getString("AddDate") ?: return@forEach)
data.attendanceList.add(attendanceObject)
if(typeObject.type != Attendance.TYPE_PRESENT) {
if(typeObject?.type != Attendance.TYPE_PRESENT) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_ATTENDANCE,

View File

@ -27,6 +27,17 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus,
private val nameFormat by lazy { DecimalFormat("#.##") }
private val types by lazy {
mapOf(
1 to ("wz" to "wzorowe"),
2 to ("bdb" to "bardzo dobre"),
3 to ("db" to "dobre"),
4 to ("popr" to "poprawne"),
5 to ("ndp" to "nieodpowiednie"),
6 to ("ng" to "naganne")
)
}
init { data.profile?.also { profile ->
apiGet(TAG, "BehaviourGrades/Points") { json ->
@ -95,8 +106,12 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus,
val addedDate = grade.getString("AddDate")?.let { Date.fromIso(it) }
?: System.currentTimeMillis()
val text = grade.getString("Text")
val type = grade.getJsonObject("BehaviourGrade")?.getInt("Id")?.let { types[it] }
val name = when {
value != null -> (if (value >= 0) "+" else "") + nameFormat.format(value)
type != null -> type.first
value != null -> (if (value > 0) "+" else "") + nameFormat.format(value)
shortName != null -> shortName
else -> return@forEach
}
@ -115,14 +130,14 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus,
val categoryName = category?.text ?: ""
val description = grade.getJsonArray("Comments")?.asJsonObjectList()?.let { comments ->
if (comments.isNotEmpty()) {
data.gradeCategories.singleOrNull {
it.type == GradeCategory.TYPE_BEHAVIOUR_COMMENT
&& it.categoryId == comments[0].asJsonObject.getLong("Id")
}?.text
} else null
} ?: ""
val comments = grade.getJsonArray("Comments")
?.asJsonObjectList()
?.mapNotNull { comment ->
val cId = comment.getLong("Id") ?: return@mapNotNull null
data.gradeCategories[cId]?.text
} ?: listOf()
val description = listOfNotNull(type?.second) + comments
val valueFrom = value ?: category?.valueFrom ?: 0f
val valueTo = category?.valueTo ?: 0f
@ -136,8 +151,8 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus,
weight = -1f,
color = color,
category = categoryName,
description = description,
comment = null,
description = text ?: description.join(" - "),
comment = if (text != null) description.join(" - ") else null,
semester = semester,
teacherId = teacherId,
subjectId = 1

View File

@ -4,15 +4,12 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -41,9 +38,10 @@ class LibrusApiLuckyNumber(override val data: DataLibrus,
luckyNumber
)
//if (luckyNumberDate > Date.getToday()) {
if (luckyNumberDate >= Date.getToday())
nextSync = luckyNumberDate.combineWith(Time(15, 0, 0))
//}
else
nextSync = System.currentTimeMillis() + 6*HOUR*1000
data.luckyNumberList.add(luckyNumberObject)
data.metadataList.add(

View File

@ -21,6 +21,14 @@ class LibrusApiPushConfig(override val data: DataLibrus,
}
init { data.app.config.sync.tokenLibrus?.also { tokenLibrus ->
if(tokenLibrus.isEmpty()) {
data.setSyncNext(ENDPOINT_LIBRUS_API_PUSH_CONFIG, SYNC_ALWAYS)
data.app.config.sync.tokenLibrusList =
data.app.config.sync.tokenLibrusList + profileId
onSuccess(ENDPOINT_LIBRUS_API_PUSH_CONFIG)
return@also
}
apiGet(TAG, "ChangeRegister", payload = JsonObject(
"provider" to "FCM",
"device" to tokenLibrus,

View File

@ -57,7 +57,7 @@ class LibrusApiTextGrades(override val data: DataLibrus,
color = category?.color ?: -1,
category = category?.text ?: "",
description = description,
comment = null,
comment = grade.getString("Phrase") /* whatever it is */,
semester = semester,
teacherId = teacherId,
subjectId = subjectId

View File

@ -111,7 +111,7 @@ class LibrusMessagesGetList(override val data: DataLibrus,
data.messageIgnoreList.add(messageObject)
data.messageRecipientList.add(messageRecipientObject)
data.metadataList.add(Metadata(
data.setSeenMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,

View File

@ -29,7 +29,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus,
init { data.profile?.also { profile ->
synergiaGet(TAG, "moje_zadania", method = POST, parameters = mapOf(
"dataOd" to
if (!data.profile.empty)
if (profile.empty)
profile.getSemesterStart(1).stringY_m_d
else
Date.getToday().stringY_m_d,
@ -54,7 +54,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus,
val teacherId = data.teacherList.singleOrNull { teacherName == it.fullName }?.id
?: -1
val topic = elements[2].text().trim()
val addedDate = Date.fromY_m_d(elements[4].text().trim()).inMillis
val addedDate = Date.fromY_m_d(elements[4].text().trim())
val eventDate = Date.fromY_m_d(elements[6].text().trim())
val id = "/podglad/([0-9]+)'".toRegex().find(
elements[9].select("input").attr("onclick")
@ -63,10 +63,25 @@ class LibrusSynergiaHomework(override val data: DataLibrus,
val lessons = data.db.timetableDao().getForDateNow(profileId, eventDate)
val startTime = lessons.firstOrNull { it.subjectId == subjectId }?.startTime
val moreInfo = graphElements[2 * i + 1].select("td[title]")
.attr("title").trim()
val description = "Treść: (.*)".toRegex(RegexOption.DOT_MATCHES_ALL).find(moreInfo)
?.get(1)?.replace("<br.*/>".toRegex(), "\n")?.trim()
/*val moreInfo = graphElements[2 * i + 1].select("td[title]")
.attr("title").trim()*/
var description = ""
graphElements.forEach { graphEl ->
graphEl.select("td[title]")?.also {
val title = it.attr("title")
val r = "Temat: (.*?)<br.?/>Data udostępnienia: (.*?)<br.?/>Termin wykonania: (.*?)<br.?/>Treść: (.*)"
.toRegex(RegexOption.DOT_MATCHES_ALL).find(title) ?: return@forEach
val gTopic = r[1].trim()
val gAddedDate = Date.fromY_m_d(r[2].trim())
val gEventDate = Date.fromY_m_d(r[3].trim())
if (gTopic == topic && gAddedDate == addedDate && gEventDate == eventDate) {
description = r[4].replace("<br.?/>".toRegex(), "\n").trim()
return@forEach
}
}
}
val seen = when (profile.empty) {
true -> true
@ -94,7 +109,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus,
id,
seen,
seen,
addedDate
addedDate.inMillis
))
}
}

View File

@ -54,6 +54,8 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
return@portalGet
}
val isParent = account.getString("group") == "parent"
val id = account.getInt("id") ?: continue
val login = account.getString("login") ?: continue
val token = account.getString("accessToken") ?: continue
@ -69,7 +71,7 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) {
data.portalEmail,
studentNameLong,
studentNameShort,
null
if (isParent) studentNameLong else null /* temporarily - there is no parent name provided, only the type */
).apply {
studentData["accountId"] = id
studentData["accountLogin"] = login

View File

@ -137,6 +137,7 @@ class LibrusLoginApi {
"librus_change_password_error" -> ERROR_LOGIN_LIBRUS_API_CHANGE_PASSWORD_ERROR
"librus_password_change_required" -> ERROR_LOGIN_LIBRUS_API_PASSWORD_CHANGE_REQUIRED
"invalid_grant" -> ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN
"invalid_request" -> ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST
else -> ERROR_LOGIN_LIBRUS_API_OTHER
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)

View File

@ -8,7 +8,6 @@ import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -64,21 +63,15 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
}
if (data.isMessagesLoginValid()) {
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(data.messagesSessionId!!)
.domain("wiadomosci.librus.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("wiadomosci.librus.pl", "DZIENNIKSID", data.messagesSessionId)
onSuccess()
}
else {
data.app.cookieJar.clearForDomain("wiadomosci.librus.pl")
data.app.cookieJar.clear("wiadomosci.librus.pl")
if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_SYNERGIA)) {
loginWithSynergia()
}
else if (data.apiLogin != null && data.apiPassword != null) {
else if (data.apiLogin != null && data.apiPassword != null && false) {
loginWithCredentials()
}
else {
@ -148,7 +141,7 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
}
private fun saveSessionId(response: Response?, text: String?) {
var sessionId = data.app.cookieJar.getCookie("wiadomosci.librus.pl", "DZIENNIKSID")
var sessionId = data.app.cookieJar.get("wiadomosci.librus.pl", "DZIENNIKSID")
sessionId = sessionId?.replace("-MAINT", "") // dunno what's this
sessionId = sessionId?.replace("MAINT", "") // dunno what's this
if (sessionId == null) {

View File

@ -37,19 +37,19 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
}
else if (data.portalRefreshToken != null) {
if (data.fakeLogin) {
data.app.cookieJar.clearForDomain("librus.szkolny.eu")
data.app.cookieJar.clear("librus.szkolny.eu")
}
else {
data.app.cookieJar.clearForDomain("portal.librus.pl")
data.app.cookieJar.clear("portal.librus.pl")
}
accessToken(null, data.portalRefreshToken)
}
else {
if (data.fakeLogin) {
data.app.cookieJar.clearForDomain("librus.szkolny.eu")
data.app.cookieJar.clear("librus.szkolny.eu")
}
else {
data.app.cookieJar.clearForDomain("portal.librus.pl")
data.app.cookieJar.clear("portal.librus.pl")
}
authorize(if (data.fakeLogin) FAKE_LIBRUS_AUTHORIZE else LIBRUS_AUTHORIZE_URL)
}

View File

@ -8,7 +8,6 @@ import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi
@ -30,17 +29,11 @@ class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Un
}
if (data.isSynergiaLoginValid()) {
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("DZIENNIKSID")
.value(data.synergiaSessionId!!)
.domain("synergia.librus.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("synergia.librus.pl", "DZIENNIKSID", data.synergiaSessionId)
onSuccess()
}
else {
data.app.cookieJar.clearForDomain("synergia.librus.pl")
data.app.cookieJar.clear("synergia.librus.pl")
if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_API)) {
loginWithApi()
}
@ -92,7 +85,7 @@ class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Un
}
if (location?.endsWith("centrum_powiadomien") == true) {
val sessionId = data.app.cookieJar.getCookie("synergia.librus.pl", "DZIENNIKSID")
val sessionId = data.app.cookieJar.get("synergia.librus.pl", "DZIENNIKSID")
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_SYNERGIA_NO_SESSION_ID)
.withResponse(response)
@ -117,7 +110,7 @@ class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Un
}
}
data.app.cookieJar.clearForDomain("synergia.librus.pl")
data.app.cookieJar.clear("synergia.librus.pl")
Request.builder()
.url(LIBRUS_SYNERGIA_TOKEN_LOGIN_URL.replace("TOKEN", token) + "/uczen/widok/centrum_powiadomien")
.userAgent(LIBRUS_USER_AGENT)

View File

@ -8,7 +8,6 @@ import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.FileCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.models.ApiError
@ -26,6 +25,23 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
val profile
get() = data.profile
/* TODO
add error handling:
<!-- CONTENT -->
<div id="content">
<a name="tresc"></a>
<h1>Ważna informacja!</h1>
<div class="okienko_informacyjne">
Korzystasz z hasła, które zostało wygenerowane automatycznie.<hr/>
Pamiętaj, aby je zmienić oraz uzupełnić dane w swoim profilu.<br/><br/>
Obie te operacje można wykonać uruchamiając opcję
<a href="https://poznan.mobidziennik.pl/dziennik/edytujprofil" title="Edycja profilu, możliwość zmiany hasła">Moje konto->Edycja profilu</a>.
</div> </div>
*/
fun webGet(
tag: String,
endpoint: String,
@ -65,6 +81,12 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
return
}
if (text.contains("<h2>Problemy z wydajnością</h2>")) {
data.error(ApiError(TAG, ERROR_MOBIDZIENNIK_WEB_SERVER_PROBLEM)
.withResponse(response))
return
}
try {
onSuccess(text)
} catch (e: Exception) {
@ -82,18 +104,8 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
}
}
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name(data.webSessionKey!!)
.value(data.webSessionValue!!)
.domain("${data.loginServerName}.mobidziennik.pl")
.secure().httpOnly().build(),
Cookie.Builder()
.name("SERVERID")
.value(data.webServerId!!)
.domain("${data.loginServerName}.mobidziennik.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("${data.loginServerName}.mobidziennik.pl", data.webSessionKey, data.webSessionValue)
data.app.cookieJar.set("${data.loginServerName}.mobidziennik.pl", "SERVERID", data.webServerId)
Request.builder()
.url(url)
@ -164,18 +176,8 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
}
}
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name(data.webSessionKey!!)
.value(data.webSessionValue!!)
.domain("${data.loginServerName}.mobidziennik.pl")
.secure().httpOnly().build(),
Cookie.Builder()
.name("SERVERID")
.value(data.webServerId!!)
.domain("${data.loginServerName}.mobidziennik.pl")
.secure().httpOnly().build()
))
data.app.cookieJar.set("${data.loginServerName}.mobidziennik.pl", data.webSessionKey, data.webSessionValue)
data.app.cookieJar.set("${data.loginServerName}.mobidziennik.pl", "SERVERID", data.webServerId)
Request.builder()
.url(url)

View File

@ -118,7 +118,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik,
// this needs to be at the end
message.apply {
this.body = body.html().replace("\n", "<br>")
this.body = body.html()
clearAttachments()
content.select("ul li").map { it.select("a").first() }.forEach {

View File

@ -32,15 +32,15 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik,
val doc = Jsoup.parse(text)
val listElement = doc.getElementsByClass("spis").first()
val listElement = doc.getElementsByClass("spis")?.first()
if (listElement == null) {
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY)
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL)
return@webGet
}
val list = listElement.getElementsByClass("podswietl")
for (item in list) {
val id = item.attr("rel").replace("[^\\d]".toRegex(), "").toLongOrNull() ?: continue
list?.forEach { item ->
val id = item.attr("rel").replace("[^\\d]".toRegex(), "").toLongOrNull() ?: return@forEach
val subjectEl = item.select("td:eq(0) div").first()
val subject = subjectEl.text()

View File

@ -36,9 +36,9 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik,
val doc = Jsoup.parse(text)
val list = doc.getElementsByClass("spis").first().getElementsByClass("podswietl")
for (item in list) {
val id = item.attr("rel").toLongOrNull() ?: continue
val list = doc.getElementsByClass("spis")?.first()?.getElementsByClass("podswietl")
list?.forEach { item ->
val id = item.attr("rel").toLongOrNull() ?: return@forEach
val subjectEl = item.select("td:eq(0)").first()
var hasAttachments = false

View File

@ -40,9 +40,9 @@ class MobidziennikWebMessagesSent(override val data: DataMobidziennik,
val doc = Jsoup.parse(text)
val list = doc.getElementsByClass("spis").first().getElementsByClass("podswietl")
for (item in list) {
val id = item.attr("rel").toLongOrNull() ?: continue
val list = doc.getElementsByClass("spis")?.first()?.getElementsByClass("podswietl")
list?.forEach { item ->
val id = item.attr("rel").toLongOrNull() ?: return@forEach
val subjectEl = item.select("td:eq(0)").first()
var hasAttachments = false

View File

@ -26,7 +26,7 @@ class MobidziennikLoginWeb(val data: DataMobidziennik, val onSuccess: () -> Unit
}
else {
if (data.loginServerName.isNotNullNorEmpty() && data.loginUsername.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) {
data.app.cookieJar.clearForDomain(data.loginServerName + ".mobidziennik.pl")
data.app.cookieJar.clear("${data.loginServerName}.mobidziennik.pl")
loginWithCredentials()
}
else {
@ -58,10 +58,10 @@ class MobidziennikLoginWeb(val data: DataMobidziennik, val onSuccess: () -> Unit
}
}
val cookies = data.app.cookieJar.getForDomain("${data.loginServerName}.mobidziennik.pl")
val cookie = cookies.singleOrNull { it.name().length > 32 }
val sessionKey = cookie?.name()
val sessionId = cookie?.value()
val cookies = data.app.cookieJar.getAll("${data.loginServerName}.mobidziennik.pl")
val cookie = cookies.entries.firstOrNull { it.key.length > 32 }
val sessionKey = cookie?.key
val sessionId = cookie?.value
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID)
.withResponse(response)
@ -71,7 +71,7 @@ class MobidziennikLoginWeb(val data: DataMobidziennik, val onSuccess: () -> Unit
data.webSessionKey = sessionKey
data.webSessionValue = sessionId
data.webServerId = data.app.cookieJar.getCookie("${data.loginServerName}.mobidziennik.pl", "SERVERID")
data.webServerId = data.app.cookieJar.get("${data.loginServerName}.mobidziennik.pl", "SERVERID")
data.webSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */
onSuccess()
}

View File

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.template
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_API
@ -28,13 +27,7 @@ class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
loginMethods.clear()
if (isWebLoginValid()) {
loginMethods += LOGIN_METHOD_TEMPLATE_WEB
app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("AuthCookie")
.value(webCookie!!)
.domain("eregister.example.com")
.secure().httpOnly().build()
))
app.cookieJar.set("eregister.example.com", "AuthCookie", webCookie)
}
if (isApiLoginValid())
loginMethods += LOGIN_METHOD_TEMPLATE_API

View File

@ -4,12 +4,11 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.template.login
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING
import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING
import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.currentTimeUnix
class TemplateLoginWeb(val data: DataTemplate, val onSuccess: () -> Unit) {
companion object {
@ -23,17 +22,11 @@ class TemplateLoginWeb(val data: DataTemplate, val onSuccess: () -> Unit) {
}
if (data.isWebLoginValid()) {
data.app.cookieJar.saveFromResponse(null, listOf(
Cookie.Builder()
.name("AuthCookie")
.value(data.webCookie!!)
.domain("eregister.example.com")
.secure().httpOnly().build()
))
data.app.cookieJar.set("eregister.example.com", "AuthCookie", data.webCookie)
onSuccess()
}
else {
data.app.cookieJar.clearForDomain("eregister.example.com")
data.app.cookieJar.clear("eregister.example.com")
if (/*data.webLogin != null && data.webPassword != null && */true) {
loginWithCredentials()
}

View File

@ -10,7 +10,10 @@ import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.values
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
@ -26,6 +29,25 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
}
}
init {
// during the first sync `profile.studentClassName` is already set
if (teamList.values().none { it.type == Team.TYPE_CLASS }) {
profile?.studentClassName?.also { name ->
val id = Utils.crc16(name.toByteArray()).toLong()
val teamObject = Team(
profileId,
id,
name,
Team.TYPE_CLASS,
"$schoolName:$name",
-1
)
teamList.put(id, teamObject)
}
}
}
override fun generateUserCode() = "$schoolName:$studentId"
/**
@ -188,11 +210,11 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"SZ9" -> "http://hack.szkolny.eu"
else -> null
}
return if (url != null) "$url/$symbol" else loginStore.getLoginData("apiUrl", null)
return if (url != null) "$url/$symbol/" else loginStore.getLoginData("apiUrl", null)
}
val fullApiUrl: String?
get() {
return "$apiUrl/$schoolSymbol"
return "$apiUrl$schoolSymbol/"
}
}

View File

@ -13,8 +13,6 @@ import io.github.wulkanowy.signer.android.signContent
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection
import java.util.*
@ -38,26 +36,10 @@ open class VulcanApi(open val data: DataVulcan, open val lastSync: Long?) {
baseUrl: Boolean = false,
onSuccess: (json: JsonObject, response: Response?) -> Unit
) {
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}/$endpoint"
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint"
d(tag, "Request: Vulcan/Api - $url")
if (data.teamList.size() == 0) {
data.profile?.studentClassName?.also { name ->
val id = Utils.crc16(name.toByteArray()).toLong()
val teamObject = Team(
profileId,
id,
name,
Team.TYPE_CLASS,
"${data.schoolName}:$name",
-1
)
data.teamList.put(id, teamObject)
}
}
val finalPayload = JsonObject()
parameters.map { (name, value) ->
when (value) {

View File

@ -86,31 +86,36 @@ class VulcanApiTimetable(override val data: DataVulcan,
data.teamList[id] = team
}
team.id
} ?: data.studentClassId.toLong()
} ?: data.teamClass?.id ?: -1
val subjectId = lesson.getLong("IdPrzedmiot")?.let {
when (it) {
0L -> {
val subjectName = lesson.getString("PrzedmiotNazwa") ?: ""
val subjectId = lesson.getLong("IdPrzedmiot").let { id ->
// get the specified subject name
val subjectName = lesson.getString("PrzedmiotNazwa") ?: ""
data.subjectList.singleOrNull { subject -> subject.longName == subjectName }?.id
?: {
/**
* CREATE A NEW SUBJECT IF IT DOESN'T EXIST
*/
val subjectObject = Subject(
profileId,
-1 * crc16(subjectName.toByteArray()).toLong(),
subjectName,
subjectName
)
data.subjectList.put(subjectObject.id, subjectObject)
subjectObject.id
}.invoke()
}
else -> it
val condition = when (id) {
// "special" subject - e.g. one time classes, a trip, etc.
0L -> { subject: Subject -> subject.longName == subjectName }
// normal subject, check if it exists
else -> { subject: Subject -> subject.id == id }
}
data.subjectList.singleOrNull(condition)?.id ?: {
/**
* CREATE A NEW SUBJECT IF IT DOESN'T EXIST
*/
val subjectObject = Subject(
profileId,
if (id == null || id == 0L)
-1 * crc16(subjectName.toByteArray()).toLong()
else
id,
subjectName,
subjectName
)
data.subjectList.put(subjectObject.id, subjectObject)
subjectObject.id
}()
}
val lessonObject = Lesson(profileId, -1).apply {

View File

@ -137,7 +137,7 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) {
}
Request.builder()
.url("${data.apiUrl}/$VULCAN_API_ENDPOINT_CERTIFICATE")
.url("${data.apiUrl}$VULCAN_API_ENDPOINT_CERTIFICATE")
.userAgent(VULCAN_API_USER_AGENT)
.addHeader("RequestMobileType", "RegisterDevice")
.addParameter("PIN", data.apiPin)

View File

@ -12,10 +12,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
@ -28,7 +25,6 @@ import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.md5
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.utils.models.Date
@ -80,7 +76,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
withContext(Dispatchers.Default) { block() }
}
catch (e: Exception) {
errorSnackbar.addError(ApiError.fromThrowable(TAG, e)).show()
errorSnackbar.addError(e.toApiError(TAG)).show()
null
}
}
@ -91,7 +87,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
catch (e: Exception) {
ErrorDetailsDialog(
activity,
listOf(ApiError.fromThrowable(TAG, e)),
listOf(e.toApiError(TAG)),
R.string.error_occured
)
null
@ -160,7 +156,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
}
@Throws(Exception::class)
fun getEvents(profiles: List<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>): List<EventFull> {
fun getEvents(profiles: List<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>, lastSyncTime: Long): List<EventFull> {
val teams = app.db.teamDao().allNow
val response = api.serverSync(ServerSyncRequest(
@ -185,19 +181,26 @@ class SzkolnyApi(val app: App) : CoroutineScope {
}
}
},
lastSync = lastSyncTime,
notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) }
)).execute()
parseResponse(response)
val (events, hasBrowsers) = parseResponse(response)
val events = mutableListOf<EventFull>()
hasBrowsers?.let {
app.config.sync.webPushEnabled = it
}
response.body()?.data?.events?.forEach { event ->
val eventList = mutableListOf<EventFull>()
events.forEach { event ->
// skip blacklisted events
if (event.id in blacklistedIds)
return@forEach
// create the event for every matching team and profile
teams.filter { it.code == event.teamCode }.onEach { team ->
val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach
events.add(EventFull(event).apply {
eventList += EventFull(event).apply {
profileId = team.profileId
teamId = team.id
addedManually = true
@ -205,11 +208,11 @@ class SzkolnyApi(val app: App) : CoroutineScope {
notified = profile.empty
if (profile.userCode == event.sharedBy) sharedBy = "self"
})
}
}
}
return events
return eventList
}
@Throws(Exception::class)
@ -253,9 +256,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
browserId = browserId,
pairToken = pairToken
)).execute()
parseResponse(response)
return response.body()?.data?.browsers ?: emptyList()
return parseResponse(response).browsers
}
@Throws(Exception::class)
@ -265,9 +267,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
device = getDevice(),
action = "listBrowsers"
)).execute()
parseResponse(response)
return response.body()?.data?.browsers ?: emptyList()
return parseResponse(response).browsers
}
@Throws(Exception::class)
@ -278,9 +279,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
action = "unpairBrowser",
browserId = browserId
)).execute()
parseResponse(response)
return response.body()?.data?.browsers ?: emptyList()
return parseResponse(response).browsers
}
@Throws(Exception::class)
@ -307,9 +307,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
@Throws(Exception::class)
fun getUpdate(channel: String): List<Update> {
val response = api.updates(channel).execute()
parseResponse(response)
return response.body()?.data ?: emptyList()
return parseResponse(response)
}
@Throws(Exception::class)
@ -321,8 +319,7 @@ class SzkolnyApi(val app: App) : CoroutineScope {
targetDeviceId = targetDeviceId,
text = text
)).execute()
val data = parseResponse(response)
return data.message
return parseResponse(response).message
}
}

View File

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

View File

@ -11,6 +11,8 @@ data class ServerSyncRequest(
val userCodes: List<String>,
val users: List<User>? = null,
val lastSync: Long,
val notifications: List<Notification>? = null
) {
data class User(

View File

@ -6,4 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.response
import pl.szczodrzynski.edziennik.data.db.full.EventFull
data class ServerSyncResponse(val events: List<EventFull>)
data class ServerSyncResponse(
val events: List<EventFull>,
val hasBrowsers: Boolean? = null
)

View File

@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.models.Date
class AppSync(val app: App, val notifications: MutableList<Notification>, val profiles: List<Profile>, val api: SzkolnyApi) {
companion object {
@ -24,26 +25,27 @@ class AppSync(val app: App, val notifications: MutableList<Notification>, val pr
*
* @return a number of events inserted to DB, possibly needing a notification
*/
fun run(): Int {
val profiles = profiles.filter { it.registration == Profile.REGISTRATION_ENABLED && !it.archived }
if (profiles.isNotEmpty()) {
val blacklistedIds = app.db.eventDao().blacklistedIds;
val events = api.getEvents(profiles, notifications, blacklistedIds)
fun run(lastSyncTime: Long, markAsSeen: Boolean = false): Int {
val blacklistedIds = app.db.eventDao().blacklistedIds
val events = api.getEvents(profiles, notifications, blacklistedIds, lastSyncTime)
if (events.isNotEmpty()) {
app.db.metadataDao().addAllIgnore(events.map { event ->
Metadata(
event.profileId,
Metadata.TYPE_EVENT,
event.id,
event.seen,
event.notified,
event.addedDate
)
})
return app.db.eventDao().addAll(events).size
}
app.config.sync.lastAppSync = System.currentTimeMillis()
if (events.isNotEmpty()) {
val today = Date.getToday()
app.db.metadataDao().addAllIgnore(events.map { event ->
val isPast = event.eventDate < today
Metadata(
event.profileId,
Metadata.TYPE_EVENT,
event.id,
isPast || markAsSeen || event.seen,
isPast || markAsSeen || event.notified,
event.addedDate
)
})
return app.db.eventDao().addAll(events).size
}
return 0;
}
}
}

View File

@ -10,6 +10,7 @@ import androidx.core.util.forEach
import androidx.core.util.set
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SERVER_MESSAGE
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.data.db.entity.Notification as AppNotification
class PostNotifications(val app: App, nList: List<AppNotification>) {
@ -17,22 +18,34 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
private const val TAG = "PostNotifications"
}
/*public boolean shouldBeQuiet() {
long now = Time.getNow().getInMillis();
long start = app.config.getSync().getQuietHoursStart();
long end = app.config.getSync().getQuietHoursEnd();
private val quiet by lazy { shouldBeQuiet() }
fun shouldBeQuiet(): Boolean {
if (!app.config.sync.quietHoursEnabled)
return false
val now = Time.getNow().value
val start = app.config.sync.quietHoursStart?.value ?: return false
var end = app.config.sync.quietHoursEnd?.value ?: return false
if (start > end) {
end += 1000 * 60 * 60 * 24;
//Log.d(TAG, "Night passing");
// the range spans between two days
end += 240000
}
if (start > now) {
now += 1000 * 60 * 60 * 24;
//Log.d(TAG, "Now is smaller");
}
//Log.d(TAG, "Start is "+start+", now is "+now+", end is "+end);
return start > 0 && now >= start && now <= end;
}*/
fun shouldBeQuiet() = false
return now in start..end || now+240000 in start..end
}
private fun NotificationCompat.Builder.addDefaults(): NotificationCompat.Builder {
return this.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(if (quiet) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MAX)
.also {
if (quiet) {
it.setSound(null)
it.setVibrate(longArrayOf())
}
else
it.setDefaults(NotificationCompat.DEFAULT_ALL)
}
.setGroup(if (quiet) app.notificationChannelsManager.dataQuiet.key else app.notificationChannelsManager.data.key)
}
private fun buildSummaryText(summaryCounts: SparseIntArray): CharSequence {
val summaryTexts = mutableListOf<String>()
@ -108,11 +121,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
it.addLine(line)
}
})
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notificationChannelsManager.data.key)
.addDefaults()
.setContentIntent(summaryIntent)
.setAutoCancel(true)
.build()
@ -131,11 +140,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.setStyle(NotificationCompat.BigTextStyle()
.bigText(it.text))
.setWhen(it.addedDate)
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notificationChannelsManager.data.key)
.addDefaults()
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(it.getPendingIntent(app))
.setAutoCancel(true)
@ -155,11 +160,7 @@ class PostNotifications(val app: App, nList: List<AppNotification>) {
.setContentText(buildSummaryText(summaryCounts))
.setTicker(newNotificationsText)
.setSmallIcon(R.drawable.ic_notification)
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notificationChannelsManager.data.key)
.addDefaults()
.setGroupSummary(true)
.setContentIntent(summaryIntent)
.setAutoCancel(true)

View File

@ -31,16 +31,23 @@ class SzkolnyTask(val app: App, val syncingProfiles: List<Profile>) : IApiTask(-
val notifications = Notifications(app, notificationList, profiles)
notifications.run()
val shouldAppSync = notificationList.isNotEmpty() || (System.currentTimeMillis() - app.config.lastAppSync > 24*HOUR*1000)
// do an AppSync every 24 hours, or if WebPush has a notification
val appSyncProfiles = profiles.filter { it.registration == Profile.REGISTRATION_ENABLED && !it.archived }
// App Sync conditions:
// - every 24 hours && any profile is registered
// - if there are new notifications && any browser is paired
val shouldAppSync =
System.currentTimeMillis() - app.config.sync.lastAppSync > 24*HOUR*1000
&& appSyncProfiles.isNotEmpty()
|| notificationList.isNotEmpty()
&& app.config.sync.webPushEnabled
if (shouldAppSync) {
// send notifications to web push, get shared events
val addedEvents = AppSync(app, notificationList, profiles, api).run()
val addedEvents = AppSync(app, notificationList, appSyncProfiles, api).run(app.config.sync.lastAppSync)
if (addedEvents > 0) {
// create notifications for shared events (not present before app sync)
notifications.sharedEventNotifications()
}
app.config.lastAppSync = System.currentTimeMillis()
}
d(TAG, "Created ${notificationList.count()} notifications.")

View File

@ -41,8 +41,9 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
Lesson::class,
ConfigEntry::class,
LibrusLesson::class,
TimetableManual::class,
Metadata::class
], version = 78)
], version = 79)
@TypeConverters(
ConverterTime::class,
ConverterDate::class,
@ -80,6 +81,7 @@ abstract class AppDb : RoomDatabase() {
abstract fun timetableDao(): TimetableDao
abstract fun configDao(): ConfigDao
abstract fun librusLessonDao(): LibrusLessonDao
abstract fun timetableManualDao(): TimetableManualDao
abstract fun metadataDao(): MetadataDao
companion object {
@ -161,7 +163,8 @@ abstract class AppDb : RoomDatabase() {
Migration75(),
Migration76(),
Migration77(),
Migration78()
Migration78(),
Migration79()
).allowMainThreadQueries().build()
}
}

View File

@ -8,7 +8,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date
class ConverterDateInt {
@TypeConverter
fun toDate(value: Int): Date = Date.fromValue(value)
fun toDate(value: Int): Date? = if (value == 0) null else Date.fromValue(value)
@TypeConverter
fun toInt(date: Date?): Int = date?.value ?: 0

View File

@ -41,7 +41,7 @@ interface ProfileDao {
fun getIdsByLoginStoreIdNow(loginStoreId: Int): List<Int>
@get:Query("SELECT * FROM profiles WHERE syncEnabled = 1 AND archived = 0 AND profileId >= 0 ORDER BY profileId")
val profilesForSyncNow: List<Profile>
val profilesForFirebaseNow: List<Profile>
@get:Query("SELECT profileId FROM profiles WHERE syncEnabled = 1 AND archived = 0 AND profileId >= 0 ORDER BY profileId")
val idsForSyncNow: List<Int>

View File

@ -5,10 +5,9 @@
package pl.szczodrzynski.edziennik.data.db.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
@ -53,6 +52,9 @@ interface TimetableDao {
@Query("DELETE FROM timetable WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))")
fun clearBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date)
@RawQuery(observedEntities = [Lesson::class])
fun getRaw(query: SupportSQLiteQuery): LiveData<List<LessonFull>>
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND type != -1 AND type != 0
@ -67,12 +69,11 @@ interface TimetableDao {
""")
fun getChangesForDateNow(profileId: Int, date: Date): List<LessonFull>
@Query("""
fun getForDate(profileId: Int, date: Date) = getRaw(SimpleSQLiteQuery("""
$QUERY
WHERE timetable.profileId = :profileId AND ((type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date))
WHERE timetable.profileId = $profileId AND ((type != 3 AND date = "${date.stringY_m_d}") OR ((type = 3 OR type = 1) AND oldDate = "${date.stringY_m_d}"))
ORDER BY id, type
""")
fun getForDate(profileId: Int, date: Date): LiveData<List<LessonFull>>
"""))
@Query("""
$QUERY

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-22.
*/
package pl.szczodrzynski.edziennik.data.db.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import pl.szczodrzynski.edziennik.data.db.entity.TimetableManual
import pl.szczodrzynski.edziennik.utils.models.Date
@Dao
interface TimetableManualDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(timetableManual: TimetableManual)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addAll(timetableManualList: List<TimetableManual>)
@Query("SELECT * FROM timetableManual WHERE profileId = :profileId")
fun getAll(profileId: Int): LiveData<List<TimetableManual>>
@Query("SELECT * FROM timetableManual WHERE profileId = :profileId AND date >= :dateFrom AND date <= :dateTo")
fun getAllByDateRange(profileId: Int, dateFrom: Date, dateTo: Date): LiveData<List<TimetableManual>>
@Query("SELECT * FROM timetableManual WHERE profileId = :profileId AND (date IS NULL OR date = 0 OR date >= :dateFrom)")
fun getAllToDisplay(profileId: Int, dateFrom: Date): LiveData<List<TimetableManual>>
@Delete
fun delete(timetableManual: TimetableManual)
@Query("DELETE FROM timetableManual WHERE profileId = :profileId")
fun clear(profileId: Int)
}

View File

@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
/*public Grade(int profileId, long id, String category, int color, String description, String name, float value, float weight, int semester, long teacherId, long subjectId) {
@ -81,5 +82,11 @@ open class Grade(
*/
@ColumnInfo(name = "gradeIsImprovement")
var isImprovement = false
@Ignore
var showAsUnseen = false
val isImproved
get() = parentId ?: -1L != -1L
}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.db.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -66,6 +67,9 @@ open class Lesson(val profileId: Int, var id: Long) {
fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF)
@Ignore
var showAsUnseen = false
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-22.
*/
package pl.szczodrzynski.edziennik.data.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@Entity(tableName = "timetableManual",
indices = [
Index(value = ["profileId", "date"]),
Index(value = ["profileId", "weekDay"])
])
class TimetableManual(
val profileId: Int,
var type: Int,
var repeatBy: Int,
@PrimaryKey(autoGenerate = true)
var id: Int = 0
) {
companion object {
const val TYPE_NORMAL = 0
const val TYPE_CANCELLED = 1
const val TYPE_CHANGE = 2
const val TYPE_SHIFTED_SOURCE = 3
const val TYPE_SHIFTED_TARGET = 4
const val TYPE_REMOVED = 5
const val TYPE_CLASSROOM = 6
const val REPEAT_WEEKLY = 0
const val REPEAT_ONCE = 1
const val REPEAT_BY_SUBJECT = 2
}
// `date` for one time lesson
@ColumnInfo(typeAffinity = ColumnInfo.INTEGER)
var date: Date? = null
// `weekDay` for repeating lesson (every week)
var weekDay: Int? = null
var lessonNumber: Int? = null
var startTime: Time? = null
var endTime: Time? = null
var subjectId: Long? = null
var teacherId: Long? = null
var teamId: Long? = null
var classroom: String? = null
fun verifyParams(): Boolean {
return when (repeatBy) {
REPEAT_WEEKLY -> date == null && weekDay != null
REPEAT_ONCE -> date != null && weekDay == null
REPEAT_BY_SUBJECT -> date == null && weekDay == null && subjectId != null
else -> false
}
}
}

View File

@ -0,0 +1,27 @@
package pl.szczodrzynski.edziennik.data.db.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration79 : Migration(78, 79) {
override fun migrate(database: SupportSQLiteDatabase) {
// manual timetable implementation
database.execSQL("""CREATE TABLE timetableManual (
profileId INTEGER NOT NULL,
id INTEGER PRIMARY KEY NOT NULL,
type INTEGER NOT NULL,
repeatBy INTEGER NOT NULL DEFAULT 0,
date INTEGER DEFAULT NULL,
weekDay INTEGER DEFAULT NULL,
lessonNumber INTEGER DEFAULT NULL,
startTime TEXT DEFAULT NULL,
endTime TEXT DEFAULT NULL,
subjectId INTEGER DEFAULT NULL,
teacherId INTEGER DEFAULT NULL,
teamId INTEGER DEFAULT NULL,
classroom TEXT DEFAULT NULL
)""")
database.execSQL("CREATE INDEX index_timetableManual_profileId_date ON timetableManual (profileId, date)")
database.execSQL("CREATE INDEX index_timetableManual_profileId_weekDay ON timetableManual (profileId, weekDay)")
}
}

View File

@ -9,8 +9,8 @@ import android.app.PendingIntent
import android.app.PendingIntent.CanceledException
import android.content.Intent
import android.util.Log
import com.google.firebase.iid.zzaq
import com.google.firebase.iid.zzv
import com.google.firebase.iid.zzad
import com.google.firebase.iid.zzaz
import com.google.firebase.messaging.zzc
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.*
@ -36,7 +36,7 @@ open class FirebaseService : zzc() {
// apparently this gets the correct intent from some
// kind of queue inside Firebase's InstanceID Receiver
final override fun zza(intent: Intent?) = zzaq.zza()?.zzb()
final override fun zza(intent: Intent?) = zzaz.zza()?.zzb()
final override fun zzb(intent: Intent?): Boolean {
val action = intent?.action
if (action == "com.google.firebase.messaging.NOTIFICATION_OPEN") {
@ -80,7 +80,7 @@ open class FirebaseService : zzc() {
val ackBundle = Bundle(
"google.message_id" to messageId
)
zzv.zza(this).zza(2, ackBundle)
zzad.zza(this).zza(2, ackBundle)
}
// check for duplicate message
// and add it to queue

View File

@ -36,7 +36,7 @@ class MyFirebaseService : FirebaseService(), CoroutineScope {
putString(System.currentTimeMillis().toString(), message.toString())
apply()
}
val profiles = app.db.profileDao().profilesForSyncNow
val profiles = app.db.profileDao().profilesForFirebaseNow
when (message.from) {
"640759989760" -> SzkolnyAppFirebase(app, profiles, message)
"747285019373" -> SzkolnyMobidziennikFirebase(app, profiles, message)

View File

@ -101,7 +101,9 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
val notificationList = mutableListOf<Notification>()
teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team ->
val profile = profiles.firstOrNull { it.id == team.profileId }
val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach
if (profile.registration != Profile.REGISTRATION_ENABLED)
return@forEach
val event = Event(
team.profileId,
json.getLong("id") ?: return,
@ -116,12 +118,9 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
team.id
)
// TODO? i guess - this comment is here for like a year
//val oldEvent: Event? = app.db.eventDao().getByIdNow(profile?.id ?: -1, event.id)
event.sharedBy = json.getString("sharedBy")
event.sharedByName = json.getString("sharedByName")
if (profile?.userCode == event.sharedBy) event.sharedBy = "self"
if (profile.userCode == event.sharedBy) event.sharedBy = "self"
val metadata = Metadata(
event.profileId,
@ -132,18 +131,6 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
json.getLong("addedDate") ?: System.currentTimeMillis()
)
//val eventType = eventTypes.firstOrNull { it.profileId == profile?.id && it.id == event.type }
/*val text = app.getString(
if (oldEvent == null)
R.string.notification_shared_event_format
else
R.string.notification_shared_event_modified_format,
event.sharedByName,
eventType?.name ?: "wydarzenie",
event.eventDate.formattedString,
event.topic
)*/
val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_SHARED_HOMEWORK else Notification.TYPE_NEW_SHARED_EVENT
val notificationFilter = app.config.getFor(event.profileId).sync.notificationFilter
@ -153,8 +140,8 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
title = app.getNotificationTitle(type),
text = message,
type = type,
profileId = profile?.id,
profileName = profile?.name,
profileId = profile.id,
profileName = profile.name,
viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA,
addedDate = metadata.addedDate
).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong())
@ -177,18 +164,20 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
val notificationList = mutableListOf<Notification>()
teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team ->
val profile = profiles.firstOrNull { it.id == team.profileId }
val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach
if (profile.registration != Profile.REGISTRATION_ENABLED)
return@forEach
val notificationFilter = app.config.getFor(team.profileId).sync.notificationFilter
if (!notificationFilter.contains(Notification.TYPE_REMOVED_SHARED_EVENT)) {
val notification = Notification(
id = Notification.buildId(profile?.id
id = Notification.buildId(profile.id
?: 0, Notification.TYPE_REMOVED_SHARED_EVENT, eventId),
title = app.getNotificationTitle(Notification.TYPE_REMOVED_SHARED_EVENT),
text = message,
type = Notification.TYPE_REMOVED_SHARED_EVENT,
profileId = profile?.id,
profileName = profile?.name,
profileId = profile.id,
profileName = profile.name,
viewId = MainActivity.DRAWER_ITEM_AGENDA
)
notificationList += notification

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-24.
*/
package pl.szczodrzynski.edziennik.network.cookie
import okhttp3.Cookie
class DumbCookie(var cookie: Cookie) {
constructor(domain: String, name: String, value: String, expiresAt: Long? = null) : this(
Cookie.Builder()
.name(name)
.value(value)
.also { if (expiresAt != null) it.expiresAt(expiresAt) }
.domain(domain)
.build()
)
init {
cookie = Cookie.Builder()
.name(cookie.name())
.value(cookie.value())
.expiresAt(cookie.expiresAt())
.domain(cookie.domain())
.build()
}
fun domainMatches(host: String): Boolean {
val domain = cookie.domain()
return host == domain || host.endsWith(".$domain")
}
override fun equals(other: Any?): Boolean {
if (other !is DumbCookie) return false
if (this.cookie === other.cookie) return true
return cookie.name() == other.cookie.name()
&& cookie.domain() == other.cookie.domain()
}
override fun hashCode(): Int {
var hash = 17
hash = 31 * hash + cookie.name().hashCode()
hash = 31 * hash + cookie.domain().hashCode()
return hash
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-24.
*/
package pl.szczodrzynski.edziennik.network.cookie
import android.content.Context
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
/**
* A simple cookie jar that does not care about the [Cookie.secure], [Cookie.hostOnly],
* [Cookie.httpOnly] and [Cookie.path] attributes.
*/
class DumbCookieJar(
/**
* A context to create the shared prefs file.
*/
context: Context,
/**
* Whether to persist session cookies as well, when [Cookie.persistent] is false.
*/
private val persistAll: Boolean = false
) : CookieJar {
private val prefs = context.getSharedPreferences("cookies", Context.MODE_PRIVATE)
private val sessionCookies = mutableSetOf<DumbCookie>()
private val savedCookies = mutableSetOf<DumbCookie>()
private fun save(dc: DumbCookie) {
sessionCookies.remove(dc)
sessionCookies.add(dc)
if (dc.cookie.persistent() || persistAll) {
savedCookies.remove(dc)
savedCookies.add(dc)
}
}
private fun delete(vararg toRemove: DumbCookie) {
sessionCookies.removeAll(toRemove)
savedCookies.removeAll(toRemove)
}
override fun saveFromResponse(url: HttpUrl?, cookies: List<Cookie>) {
for (cookie in cookies) {
val dc = DumbCookie(cookie)
save(dc)
}
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return sessionCookies.filter {
it.cookie.matches(url)
}.map { it.cookie }
}
fun get(domain: String, name: String): String? {
return sessionCookies.firstOrNull {
it.domainMatches(domain) && it.cookie.name() == name
}?.cookie?.value()
}
fun set(domain: String, name: String, value: String?, isSession: Boolean) = set(
domain, name, value,
if (isSession) null
else System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000L
)
/**
* Add a cookie to the cache.
* By default a session cookie is added. If [expiresAt] is set, the cookie is
* additionally persisted.
*/
fun set(domain: String, name: String?, value: String?, expiresAt: Long? = null) {
name ?: return
if (value == null) {
remove(domain, name)
return
}
val dc = DumbCookie(domain, name, value, expiresAt)
save(dc)
}
fun getAll(domain: String): Map<String, String> {
return sessionCookies.filter {
it.domainMatches(domain)
}.map { it.cookie.name() to it.cookie.value() }.toMap()
}
fun remove(domain: String, name: String) {
val toRemove = sessionCookies.filter {
it.domainMatches(domain) && it.cookie.name() == name
}
delete(*toRemove.toTypedArray())
}
fun clear(domain: String) {
val toRemove = sessionCookies.filter {
it.domainMatches(domain)
}
delete(*toRemove.toTypedArray())
}
}

View File

@ -40,7 +40,7 @@ class QrScannerDialog(
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
scannerView = ZXingScannerView(activity)
scannerView.setPadding(0, 16.dp, 0, 0)
scannerView.setPadding(0, 16.dp, 2.dp, 0)
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.qr_scanner_dialog_title)
.setView(scannerView)
@ -59,4 +59,4 @@ class QrScannerDialog(
}
scannerView.startCamera()
}}
}
}

View File

@ -90,7 +90,7 @@ class BellSyncDialog(
}
launch {
counterJob = startCoroutineTimer(repeatMillis = 1000) {
counterJob = startCoroutineTimer(repeatMillis = 500) {
val (bellDiff, multiplier) = actualBellDiff
val bellDiffText = (if (multiplier == -1) '-' else '+') + bellDiff.stringHMS
b.bellSyncHowto.text = app.getString(R.string.bell_sync_howto, bellTime.stringHM, bellDiffText)

View File

@ -19,6 +19,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.databinding.DialogEventDetailsBinding
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
@ -47,6 +48,8 @@ class EventDetailsDialog(
SzkolnyApi(app)
}
private var progressDialog: AlertDialog? = null
init { run {
if (activity.isFinishing)
return@run
@ -64,6 +67,7 @@ class EventDetailsDialog(
}
.setOnDismissListener {
onDismissListener?.invoke(TAG)
progressDialog?.dismiss()
}
.show()
@ -156,6 +160,23 @@ class EventDetailsDialog(
Toast.makeText(activity, R.string.hint_edit_event, Toast.LENGTH_SHORT).show()
true
}
b.topic.text = event.topic
BetterLink.attach(b.topic) {
dialog.dismiss()
}
}
private fun showRemovingProgressDialog() {
if (progressDialog != null) {
return
}
progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.event_removing_text)
.setCancelable(false)
.show()
}
private fun showRemoveEventDialog() {
@ -186,11 +207,14 @@ class EventDetailsDialog(
launch {
if (eventShared && eventOwn) {
// unshare + remove own event
Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show()
showRemovingProgressDialog()
api.runCatching(activity) {
unshareEvent(event)
} ?: return@launch
} ?: run {
progressDialog?.dismiss()
return@launch
}
finishRemoving()
} else if (eventShared && !eventOwn) {
@ -202,6 +226,7 @@ class EventDetailsDialog(
Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show()
finishRemoving()
}
progressDialog?.dismiss()
}
}

View File

@ -12,24 +12,33 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL
import androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationEnableDialog
import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS
import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
import kotlin.coroutines.CoroutineContext
class EventManualDialog(
@ -48,30 +57,34 @@ class EventManualDialog(
private const val TAG = "EventManualDialog"
}
private lateinit var job: Job
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val app by lazy { activity.application as App }
private lateinit var b: DialogEventManualV2Binding
private lateinit var dialog: AlertDialog
private var profile: Profile? = null
private lateinit var event: Event
private var customColor: Int? = null
private val editingShared = editingEvent?.sharedBy != null
private val editingOwn = editingEvent?.sharedBy == "self"
private var removeEventDialog: AlertDialog? = null
private var defaultLoaded = false
private val api by lazy {
SzkolnyApi(app)
}
private var enqueuedWeekDialog: AlertDialog? = null
private var enqueuedWeekStart = Date.getToday()
private var progressDialog: AlertDialog? = null
init { run {
if (activity.isFinishing)
return@run
job = Job()
onShowListener?.invoke(TAG)
EventBus.getDefault().register(this)
b = DialogEventManualV2Binding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_event_manual_title)
@ -85,6 +98,9 @@ class EventManualDialog(
}
.setOnDismissListener {
onDismissListener?.invoke(TAG)
EventBus.getDefault().unregister(this@EventManualDialog)
enqueuedWeekDialog?.dismiss()
progressDialog?.dismiss()
}
.setCancelable(false)
.create()
@ -104,12 +120,6 @@ class EventManualDialog(
show()
}
event = editingEvent?.clone() ?: Event().also { event ->
event.profileId = profileId
defaultType?.let {
event.type = it
}
}
b.shareSwitch.isChecked = editingShared
b.shareSwitch.isEnabled = !editingShared || (editingShared && editingOwn)
@ -144,41 +154,170 @@ class EventManualDialog(
else -> R.string.dialog_event_manual_share_first_notice
}
b.shareDetails.setText(text, event.sharedByName ?: "")
b.shareDetails.setText(text, editingEvent?.sharedByName ?: "")
}
private fun syncTimetable(date: Date) {
if (enqueuedWeekDialog != null) {
return
}
if (app.profile.getStudentData("timetableNotPublic", false)) {
return
}
val weekStart = date.weekStart
enqueuedWeekDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.timetable_syncing_text)
.setCancelable(false)
.show()
enqueuedWeekStart = weekStart
EdziennikTask.syncProfile(
profileId = profileId,
viewIds = listOf(
MainActivity.DRAWER_ITEM_TIMETABLE to 0
),
arguments = JsonObject(
"weekStart" to weekStart.stringY_m_d
)
).enqueue(activity)
}
private fun showSharingProgressDialog() {
if (progressDialog != null) {
return
}
progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.event_sharing_text)
.setCancelable(false)
.show()
}
private fun showRemovingProgressDialog() {
if (progressDialog != null) {
return
}
progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.event_removing_text)
.setCancelable(false)
.show()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
if (event.profileId == profileId) {
enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null
progressDialog?.dismiss()
launch {
b.timeDropdown.loadItems()
b.timeDropdown.selectDefault(editingEvent?.startTime)
b.timeDropdown.selectDefault(defaultLesson?.displayStartTime ?: defaultTime)
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) {
enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null
progressDialog?.dismiss()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null
progressDialog?.dismiss()
}
private fun loadLists() { launch {
profile = withContext(Dispatchers.Default) { app.db.profileDao().getByIdNow(profileId) }
with (b.dateDropdown) {
db = app.db
profileId = this@EventManualDialog.profileId
showWeekDays = false
showDays = true
showOtherDate = true
defaultLesson?.let {
nextLessonSubjectId = it.displaySubjectId
nextLessonSubjectName = it.displaySubjectName
nextLessonTeamId = it.displayTeamId
}
loadItems()
selectDefault(editingEvent?.eventDate)
selectDefault(defaultLesson?.displayDate ?: defaultDate)
onDateSelected = { date, lesson ->
b.timeDropdown.deselect()
b.timeDropdown.lessonsDate = date
this@EventManualDialog.launch {
if (!b.timeDropdown.loadItems())
syncTimetable(date)
lesson?.displayStartTime?.let { b.timeDropdown.selectTime(it) }
lesson?.displaySubjectId?.let { b.subjectDropdown.selectSubject(it) } ?: b.subjectDropdown.deselect()
lesson?.displayTeacherId?.let { b.teacherDropdown.selectTeacher(it) } ?: b.teacherDropdown.deselect()
lesson?.displayTeamId?.let { b.teamDropdown.selectTeam(it) } ?: b.teamDropdown.selectTeamClass()
}
}
}
with (b.timeDropdown) {
db = app.db
profileId = this@EventManualDialog.profileId
showAllDay = true
showCustomTime = true
lessonsDate = b.dateDropdown.getSelected() as? Date ?: Date.getToday()
displayMode = DISPLAY_LESSONS
if (!loadItems())
syncTimetable(lessonsDate ?: Date.getToday())
selectDefault(editingEvent?.startTime)
if (editingEvent != null && editingEvent.startTime == null)
select(0L)
selectDefault(defaultLesson?.displayStartTime ?: defaultTime)
onLessonSelected = { lesson ->
lesson.displaySubjectId?.let { b.subjectDropdown.selectSubject(it) } ?: b.subjectDropdown.deselect()
lesson.displayTeacherId?.let { b.teacherDropdown.selectTeacher(it) } ?: b.teacherDropdown.deselect()
lesson.displayTeamId?.let { b.teamDropdown.selectTeam(it) } ?: b.teamDropdown.selectTeamClass()
}
}
with (b.teamDropdown) {
db = app.db
profileId = this@EventManualDialog.profileId
showNoTeam = true
loadItems()
selectTeamClass()
selectDefault(editingEvent?.teamId)
selectDefault(defaultLesson?.displayTeamId)
}
with (b.subjectDropdown) {
db = app.db
profileId = this@EventManualDialog.profileId
showNoSubject = true
showCustomSubject = false
loadItems()
selectDefault(editingEvent?.subjectId)
selectDefault(defaultLesson?.displaySubjectId)
}
with (b.teacherDropdown) {
db = app.db
profileId = this@EventManualDialog.profileId
showNoTeacher = true
loadItems()
selectDefault(editingEvent?.teacherId)
selectDefault(defaultLesson?.displayTeacherId)
}
val deferred = async(Dispatchers.Default) {
// get the team list
val teams = app.db.teamDao().getAllNow(profileId)
b.teamDropdown.clear()
b.teamDropdown += TextInputDropDown.Item(
-1,
activity.getString(R.string.dialog_event_manual_no_team),
""
)
b.teamDropdown += teams.map { TextInputDropDown.Item(it.id, it.name, tag = it) }
// get the subject list
val subjects = app.db.subjectDao().getAllNow(profileId)
b.subjectDropdown.clear()
b.subjectDropdown += TextInputDropDown.Item(
-1,
activity.getString(R.string.dialog_event_manual_no_subject),
""
)
b.subjectDropdown += subjects.map { TextInputDropDown.Item(it.id, it.longName, tag = it) }
// get the teacher list
val teachers = app.db.teacherDao().getAllNow(profileId)
b.teacherDropdown.clear()
b.teacherDropdown += TextInputDropDown.Item(
-1,
activity.getString(R.string.dialog_event_manual_no_teacher),
""
)
b.teacherDropdown += teachers.map { TextInputDropDown.Item(it.id, it.fullName, tag = it) }
// get the event type list
val eventTypes = app.db.eventTypeDao().getAllNow(profileId)
b.typeDropdown.clear()
@ -186,13 +325,10 @@ class EventManualDialog(
}
deferred.await()
b.teamDropdown.isEnabled = true
b.subjectDropdown.isEnabled = true
b.teacherDropdown.isEnabled = true
b.typeDropdown.isEnabled = true
defaultType?.let {
b.typeDropdown.select(it.toLong())
b.typeDropdown.select(it)
}
b.typeDropdown.selected?.let { item ->
@ -201,9 +337,6 @@ class EventManualDialog(
// copy IDs from event being edited
editingEvent?.let {
b.teamDropdown.select(it.teamId)
b.subjectDropdown.select(it.subjectId)
b.teacherDropdown.select(it.teacherId)
b.topic.setText(it.topic)
b.typeDropdown.select(it.type.toLong())?.let { item ->
customColor = (item.tag as EventType).color
@ -215,8 +348,6 @@ class EventManualDialog(
// copy IDs from the LessonFull
defaultLesson?.let {
b.teamDropdown.select(it.displayTeamId)
b.subjectDropdown.select(it.displaySubjectId)
b.teacherDropdown.select(it.displayTeacherId)
}
b.typeDropdown.setOnChangeListener {
@ -230,7 +361,7 @@ class EventManualDialog(
}
b.typeColor.onClick {
val currentColor = (b.typeDropdown?.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT
val currentColor = (b.typeDropdown.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT
val colorPickerDialog = ColorPickerDialog.newBuilder()
.setColor(currentColor)
.create()
@ -244,278 +375,8 @@ class EventManualDialog(
})
colorPickerDialog.show(activity.fragmentManager, "color-picker-dialog")
}
loadDates()
}}
private fun loadDates() { launch {
val date = Date.getToday()
val today = date.value
var weekDay = date.weekDay
val deferred = async(Dispatchers.Default) {
val dates = mutableListOf<TextInputDropDown.Item>()
// item choosing the next lesson of specific subject
b.subjectDropdown.selected?.let {
if (it.tag is Subject) {
dates += TextInputDropDown.Item(
-it.id,
activity.getString(R.string.dialog_event_manual_date_next_lesson, it.tag.longName)
)
}
}
// TODAY
dates += TextInputDropDown.Item(
date.value.toLong(),
activity.getString(R.string.dialog_event_manual_date_today, date.formattedString),
tag = date.clone()
)
// TOMORROW
if (weekDay < 4) {
date.stepForward(0, 0, 1)
weekDay++
dates += TextInputDropDown.Item(
date.value.toLong(),
activity.getString(R.string.dialog_event_manual_date_tomorrow, date.formattedString),
tag = date.clone()
)
}
// REMAINING SCHOOL DAYS OF THE CURRENT WEEK
while (weekDay < 4) {
date.stepForward(0, 0, 1) // step one day forward
weekDay++
dates += TextInputDropDown.Item(
date.value.toLong(),
activity.getString(R.string.dialog_event_manual_date_this_week, Week.getFullDayName(weekDay), date.formattedString),
tag = date.clone()
)
}
// go to next week Monday
date.stepForward(0, 0, -weekDay + 7)
weekDay = 0
// ALL SCHOOL DAYS OF THE NEXT WEEK
while (weekDay < 4) {
dates += TextInputDropDown.Item(
date.value.toLong(),
activity.getString(R.string.dialog_event_manual_date_next_week, Week.getFullDayName(weekDay), date.formattedString),
tag = date.clone()
)
date.stepForward(0, 0, 1) // step one day forward
weekDay++
}
dates += TextInputDropDown.Item(
-1L,
activity.getString(R.string.dialog_event_manual_date_other)
)
dates
}
val dates = deferred.await()
b.dateDropdown.clear().append(dates)
defaultDate?.let {
event.eventDate = it
if (b.dateDropdown.select(it) == null)
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
editingEvent?.eventDate?.let {
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
defaultLesson?.displayDate?.let {
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
if (b.dateDropdown.selected == null) {
b.dateDropdown.select(today.toLong())
}
b.dateDropdown.isEnabled = true
b.dateDropdown.setOnChangeListener { item ->
when {
// next lesson with specified subject
item.id < -1 -> {
val teamId = defaultLesson?.teamId ?: -1
val selectedLessonDate = defaultLesson?.date ?: Date.getToday()
when (teamId) {
-1L -> app.db.timetableDao().getNextWithSubject(profileId, selectedLessonDate, -item.id)
else -> app.db.timetableDao().getNextWithSubjectAndTeam(profileId, selectedLessonDate, -item.id, teamId)
}.observeOnce(activity, Observer {
val lessonDate = it?.displayDate ?: return@Observer
b.dateDropdown.select(TextInputDropDown.Item(
lessonDate.value.toLong(),
lessonDate.formattedString,
tag = lessonDate
))
b.teamDropdown.select(it.displayTeamId)
b.subjectDropdown.select(it.displaySubjectId)
b.teacherDropdown.select(it.displayTeacherId)
defaultLoaded = false
loadHours(it.displayStartTime)
})
return@setOnChangeListener false
}
// custom date
item.id == -1L -> {
MaterialDatePicker.Builder
.datePicker()
.setSelection((b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) }
?: Date.getToday()).inMillis)
.build()
.apply {
addOnPositiveButtonClickListener {
val dateSelected = Date.fromMillis(it)
b.dateDropdown.select(TextInputDropDown.Item(
dateSelected.value.toLong(),
dateSelected.formattedString,
tag = dateSelected
))
loadHours()
}
show(this@EventManualDialog.activity.supportFragmentManager, "MaterialDatePicker")
}
return@setOnChangeListener false
}
// a specific date
else -> {
b.dateDropdown.select(item)
loadHours()
}
}
return@setOnChangeListener true
}
loadHours()
}}
private fun loadHours(defaultHour: Time? = null) {
b.timeDropdown.isEnabled = false
// get the selected date
val date = b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) } ?: return
// get all lessons for selected date
app.db.timetableDao().getForDate(profileId, date).observeOnce(activity, Observer { lessons ->
val hours = mutableListOf<TextInputDropDown.Item>()
// add All day time choice
hours += TextInputDropDown.Item(
0L,
activity.getString(R.string.dialog_event_manual_all_day)
)
lessons.forEach { lesson ->
if (lesson.type == Lesson.TYPE_NO_LESSONS) {
// indicate there are no lessons this day
hours += TextInputDropDown.Item(
-2L,
activity.getString(R.string.dialog_event_manual_no_lessons)
)
return@forEach
}
// create the lesson caption
val text = listOfNotEmpty(
lesson.displayStartTime?.stringHM ?: "",
lesson.displaySubjectName?.let {
when {
lesson.type == Lesson.TYPE_CANCELLED
|| lesson.type == Lesson.TYPE_SHIFTED_SOURCE -> it.asStrikethroughSpannable()
lesson.type != Lesson.TYPE_NORMAL -> it.asItalicSpannable()
else -> it
}
} ?: ""
)
// add an item with LessonFull as the tag
hours += TextInputDropDown.Item(
lesson.displayStartTime?.value?.toLong() ?: -1,
text.concat(" "),
tag = lesson
)
}
b.timeDropdown.clear().append(hours)
if (defaultLoaded) {
b.timeDropdown.deselect()
// select the TEAM_CLASS if possible
b.teamDropdown.items.singleOrNull {
it.tag is Team && it.tag.type == Team.TYPE_CLASS
}?.let {
b.teamDropdown.select(it)
} ?: b.teamDropdown.deselect()
// clear subject, teacher selection
b.subjectDropdown.deselect()
b.teacherDropdown.deselect()
}
else {
val setTime: (Time) -> Unit = {
event.startTime = it
if (b.timeDropdown.select(it) == null)
b.timeDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.stringHM,
tag = it
))
}
defaultTime?.let(setTime)
editingEvent?.startTime?.let(setTime)
defaultLesson?.displayStartTime?.let(setTime)
defaultHour?.let(setTime)
}
defaultLoaded = true
b.timeDropdown.isEnabled = true
// attach a listener to time dropdown
b.timeDropdown.setOnChangeListener { item ->
when (item.id) {
// no lessons this day
-2L -> {
b.timeDropdown.deselect()
return@setOnChangeListener false
}
// custom start hour
-1L -> return@setOnChangeListener false
// selected a specific lesson
else -> {
if (item.tag is LessonFull) {
// update team, subject, teacher dropdowns,
// using the LessonFull from item tag
b.teamDropdown.deselect()
b.subjectDropdown.deselect()
b.teacherDropdown.deselect()
item.tag.displayTeamId?.let {
b.teamDropdown.select(it)
}
item.tag.displaySubjectId?.let {
b.subjectDropdown.select(it)
}
item.tag.displayTeacherId?.let {
b.teacherDropdown.select(it)
}
}
}
}
return@setOnChangeListener true
}
})
}
private fun showRemoveEventDialog() {
val shareNotice = when {
editingShared && editingOwn -> "\n\n"+activity.getString(R.string.dialog_event_manual_remove_shared_self)
@ -541,37 +402,67 @@ class EventManualDialog(
}
private fun saveEvent() {
val date = b.dateDropdown.selected?.tag.instanceOfOrNull<Date>()
val startTime = b.timeDropdown.selected?.tag.instanceOfOrNull<Time>()
val teamId = b.teamDropdown.selected?.id
val date = b.dateDropdown.getSelected() as? Date
val timeSelected = b.timeDropdown.getSelected()
val teamId = b.teamDropdown.getSelected() as? Long
val type = b.typeDropdown.selected?.id
val topic = b.topic.text?.toString()
val subjectId = b.subjectDropdown.selected?.id
val teacherId = b.teacherDropdown.selected?.id
val subjectId = b.subjectDropdown.getSelected() as? Long
val teacherId = b.teacherDropdown.getSelected()
val share = b.shareSwitch.isChecked
if (share && profile?.registration != Profile.REGISTRATION_ENABLED) {
RegistrationEnableDialog(activity, profileId).showEventShareDialog {
if (it != null)
profile = it
saveEvent()
}
return
}
b.dateDropdown.error = null
b.teamDropdown.error = null
b.typeDropdown.error = null
b.topic.error = null
var isError = false
if (date == null) {
b.dateDropdown.error = app.getString(R.string.dialog_event_manual_date_choose)
b.dateDropdown.requestFocus()
isError = true
}
if (timeSelected !is Pair<*, *> && timeSelected != 0L) {
b.timeDropdown.error = app.getString(R.string.dialog_event_manual_time_choose)
if (!isError) b.timeDropdown.parent.requestChildFocus(b.timeDropdown, b.timeDropdown)
isError = true
}
if (share && teamId == null) {
b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose)
if (!isError) b.teamDropdown.parent.requestChildFocus(b.teamDropdown, b.teamDropdown)
isError = true
}
if (type == null) {
b.typeDropdown.error = app.getString(R.string.dialog_event_manual_type_choose)
if (!isError) b.typeDropdown.requestFocus()
isError = true
}
if (topic.isNullOrBlank()) {
b.topic.error = app.getString(R.string.dialog_event_manual_topic_choose)
if (!isError) b.topic.requestFocus()
isError = true
}
val startTime = if (timeSelected == 0L)
null
else
(timeSelected as? Pair<*, *>)?.first as? Time
if (isError) return
val id = System.currentTimeMillis()
@ -606,7 +497,7 @@ class EventManualDialog(
val profile = app.db.profileDao().getByIdNow(profileId)
if (!share && !editingShared) {
Toast.makeText(activity, R.string.event_manual_saving, Toast.LENGTH_SHORT).show()
//Toast.makeText(activity, R.string.event_manual_saving, Toast.LENGTH_SHORT).show()
finishAdding(eventObject, metadataObject)
}
else if (editingShared && !editingOwn) {
@ -614,7 +505,7 @@ class EventManualDialog(
// TODO
}
else if (!share && editingShared) {
Toast.makeText(activity, R.string.event_manual_unshare, Toast.LENGTH_SHORT).show()
showSharingProgressDialog()
eventObject.apply {
sharedBy = null
@ -623,13 +514,16 @@ class EventManualDialog(
api.runCatching(activity) {
unshareEvent(eventObject)
} ?: return@launch
} ?: run {
progressDialog?.dismiss()
return@launch
}
eventObject.sharedByName = null
finishAdding(eventObject, metadataObject)
}
else if (share) {
Toast.makeText(activity, R.string.event_manual_share, Toast.LENGTH_SHORT).show()
showSharingProgressDialog()
eventObject.apply {
sharedBy = profile?.userCode
@ -640,7 +534,10 @@ class EventManualDialog(
api.runCatching(activity) {
shareEvent(eventObject.withMetadata(metadataObject))
} ?: return@launch
} ?: run {
progressDialog?.dismiss()
return@launch
}
eventObject.sharedBy = "self"
finishAdding(eventObject, metadataObject)
@ -648,6 +545,7 @@ class EventManualDialog(
else {
Toast.makeText(activity, "Unknown action :(", Toast.LENGTH_SHORT).show()
}
progressDialog?.dismiss()
}
}
@ -655,11 +553,14 @@ class EventManualDialog(
launch {
if (editingShared && editingOwn) {
// unshare + remove own event
Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show()
showRemovingProgressDialog()
api.runCatching(activity) {
unshareEvent(editingEvent!!)
} ?: return@launch
} ?: run {
progressDialog?.dismiss()
return@launch
}
finishRemoving()
} else if (editingShared && !editingOwn) {
@ -668,9 +569,10 @@ class EventManualDialog(
// TODO
} else {
// remove event
Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show()
//Toast.makeText(activity, R.string.event_manual_remove, Toast.LENGTH_SHORT).show()
finishRemoving()
}
progressDialog?.dismiss()
}
}

View File

@ -3,6 +3,7 @@ package pl.szczodrzynski.edziennik.ui.dialogs.grade
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
@ -10,7 +11,9 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext
@ -50,16 +53,22 @@ class GradeDetailsDialog(
.show()
val manager = app.gradesManager
val gradeColor = manager.getColor(grade)
val gradeColor = manager.getGradeColor(grade)
b.grade = grade
b.weightText = manager.getWeightString(app, grade)
b.commentVisible = false
b.devMode = App.debugMode
b.gradeName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.3) -0x1000000 else -0x1)
b.gradeName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt())
b.gradeName.background.setTintColor(gradeColor)
b.gradeValue = if (grade.weight == 0f || grade.value < 0f) -1f else manager.getGradeValue(grade)
b.customValueDivider.isVisible = manager.plusValue != null || manager.minusValue != null
b.customValueLayout.isVisible = b.customValueDivider.isVisible
b.customValueButton.onClick {
GradesConfigDialog(activity, reloadOnDismiss = true)
}
launch {
val historyList = withContext(Dispatchers.Default) {
app.db.gradeDao().getAllWithParentIdNow(App.profileId, grade.id)

View File

@ -20,6 +20,7 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_SEM
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
import java.util.*
class GradesConfigDialog(
val activity: AppCompatActivity,
@ -88,12 +89,34 @@ class GradesConfigDialog(
else -> null
}?.isChecked = true
b.dontCountZeroToAverage.isChecked = !profileConfig.countZeroToAvg
b.dontCountGrades.isChecked = profileConfig.dontCountEnabled && profileConfig.dontCountGrades.isNotEmpty()
b.hideImproved.isChecked = profileConfig.hideImproved
b.averageWithoutWeight.isChecked = profileConfig.averageWithoutWeight
if (profileConfig.dontCountGrades.isEmpty()) {
b.dontCountGradesText.setText("nb, 0, bz, bd")
}
else {
b.dontCountGradesText.setText(profileConfig.dontCountGrades.join(", "))
}
}
private fun saveConfig() {
profileConfig.plusValue = if (b.customPlusCheckBox.isChecked) b.customPlusValue.progress else null
profileConfig.minusValue = if (b.customMinusCheckBox.isChecked) b.customMinusValue.progress else null
b.dontCountGradesText.setText(
b.dontCountGradesText
.text
?.toString()
?.toLowerCase(Locale.getDefault())
?.replace(", ", ",")
)
profileConfig.dontCountEnabled = b.dontCountGrades.isChecked
profileConfig.dontCountGrades = b.dontCountGradesText.text
?.split(",")
?.map { it.trim() }
?: listOf()
}
private fun initView() {
@ -125,6 +148,15 @@ class GradesConfigDialog(
b.gradeAverageMode2.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_AVG_2_SEM }
b.gradeAverageMode3.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_SEM_2_SEM }
b.dontCountZeroToAverage.setOnCheckedChangeListener { _, isChecked -> profileConfig.countZeroToAvg = !isChecked }
b.hideImproved.onChange { _, isChecked -> profileConfig.hideImproved = isChecked }
b.averageWithoutWeight.onChange { _, isChecked -> profileConfig.averageWithoutWeight = isChecked }
b.averageWithoutWeightHelp.onClick {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.grades_config_average_without_weight)
.setMessage(R.string.grades_config_average_without_weight_message)
.setPositiveButton(R.string.ok, null)
.show()
}
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-15.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import android.text.Html
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.task.AppSync
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import kotlin.coroutines.CoroutineContext
class RegistrationEnableDialog(
val activity: AppCompatActivity,
val profileId: Int
) : CoroutineScope {
companion object {
private const val TAG = "RegistrationEnableDialog"
}
private lateinit var app: App
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
private var progressDialog: AlertDialog? = null
init { run {
if (activity.isFinishing)
return@run
app = activity.applicationContext as App
}}
fun showEventShareDialog(onSuccess: (profile: Profile?) -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.event_manual_need_registration_title)
.setMessage(R.string.event_manual_need_registration_text)
.setPositiveButton(R.string.ok) { dialog, which ->
enableRegistration(onSuccess)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
fun showEnableDialog(onSuccess: (profile: Profile?) -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.registration_enable_dialog_title)
.setMessage(Html.fromHtml(app.getString(R.string.registration_enable_dialog_text)))
.setPositiveButton(R.string.ok) { dialog, which ->
enableRegistration(onSuccess)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun enableRegistration(onSuccess: (profile: Profile?) -> Unit) { launch {
progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.registration_enable_progress_text)
.setCancelable(false)
.show()
val profile = withContext(Dispatchers.Default) {
val profile = app.db.profileDao().getByIdNow(profileId) ?: return@withContext null
profile.registration = Profile.REGISTRATION_ENABLED
// force full registration of the user
App.config.getFor(profile.id).hash = ""
AppSync(app, mutableListOf(), listOf(profile), SzkolnyApi(app)).run(0L, markAsSeen = true)
app.db.profileDao().add(profile)
if (profile.id == App.profileId) {
App.profile.registration = profile.registration
}
return@withContext profile
}
progressDialog?.dismiss()
onSuccess(profile)
}}
}

View File

@ -18,10 +18,17 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.FileProvider
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent
import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogGenerateBlockTimetableBinding
@ -53,7 +60,7 @@ class GenerateBlockTimetableDialog(
private val app by lazy { activity.application as App }
private lateinit var job: Job
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@ -64,11 +71,15 @@ class GenerateBlockTimetableDialog(
private var showTeachersNames: Boolean = true
private var noColors: Boolean = false
private var enqueuedWeekDialog: AlertDialog? = null
private var enqueuedWeekStart = Date.getToday()
private var enqueuedWeekEnd = Date.getToday()
init { run {
if (activity.isFinishing)
return@run
job = Job()
onShowListener?.invoke(TAG)
EventBus.getDefault().register(this)
val weekCurrentStart = Week.getWeekStart()
val weekCurrentEnd = Week.getWeekEnd()
@ -88,39 +99,55 @@ class GenerateBlockTimetableDialog(
.setTitle(R.string.timetable_generate_range)
.setView(b.root)
.setNeutralButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
.setPositiveButton(R.string.save) { dialog, _ ->
dialog.dismiss()
when (b.weekSelectionRadioGroup.checkedRadioButtonId) {
R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd)
R.id.forSelectedWeekRadio -> selectDate()
}
.setPositiveButton(R.string.save, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
EventBus.getDefault().unregister(this@GenerateBlockTimetableDialog)
}
.setOnDismissListener { onDismissListener?.invoke(TAG) }
.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick {
when (b.weekSelectionRadioGroup.checkedRadioButtonId) {
R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd)
R.id.forSelectedWeekRadio -> selectDate()
}
}
}}
private fun selectDate() {
MaterialDatePicker.Builder
.datePicker()
.setSelection(Date.getToday().inMillis)
.build()
val date = Date.getToday()
DatePickerDialog
.newInstance({ _, year, monthOfYear, dayOfMonth ->
val dateSelected = Date(year, monthOfYear, dayOfMonth)
generateBlockTimetable(dateSelected.weekStart, dateSelected.weekEnd)
}, date.year, date.month, date.day)
.apply {
addOnPositiveButtonClickListener { dateInMillis ->
dismiss()
val selectedDate = Date.fromMillis(dateInMillis)
generateBlockTimetable(selectedDate.weekStart, selectedDate.weekEnd)
}
accentColor = R.attr.colorPrimary.resolveAttr(this@GenerateBlockTimetableDialog.activity)
show(this@GenerateBlockTimetableDialog.activity.supportFragmentManager, "DatePickerDialog")
}
.show(activity.supportFragmentManager, "MaterialDatePicker")
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
if (event.profileId == App.profileId) {
enqueuedWeekDialog?.dismiss()
generateBlockTimetable(enqueuedWeekStart, enqueuedWeekEnd)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) {
enqueuedWeekDialog?.dismiss()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
dialog.dismiss()
enqueuedWeekDialog?.dismiss()
}
private fun generateBlockTimetable(weekStart: Date, weekEnd: Date) { launch {
val progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.timetable_generate_progress_title)
.setMessage(R.string.timetable_generate_progress_text)
.show()
val weekDays = mutableListOf<MutableList<Lesson>>()
for (i in weekStart.weekDay..weekEnd.weekDay) {
weekDays.add(mutableListOf())
@ -157,174 +184,212 @@ class GenerateBlockTimetableDialog(
return@mapNotNull lesson
}
if (lessons.isEmpty()) {
if (enqueuedWeekDialog != null) {
return@launch
}
enqueuedWeekDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.timetable_syncing_text)
.setCancelable(false)
.show()
enqueuedWeekStart = weekStart
enqueuedWeekEnd = weekEnd
EdziennikTask.syncProfile(
profileId = App.profileId,
viewIds = listOf(
MainActivity.DRAWER_ITEM_TIMETABLE to 0
),
arguments = JsonObject(
"weekStart" to weekStart.stringY_m_d
)
).enqueue(activity)
return@launch
}
val progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.timetable_generate_progress_title)
.setMessage(R.string.timetable_generate_progress_text)
.show()
if (minTime == null) {
progressDialog.dismiss()
// TODO: Toast
return@launch
}
val diff = Time.diff(maxTime, minTime)
dialog.dismiss()
val imageWidth = WIDTH_CONSTANT + maxWeekDay * (WIDTH_WEEKDAY + WIDTH_SPACING) - WIDTH_SPACING
val imageHeight = heightProfileName + HEIGHT_CONSTANT + diff.inMinutes * HEIGHT_MINUTE + HEIGHT_FOOTER
val bitmap = Bitmap.createBitmap(imageWidth + 20, imageHeight + 30, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val uri = withContext(Dispatchers.Default) {
if (noColors) canvas.drawARGB(255, 255, 255, 255)
else canvas.drawARGB(255, 225, 225, 225)
val diff = Time.diff(maxTime, minTime)
val paint = Paint().apply {
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
val imageWidth = WIDTH_CONSTANT + maxWeekDay * (WIDTH_WEEKDAY + WIDTH_SPACING) - WIDTH_SPACING
val imageHeight = heightProfileName + HEIGHT_CONSTANT + diff.inMinutes * HEIGHT_MINUTE + HEIGHT_FOOTER
val bitmap = Bitmap.createBitmap(imageWidth + 20, imageHeight + 30, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
lessons.forEach { lesson ->
val lessonLength = Time.diff(lesson.displayEndTime, lesson.displayStartTime)
val firstOffset = Time.diff(lesson.displayStartTime, minTime)
val lessonWeekDay = lesson.displayDate!!.weekDay
if (noColors) canvas.drawARGB(255, 255, 255, 255)
else canvas.drawARGB(255, 225, 225, 225)
val left = WIDTH_CONSTANT + lessonWeekDay * (WIDTH_WEEKDAY + WIDTH_SPACING)
val top = heightProfileName + HEIGHT_CONSTANT + firstOffset.inMinutes * HEIGHT_MINUTE
val blockWidth = WIDTH_WEEKDAY
val blockHeight = lessonLength.inMinutes * HEIGHT_MINUTE
val viewWidth = 380.dp
val viewHeight = lessonLength.inMinutes * 4.dp
val layout = activity.layoutInflater.inflate(R.layout.row_timetable_block_item, null) as LinearLayout
val item: LinearLayout = layout.findViewById(R.id.timetableItemLayout)
val card: CardView = layout.findViewById(R.id.timetableItemCard)
val subjectName: TextView = layout.findViewById(R.id.timetableItemSubjectName)
val classroomName: TextView = layout.findViewById(R.id.timetableItemClassroomName)
val teacherName: TextView = layout.findViewById(R.id.timetableItemTeacherName)
val teamName: TextView = layout.findViewById(R.id.timetableItemTeamName)
if (noColors) {
card.setCardBackgroundColor(Color.WHITE)
card.cardElevation = 0f
item.setBackgroundResource(R.drawable.bg_rounded_16dp_outline)
subjectName.setTextColor(Color.BLACK)
classroomName.setTextColor(0xffaaaaaa.toInt())
teacherName.setTextColor(0xffaaaaaa.toInt())
teamName.setTextColor(0xffaaaaaa.toInt())
val paint = Paint().apply {
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
subjectName.text = lesson.displaySubjectName ?: ""
classroomName.text = lesson.displayClassroom ?: ""
teacherName.text = lesson.displayTeacherName ?: ""
teamName.text = lesson.displayTeamName ?: ""
lessons.forEach { lesson ->
val lessonLength = Time.diff(lesson.displayEndTime, lesson.displayStartTime)
val firstOffset = Time.diff(lesson.displayStartTime, minTime)
val lessonWeekDay = lesson.displayDate!!.weekDay
if (!showTeachersNames) teacherName.visibility = View.GONE
val left = WIDTH_CONSTANT + lessonWeekDay * (WIDTH_WEEKDAY + WIDTH_SPACING)
val top = heightProfileName + HEIGHT_CONSTANT + firstOffset.inMinutes * HEIGHT_MINUTE
when (lesson.type) {
Lesson.TYPE_NORMAL -> {}
Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> {
card.setCardBackgroundColor(Color.BLACK)
subjectName.setTextColor(Color.WHITE)
subjectName.text = lesson.displaySubjectName?.asStrikethroughSpannable() ?: ""
val blockWidth = WIDTH_WEEKDAY
val blockHeight = lessonLength.inMinutes * HEIGHT_MINUTE
val viewWidth = 380.dp
val viewHeight = lessonLength.inMinutes * 4.dp
val layout = activity.layoutInflater.inflate(R.layout.row_timetable_block_item, null) as LinearLayout
val item: LinearLayout = layout.findViewById(R.id.timetableItemLayout)
val card: CardView = layout.findViewById(R.id.timetableItemCard)
val subjectName: TextView = layout.findViewById(R.id.timetableItemSubjectName)
val classroomName: TextView = layout.findViewById(R.id.timetableItemClassroomName)
val teacherName: TextView = layout.findViewById(R.id.timetableItemTeacherName)
val teamName: TextView = layout.findViewById(R.id.timetableItemTeamName)
if (noColors) {
card.setCardBackgroundColor(Color.WHITE)
card.cardElevation = 0f
item.setBackgroundResource(R.drawable.bg_rounded_16dp_outline)
subjectName.setTextColor(Color.BLACK)
classroomName.setTextColor(0xffaaaaaa.toInt())
teacherName.setTextColor(0xffaaaaaa.toInt())
teamName.setTextColor(0xffaaaaaa.toInt())
}
else -> {
card.setCardBackgroundColor(0xff234158.toInt())
subjectName.setTextColor(Color.WHITE)
subjectName.setTypeface(null, Typeface.BOLD_ITALIC)
subjectName.text = lesson.displaySubjectName ?: ""
classroomName.text = lesson.displayClassroom ?: ""
teacherName.text = lesson.displayTeacherName ?: ""
teamName.text = lesson.displayTeamName ?: ""
if (!showTeachersNames) teacherName.visibility = View.GONE
when (lesson.type) {
Lesson.TYPE_NORMAL -> {
}
Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> {
card.setCardBackgroundColor(Color.BLACK)
subjectName.setTextColor(Color.WHITE)
subjectName.text = lesson.displaySubjectName?.asStrikethroughSpannable()
?: ""
}
else -> {
card.setCardBackgroundColor(0xff234158.toInt())
subjectName.setTextColor(Color.WHITE)
subjectName.setTypeface(null, Typeface.BOLD_ITALIC)
}
}
layout.isDrawingCacheEnabled = true
layout.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY))
layout.layout(0, 0, layout.measuredWidth, layout.measuredHeight)
layout.buildDrawingCache(true)
val itemBitmap = layout.drawingCache
canvas.drawBitmap(itemBitmap, null, Rect(left, top, left + blockWidth, top + blockHeight), paint)
}
val textPaint = Paint().apply {
setARGB(255, 0, 0, 0)
textAlign = Paint.Align.CENTER
textSize = 30f
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
for (w in 0..maxWeekDay) {
val x = WIDTH_CONSTANT + w * WIDTH_WEEKDAY + w * WIDTH_SPACING
canvas.drawText(Week.getFullDayName(w), x + (WIDTH_WEEKDAY / 2f), heightProfileName + HEIGHT_CONSTANT / 2 + 10f, textPaint)
}
if (showProfileName) {
textPaint.textSize = 50f
canvas.drawText("${app.profile.name} - plan lekcji, ${weekStart.formattedStringShort} - ${weekEnd.formattedStringShort}", (imageWidth + 20) / 2f, 80f, textPaint)
}
textPaint.apply {
setARGB(128, 0, 0, 0)
textAlign = Paint.Align.RIGHT
textSize = 26f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
}
val footerTextPaintCenter = ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
canvas.drawText("Wygenerowano w aplikacji Szkolny.eu", imageWidth - 10f, imageHeight - footerTextPaintCenter - 10f, textPaint)
textPaint.apply {
setARGB(255, 127, 127, 127)
textAlign = Paint.Align.CENTER
textSize = 16f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
}
val textPaintCenter = ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
val linePaint = Paint().apply {
setARGB(255, 100, 100, 100)
style = Paint.Style.STROKE
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
val minTimeInt = ((minTime!!.value / 10000) * 60) + ((minTime!!.value / 100) % 100)
lessonRanges.forEach { (startTime, endTime) ->
listOf(startTime, endTime).forEach { value ->
val hour = value / 10000
val minute = (value / 100) % 100
val time = Time(hour, minute, 0)
val firstOffset = time.inMinutes - minTimeInt // offset in minutes
val top = (heightProfileName + HEIGHT_CONSTANT + firstOffset * HEIGHT_MINUTE).toFloat()
canvas.drawText(time.stringHM, WIDTH_CONSTANT / 2f, top - textPaintCenter, textPaint)
canvas.drawLine(WIDTH_CONSTANT.toFloat(), top, imageWidth.toFloat(), top, linePaint)
}
}
layout.isDrawingCacheEnabled = true
layout.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY))
layout.layout(0, 0, layout.measuredWidth, layout.measuredHeight)
layout.buildDrawingCache(true)
val today = Date.getToday().stringY_m_d
val now = Time.getNow().stringH_M_S
val itemBitmap = layout.drawingCache
canvas.drawBitmap(itemBitmap, null, Rect(left, top, left + blockWidth, top + blockHeight), paint)
}
val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() }
val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png")
val textPaint = Paint().apply {
setARGB(255, 0, 0, 0)
textAlign = Paint.Align.CENTER
textSize = 30f
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
for (w in 0..maxWeekDay) {
val x = WIDTH_CONSTANT + w * WIDTH_WEEKDAY + w * WIDTH_SPACING
canvas.drawText(Week.getFullDayName(w), x + (WIDTH_WEEKDAY / 2f), heightProfileName + HEIGHT_CONSTANT / 2 + 10f, textPaint)
}
if (showProfileName) {
textPaint.textSize = 50f
canvas.drawText("${app.profile.name} - plan lekcji, ${weekStart.formattedStringShort} - ${weekEnd.formattedStringShort}", (imageWidth + 20) / 2f, 80f, textPaint)
}
textPaint.apply {
setARGB(128, 0, 0, 0)
textAlign = Paint.Align.RIGHT
textSize = 26f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
}
val footerTextPaintCenter = ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
canvas.drawText("Wygenerowano w aplikacji Szkolny.eu", imageWidth - 10f, imageHeight - footerTextPaintCenter - 10f, textPaint)
textPaint.apply {
setARGB(255, 127, 127, 127)
textAlign = Paint.Align.CENTER
textSize = 16f
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
}
val textPaintCenter = ((textPaint.descent() + textPaint.ascent()) / 2).roundToInt()
val linePaint = Paint().apply {
setARGB(255, 100, 100, 100)
style = Paint.Style.STROKE
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
isAntiAlias = true
isFilterBitmap = true
isDither = true
}
val minTimeInt = ((minTime!!.value / 10000) * 60) + ((minTime!!.value / 100) % 100)
lessonRanges.forEach { (startTime, endTime) ->
listOf(startTime, endTime).forEach { value ->
val hour = value / 10000
val minute = (value / 100) % 100
val time = Time(hour, minute, 0)
val firstOffset = time.inMinutes - minTimeInt // offset in minutes
val top = (heightProfileName + HEIGHT_CONSTANT + firstOffset * HEIGHT_MINUTE).toFloat()
canvas.drawText(time.stringHM, WIDTH_CONSTANT / 2f, top - textPaintCenter, textPaint)
canvas.drawLine(WIDTH_CONSTANT.toFloat(), top, imageWidth.toFloat(), top, linePaint)
try {
val fos = FileOutputStream(outputFile)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.close()
} catch (e: Exception) {
Log.e("SAVE_IMAGE", e.message, e)
return@withContext null
}
}
val today = Date.getToday().stringY_m_d
val now = Time.getNow().stringH_M_S
val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() }
val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png")
try {
val fos = FileOutputStream(outputFile)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.close()
} catch (e: Exception) {
Log.e("SAVE_IMAGE", e.message, e)
return@launch
}
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile)
} else {
Uri.parse("file://" + outputFile.absolutePath)
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile)
} else {
Uri.parse("file://" + outputFile.absolutePath)
}
uri
}
progressDialog.dismiss()

View File

@ -25,7 +25,6 @@ import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableUtils
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
@ -50,7 +49,7 @@ class LessonDetailsDialog(
get() = job + Dispatchers.Main
private lateinit var adapter: EventListAdapter
private val utils by lazy { TimetableUtils() }
private val manager by lazy { app.timetableManager }
init { run {
if (activity.isFinishing)
@ -91,7 +90,7 @@ class LessonDetailsDialog(
val lessonTime = lesson.displayStartTime ?: return
b.lessonDate.text = Week.getFullDayName(lessonDate.weekDay) + ", " + lessonDate.formattedString
b.annotationVisible = utils.getAnnotation(activity, lesson, b.annotation)
b.annotationVisible = manager.getAnnotation(activity, lesson, b.annotation)
if (lesson.type >= Lesson.TYPE_SHIFTED_SOURCE) {
b.shiftedLayout.visibility = View.VISIBLE

View File

@ -126,6 +126,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
}
private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch {
if (!isAdded)
return@launch
delay(500)
val eventList = mutableListOf<CalendarEvent>()
@ -139,6 +141,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
/**
* LESSON CHANGES
*/
if (!isAdded)
return@launch
val lessons = withContext(Dispatchers.Default) { app.db.timetableDao().getAllChangesNow(app.profileId) }
val lessonChangeCounters = mutableListOf<LessonChangeCounter>()
@ -170,6 +174,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
/**
* TEACHER ABSENCES
*/
if (!isAdded)
return@launch
val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true)
@ -208,6 +214,8 @@ class AgendaFragment : Fragment(), CoroutineScope {
/**
* EVENTS
*/
if (!isAdded)
return@launch
val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) }
val unreadEventDates = mutableSetOf<Int>()

View File

@ -33,6 +33,7 @@ import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static pl.szczodrzynski.edziennik.data.db.entity.LoginStore.LOGIN_TYPE_LIBRUS;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT;
@ -90,6 +91,18 @@ public class AnnouncementsFragment extends Fragment {
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addItemDecoration(new SimpleDividerItemDecoration(view.getContext()));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.setEnabled(false);
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.setEnabled(true);
}
}
});
app.db.announcementDao().getAll(App.Companion.getProfileId()).observe(this, announcements -> {
if (app == null || activity == null || b == null || !isAdded())
return;

View File

@ -21,6 +21,7 @@ import androidx.core.graphics.ColorUtils;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial;
@ -40,6 +41,7 @@ import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_ABSENT;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_ABSENT_EXCUSED;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_BELATED;
@ -181,6 +183,18 @@ public class AttendanceFragment extends Fragment {
b.attendanceView.setLayoutManager(linearLayoutManager);
b.attendanceView.addItemDecoration(new SimpleDividerItemDecoration(getContext()));
b.attendanceView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.setEnabled(false);
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.setEnabled(true);
}
}
});
App.db.attendanceDao().getAll(App.Companion.getProfileId()).observe(this, attendance -> {
if (app == null || activity == null || b == null || !isAdded())
return;

View File

@ -40,6 +40,7 @@ class MainSnackbar(val activity: AppCompatActivity) {
setAction(actionText) {
onClick?.invoke()
}
duration = 7000
show()
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.base
import androidx.fragment.app.Fragment
abstract class PagerFragment : Fragment() {
private var isPageCreated = false
/**
* Called when the page is first shown, or if previous
* [onPageCreated] returned false
*
* @return true if the view is set up
* @return false if the setup failed. The method may be then called
* again, when page becomes visible.
*/
abstract fun onPageCreated(): Boolean
override fun onResume() {
if (!isPageCreated) {
isPageCreated = onPageCreated()
}
super.onResume()
}
}

View File

@ -15,6 +15,7 @@ import androidx.appcompat.widget.PopupMenu;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial;
@ -31,6 +32,7 @@ import pl.szczodrzynski.edziennik.databinding.FragmentBehaviourBinding;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_NOTICE;
public class BehaviourFragment extends Fragment {
@ -97,6 +99,18 @@ public class BehaviourFragment extends Fragment {
b.noticesView.setHasFixedSize(true);
b.noticesView.setLayoutManager(linearLayoutManager);
b.noticesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.setEnabled(false);
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.setEnabled(true);
}
}
});
app.db.noticeDao().getAll(App.Companion.getProfileId()).observe(this, notices -> {
if (app == null || activity == null || b == null || !isAdded())
return;

View File

@ -48,6 +48,7 @@ class ErrorSnackbar(val activity: AppCompatActivity) : CoroutineScope {
fun addError(apiError: ApiError): ErrorSnackbar {
errors.add(apiError)
snackbar?.setText(apiError.getStringReason(activity))
snackbar?.duration = 15000
return this
}

View File

@ -1,54 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.feedback;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.databinding.ActivityFeedbackBinding;
import pl.szczodrzynski.edziennik.utils.Themes;
public class FeedbackActivity extends AppCompatActivity {
private static final String TAG = "FeedbackActivity";
private App app;
private ActivityFeedbackBinding b;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(Themes.INSTANCE.getAppTheme());
b = DataBindingUtil.inflate(getLayoutInflater(), R.layout.activity_feedback, null, false);
setContentView(b.getRoot());
app = (App) getApplication();
setSupportActionBar(b.toolbar);
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) // Press Back Icon
{
finish();
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
}

View File

@ -0,0 +1,25 @@
package pl.szczodrzynski.edziennik.ui.modules.feedback
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.Themes.appTheme
class FeedbackActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(appTheme)
setContentView(R.layout.activity_feedback)
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.feedbackFragment, FeedbackFragment())
transaction.commitAllowingStateLoss()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home)
finish()
return super.onOptionsItemSelected(item)
}
}

View File

@ -12,6 +12,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import coil.Coil
import coil.api.load
@ -22,15 +23,26 @@ import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.crc16
import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.databinding.FragmentFeedbackBinding
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.openUrl
import java.util.*
import kotlin.collections.List
import kotlin.collections.any
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.collections.forEach
import kotlin.collections.forEachIndexed
import kotlin.collections.isNotEmpty
import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.coroutines.CoroutineContext
class FeedbackFragment : Fragment(), CoroutineScope {
@ -39,7 +51,7 @@ class FeedbackFragment : Fragment(), CoroutineScope {
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var activity: AppCompatActivity
private lateinit var b: FragmentFeedbackBinding
private val job: Job = Job()
@ -54,11 +66,10 @@ class FeedbackFragment : Fragment(), CoroutineScope {
private var receiver: BroadcastReceiver? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
activity = (getActivity() as AppCompatActivity?) ?: return null
if (context == null)
return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
// activity, context and profile is valid
b = FragmentFeedbackBinding.inflate(inflater)
// prevent doubled received messages on enter
@ -239,7 +250,7 @@ class FeedbackFragment : Fragment(), CoroutineScope {
}
launch {
val message = api.runCatching(activity.errorSnackbar) {
val message = api.runCatching(activity) {
val message = api.sendFeedbackMessage(
senderName = App.profile.accountName ?: App.profile.studentNameLong,
targetDeviceId = if (isDev) currentDeviceId else null,

View File

@ -6,7 +6,6 @@ package pl.szczodrzynski.edziennik.ui.modules.grades
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.text.TextUtils
import android.util.AttributeSet
import android.util.TypedValue.COMPLEX_UNIT_SP
@ -48,7 +47,7 @@ class GradeView : AppCompatTextView {
val gradeName = grade.name
val gradeColor = manager.getColor(grade)
val gradeColor = manager.getGradeColor(grade)
text = if (periodGradesTextual)
when (grade.type) {
@ -78,12 +77,12 @@ class GradeView : AppCompatTextView {
TYPE_SEMESTER2_PROPOSED,
TYPE_YEAR_PROPOSED -> android.R.attr.textColorPrimary.resolveAttr(context)
else -> if (ColorUtils.calculateLuminance(gradeColor) > 0.3)
0x99000000.toInt()
0xaa000000.toInt()
else
0x99ffffff.toInt()
0xccffffff.toInt()
})
typeface = Typeface.create("serif-monospace", Typeface.BOLD)
//typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
setBackgroundResource(when (grade.type) {
TYPE_SEMESTER1_PROPOSED,
TYPE_SEMESTER2_PROPOSED,
@ -104,7 +103,7 @@ class GradeView : AppCompatTextView {
setPadding(2.dp, 2.dp, 2.dp, 2.dp)
}
else {
setTextSize(COMPLEX_UNIT_SP, 16f)
setTextSize(COMPLEX_UNIT_SP, 14f)
setPadding(5.dp, 0, 5.dp, 0)
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
setMargins(0, 0, 5.dp, 0)

View File

@ -10,19 +10,24 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.modules.grades.models.*
import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.*
import kotlin.coroutines.CoroutineContext
class GradesAdapter(
val activity: AppCompatActivity,
var onGradeClick: ((item: GradeFull) -> Unit)? = null,
var onGradesEditorClick: ((subject: GradesSubject, semester: GradesSemester) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CoroutineScope {
companion object {
private const val TAG = "GradesAdapter"
private const val ITEM_TYPE_SUBJECT = 0
@ -34,6 +39,13 @@ class GradesAdapter(
const val STATE_OPENED = 1
}
private val app = activity.applicationContext as App
private val manager = app.gradesManager
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var items = mutableListOf<Any>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -67,56 +79,63 @@ class GradesAdapter(
}
if (model !is ExpandableItemModel<*>)
return@OnClickListener
expandModel(model, view)
}
fun expandModel(model: ExpandableItemModel<*>?, view: View?, notifyAdapter: Boolean = true) {
model ?: return
val position = items.indexOf(model)
if (position == -1)
return@OnClickListener
//val position = it.getTag(R.string.tag_key_position) as? Int ?: return@OnClickListener
return
if (model is GradesSubject || model is GradesSemester) {
view.findViewById<View>(R.id.dropdownIcon)?.let { dropdownIcon ->
view?.findViewById<View>(R.id.dropdownIcon)?.let { dropdownIcon ->
ObjectAnimator.ofFloat(
dropdownIcon,
View.ROTATION,
if (model.state == STATE_CLOSED) 0f else 180f,
if (model.state == STATE_CLOSED) 180f else 0f
dropdownIcon,
View.ROTATION,
if (model.state == STATE_CLOSED) 0f else 180f,
if (model.state == STATE_CLOSED) 180f else 0f
).setDuration(200).start();
}
}
if (model is GradesSubject) {
val preview = view.findViewById<View>(R.id.previewContainer)
val summary = view.findViewById<View>(R.id.yearSummary)
val preview = view?.findViewById<View>(R.id.previewContainer)
val summary = view?.findViewById<View>(R.id.yearSummary)
preview?.visibility = if (model.state == STATE_CLOSED) View.INVISIBLE else View.VISIBLE
summary?.visibility = if (model.state == STATE_CLOSED) View.VISIBLE else View.INVISIBLE
}
if (model.state == STATE_CLOSED) {
val subItems = if (model is GradesSemester && model.grades.isEmpty())
listOf(GradesEmpty())
else
model.items
val subItems = when {
model is GradesSemester && model.grades.isEmpty() ->
listOf(GradesEmpty())
model is GradesSemester && manager.hideImproved ->
model.items.filter { !it.seen || !it.isImproved }
else -> model.items
}
model.state = STATE_OPENED
items.addAll(position + 1, subItems.filterNotNull())
notifyItemRangeInserted(position + 1, subItems.size)
/*notifyItemRangeChanged(
position + subItems.size,
items.size - (position + subItems.size)
)*/
//notifyItemRangeChanged(position, items.size - position)
if (notifyAdapter) notifyItemRangeInserted(position + 1, subItems.size)
if (model is GradesSubject) {
// auto expand first semester
if (model.semesters.isNotEmpty()) {
val semester = model.semesters.firstOrNull { it.grades.isNotEmpty() } ?: model.semesters.first()
val semesterIndex = model.semesters.indexOf(semester)
val grades = if (semester.grades.isEmpty())
listOf(GradesEmpty())
else
semester.grades
val grades = when {
semester.grades.isEmpty() ->
listOf(GradesEmpty())
manager.hideImproved ->
semester.grades.filter { !it.seen || !it.isImproved }
else -> semester.grades
}
semester.state = STATE_OPENED
items.addAll(position + 2 + semesterIndex, grades)
notifyItemRangeInserted(position + 2 + semesterIndex, grades.size)
if (notifyAdapter) notifyItemRangeInserted(position + 2 + semesterIndex, grades.size)
}
}
}
@ -138,9 +157,7 @@ class GradesAdapter(
if (end != -1) {
items.subList(start, end).clear()
notifyItemRangeRemoved(start, end - start)
//notifyItemRangeChanged(start, end - start)
//notifyItemRangeChanged(position, items.size - position)
if (notifyAdapter) notifyItemRangeRemoved(start, end - start)
}
model.state = STATE_CLOSED
@ -152,8 +169,6 @@ class GradesAdapter(
if (holder !is BindableViewHolder<*>)
return
val app = activity.applicationContext as App
val viewType = when (holder) {
is SubjectViewHolder -> ITEM_TYPE_SUBJECT
is SemesterViewHolder -> ITEM_TYPE_SEMESTER
@ -167,11 +182,11 @@ class GradesAdapter(
holder.itemView.setTag(R.string.tag_key_model, item)
when {
holder is SubjectViewHolder && item is GradesSubject -> holder.onBind(activity, app, item, position)
holder is SemesterViewHolder && item is GradesSemester -> holder.onBind(activity, app, item, position)
holder is EmptyViewHolder && item is GradesEmpty -> holder.onBind(activity, app, item, position)
holder is GradeViewHolder && item is GradeFull -> holder.onBind(activity, app, item, position)
holder is StatsViewHolder && item is GradesStats -> holder.onBind(activity, app, item, position)
holder is SubjectViewHolder && item is GradesSubject -> holder.onBind(activity, app, item, position, this)
holder is SemesterViewHolder && item is GradesSemester -> holder.onBind(activity, app, item, position, this)
holder is EmptyViewHolder && item is GradesEmpty -> holder.onBind(activity, app, item, position, this)
holder is GradeViewHolder && item is GradeFull -> holder.onBind(activity, app, item, position, this)
holder is StatsViewHolder && item is GradesStats -> holder.onBind(activity, app, item, position, this)
}
if (holder is SemesterViewHolder && item is GradesSemester) {
@ -184,5 +199,23 @@ class GradesAdapter(
holder.itemView.setOnClickListener(onClickListener)
}
fun notifyItemChanged(model: Any) {
startCoroutineTimer(1000L, 0L) {
val index = items.indexOf(model)
if (index != -1)
notifyItemChanged(index)
}
}
fun removeItem(model: Any) {
startCoroutineTimer(2000L, 0L) {
val index = items.indexOf(model)
if (index != -1) {
items.removeAt(index)
notifyItemRemoved(index)
}
}
}
override fun getItemCount() = items.size
}

View File

@ -4,31 +4,39 @@
package pl.szczodrzynski.edziennik.ui.modules.grades
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.Bundle
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_GRADES_EDITOR
import pl.szczodrzynski.edziennik.averageOrNull
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.GradesFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradeDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSemester
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesStats
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext
import kotlin.math.max
class GradesFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "GradesFragment"
@ -47,7 +55,9 @@ class GradesFragment : Fragment(), CoroutineScope {
GradesAdapter(activity)
}
private val manager by lazy { app.gradesManager }
private val dontCountEnabled by lazy { manager.dontCountEnabled }
private val dontCountGrades by lazy { manager.dontCountGrades }
private var expandSubjectId = 0L
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
@ -62,6 +72,8 @@ class GradesFragment : Fragment(), CoroutineScope {
if (!isAdded)
return
expandSubjectId = arguments?.getLong("gradesSubjectId") ?: 0L
app.db.gradeDao()
.getAllOrderBy(App.profileId, app.gradesManager.getOrderByString())
.observe(this, Observer { grades ->
@ -71,6 +83,16 @@ class GradesFragment : Fragment(), CoroutineScope {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
//addItemDecoration(SimpleDividerItemDecoration(context))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.isEnabled = false
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.isEnabled = true
}
}
})
}
}
@ -111,8 +133,29 @@ class GradesFragment : Fragment(), CoroutineScope {
"finalOtherSemester" to otherSemester?.finalGrade?.value
))
}
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_grades_config)
.withIcon(Icon2.cmd_settings_outline)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
GradesConfigDialog(activity, true, null, null)
}),
BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
AsyncTask.execute { App.db.metadataDao().setAllSeen(App.profileId, TYPE_GRADE, true) }
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
})
)
activity.gainAttention()
}
@Suppress("SuspendFunctionOnCoroutineScope")
private suspend fun processGrades(grades: List<GradeFull>) {
val items = mutableListOf<GradesSubject>()
@ -121,6 +164,8 @@ class GradesFragment : Fragment(), CoroutineScope {
var subject = GradesSubject(subjectId, "")
var semester = GradesSemester(0, 1)
val hideImproved = manager.hideImproved
// grades returned by the query are ordered
// by the subject ID, so it's easier and probably
// a bit faster to build all the models
@ -144,6 +189,11 @@ class GradesFragment : Fragment(), CoroutineScope {
?: GradesSemester(subject.subjectId, grade.semester).also { subject.semesters += it }
}
grade.showAsUnseen = !grade.seen
if (!grade.seen) {
semester.hasUnseen = true
}
when (grade.type) {
Grade.TYPE_SEMESTER1_PROPOSED,
Grade.TYPE_SEMESTER2_PROPOSED -> semester.proposedGrade = grade
@ -237,18 +287,41 @@ class GradesFragment : Fragment(), CoroutineScope {
adapter.items = items.toMutableList()
adapter.items.add(stats)
var expandSubjectModel: GradesSubject? = null
if (expandSubjectId != 0L) {
expandSubjectModel = items.firstOrNull { it.subjectId == expandSubjectId }
adapter.expandModel(
model = expandSubjectModel,
view = null,
notifyAdapter = false
)
}
withContext(Dispatchers.Main) {
adapter.notifyDataSetChanged()
}
startCoroutineTimer(500L, 0L) {
if (expandSubjectModel != null) {
b.gradesRecyclerView.smoothScrollToPosition(
items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0)
)
}
}
}
private fun countGrade(grade: Grade, averages: GradesAverages) {
val value = manager.getGradeValue(grade)
val weight = manager.getGradeWeight(dontCountGrades, grade)
val weight = manager.getGradeWeight(dontCountEnabled, dontCountGrades, grade)
when (grade.type) {
Grade.TYPE_NORMAL -> {
averages.normalSum += value
averages.normalCount ++
if (grade.value > 0f) {
// count to the arithmetic average
// only if value more than 0
// to exclude "+", "-", "np" etc.
averages.normalSum += value
averages.normalCount++
}
averages.normalWeightedSum += value * weight
averages.normalWeightedCount += weight
}

View File

@ -3,7 +3,6 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.editor
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -49,8 +48,7 @@ class GradesEditorAdapter(
holder.gradesListName.text = editorGrade.name
holder.gradesListName.isSelected = true
holder.gradesListName.setTypeface(null, Typeface.BOLD)
holder.gradesListName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.25) -0x1000000 else -0x1)
holder.gradesListName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.25) 0xaa000000.toInt() else 0xccffffff.toInt())
holder.gradesListName.background.colorFilter = PorterDuffColorFilter(gradeColor, PorterDuff.Mode.MULTIPLY)
holder.gradesListCategory.text = editorGrade.category
if (editorGrade.weight < 0) {

View File

@ -16,7 +16,6 @@ import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.databinding.FragmentGradesEditorBinding
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG
@ -60,16 +59,13 @@ class GradesEditorFragment : Fragment() {
if (context == null)
return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false)
// activity, context and profile is valid
b = FragmentGradesEditorBinding.inflate(inflater)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (app.profile == null || !isAdded)
if (!isAdded)
return
subjectId = arguments.getLong("subjectId", -1)
@ -110,7 +106,7 @@ class GradesEditorFragment : Fragment() {
continue
}
var weight = editorGrade.weight
if (!config.countZeroToAvg && editorGrade.name == "0") {
if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.toLowerCase().trim())) {
weight = 0f
}
val value = editorGrade.value * weight
@ -175,7 +171,7 @@ class GradesEditorFragment : Fragment() {
averageSemester = 0f
for (editorGrade in editorGrades) {
var weight = editorGrade.weight
if (!config.countZeroToAvg && editorGrade.name == "0") {
if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.toLowerCase().trim())) {
weight = 0f
}
val value = editorGrade.value * weight
@ -218,7 +214,7 @@ class GradesEditorFragment : Fragment() {
continue
}
var weight = grade.weight
if (!config.countZeroToAvg && grade.name == "0") {
if (config.dontCountEnabled && config.dontCountGrades.contains(grade.name.toLowerCase().trim())) {
weight = 0f
}
val value = grade.value * weight

View File

@ -4,16 +4,17 @@
package pl.szczodrzynski.edziennik.ui.modules.grades.models
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
data class GradesSemester(
val subjectId: Long,
val number: Int,
val grades: MutableList<Grade> = mutableListOf()
) : ExpandableItemModel<Grade>(grades) {
val grades: MutableList<GradeFull> = mutableListOf()
) : ExpandableItemModel<GradeFull>(grades) {
override var level = 2
var hasUnseen = false
val averages = GradesAverages()
var proposedGrade: GradeFull? = null
var finalGrade: GradeFull? = null

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