Compare commits

...

43 Commits

Author SHA1 Message Date
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
103 changed files with 2115 additions and 691 deletions

View File

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

View File

@ -1,4 +1,4 @@
<h3>Wersja 4.0-beta.12, 2020-03-10</h3> <h3>Wersja 4.0-beta.14, 2020-03-24</h3>
<ul> <ul>
<li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość oraz poprawność pobieranych danych.</li> <li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość 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><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>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0xb8, 0x59, 0x75, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0xc4, 0x97, 0xfb, 0xbd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -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.Manifest
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -10,6 +12,7 @@ import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
@ -23,6 +26,7 @@ import android.util.Base64
import android.util.Base64.NO_WRAP import android.util.Base64.NO_WRAP
import android.util.Base64.encodeToString import android.util.Base64.encodeToString
import android.view.View import android.view.View
import android.view.WindowManager
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.RadioButton import android.widget.RadioButton
@ -51,6 +55,7 @@ import okhttp3.RequestBody
import okhttp3.TlsVersion import okhttp3.TlsVersion
import okio.Buffer import okio.Buffer
import pl.szczodrzynski.edziennik.data.api.* 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.SzkolnyApiException
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.db.entity.Notification 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) { private fun ApiResponse.Error.toErrorCode() = when (this.code) {
else -> ERROR_API_EXCEPTION 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? { inline fun <A, B, R> ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? {
if (a != null && b != null) { 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 } fun Iterable<Int>.averageOrNull() = this.average().let { if (it.isNaN()) null else it }
@kotlin.jvm.JvmName("averageOrNullOfFloat") @kotlin.jvm.JvmName("averageOrNullOfFloat")
fun Iterable<Float>.averageOrNull() = this.average().let { if (it.isNaN()) null else it } 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

@ -49,6 +49,7 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog 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.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
@ -444,6 +445,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// WHAT'S NEW DIALOG // WHAT'S NEW DIALOG
if (app.config.appVersion < BuildConfig.VERSION_CODE) { if (app.config.appVersion < BuildConfig.VERSION_CODE) {
// force an AppSync after update
app.config.sync.lastAppSync = 0L
ChangelogDialog(this) ChangelogDialog(this)
if (app.config.appVersion < 170) { if (app.config.appVersion < 170) {
//Intent intent = new Intent(this, ChangelogIntroActivity.class); //Intent intent = new Intent(this, ChangelogIntroActivity.class);
@ -721,6 +724,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
) )
true true
} }
"createManualEvent" -> {
val date = extras.getString("eventDate")?.let { Date.fromY_m_d(it) } ?: Date.getToday()
EventManualDialog(
this,
App.profileId,
defaultDate = date
)
true
}
else -> false else -> false
} }
if (handled && !navLoading) { if (handled && !navLoading) {
@ -1139,7 +1151,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
private var targetHomeId: Int = -1 private var targetHomeId: Int = -1
override fun onBackPressed() { override fun onBackPressed() {
if (!b.navView.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() b.navView.drawer.toggle()
} else { } else {
navigateUp() navigateUp()

View File

@ -105,11 +105,6 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() } get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value } 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 var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf() private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init { init {

View File

@ -20,6 +20,11 @@ class ConfigSync(private val config: Config) {
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true } get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true }
set(value) { config.set("syncEnabled", value); mSyncEnabled = value } 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 private var mSyncOnlyWifi: Boolean? = null
var onlyWifi: Boolean var onlyWifi: Boolean
get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates } get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates }
@ -35,6 +40,11 @@ class ConfigSync(private val config: Config) {
get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true } get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true }
set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value } 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 }
/* ____ _ _ _ /* ____ _ _ _
/ __ \ (_) | | | | / __ \ (_) | | | |
| | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___ | | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___

View File

@ -21,11 +21,6 @@ class ProfileConfigGrades(private val config: ProfileConfig) {
get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES } get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES }
set(value) { config.set("yearAverageMode", value); mYearAverageMode = value } 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 private var mHideImproved: Boolean? = null
var hideImproved: Boolean var hideImproved: Boolean
get() { mHideImproved = mHideImproved ?: config.values.get("hideImproved", false); return mHideImproved ?: false } get() { mHideImproved = mHideImproved ?: config.values.get("hideImproved", false); return mHideImproved ?: false }
@ -45,6 +40,11 @@ class ProfileConfigGrades(private val config: ProfileConfig) {
get() { mMinusValue = mMinusValue ?: config.values.getFloat("minusValue"); return mMinusValue } get() { mMinusValue = mMinusValue ?: config.values.getFloat("minusValue"); return mMinusValue }
set(value) { config.set("minusValue", value); mMinusValue = value } 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 private var mDontCountGrades: List<String>? = null
var dontCountGrades: List<String> var dontCountGrades: List<String>
get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() } get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() }

View File

@ -14,7 +14,7 @@ class ProfileConfigMigration(config: ProfileConfig) {
if (dataVersion < 1) { if (dataVersion < 1) {
grades.colorMode = COLOR_MODE_WEIGHTED grades.colorMode = COLOR_MODE_WEIGHTED
grades.countZeroToAvg = true grades.dontCountEnabled = false
grades.yearAverageMode = YEAR_ALL_GRADES grades.yearAverageMode = YEAR_ALL_GRADES
ui.agendaViewType = AGENDA_DEFAULT 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.IApiTask
import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.toApiError
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -181,7 +182,7 @@ class ApiService : Service() {
is SzkolnyTask -> task.run(taskCallback) is SzkolnyTask -> task.run(taskCallback)
} }
} catch (e: Exception) { } 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_LIBRUS_API_NOTICEBOARD_PROBLEM = 183
const val ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED = 184 const val ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED = 184
const val ERROR_LIBRUS_API_DEVICE_REGISTERED = 185 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_INVALID_LOGIN = 201
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202 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_WEB_NO_SESSION_ID = 215
const val ERROR_LOGIN_MOBIDZIENNIK_API2_INVALID_LOGIN = 216 const val ERROR_LOGIN_MOBIDZIENNIK_API2_INVALID_LOGIN = 216
const val ERROR_LOGIN_MOBIDZIENNIK_API2_OTHER = 217 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_SYMBOL = 301
const val ERROR_LOGIN_VULCAN_INVALID_TOKEN = 302 const val ERROR_LOGIN_VULCAN_INVALID_TOKEN = 302

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.data.api package pl.szczodrzynski.edziennik.data.api
import kotlin.text.RegexOption.DOT_MATCHES_ALL import kotlin.text.RegexOption.DOT_MATCHES_ALL
import kotlin.text.RegexOption.IGNORE_CASE
object Regexes { object Regexes {
val STYLE_CSS_COLOR by lazy { val STYLE_CSS_COLOR by lazy {
@ -126,7 +127,7 @@ object Regexes {
val LIBRUS_ATTACHMENT_KEY by lazy { 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 { val EDUDZIENNIK_TEACHERS by lazy {
"""<div class="teacher">.*?<p>(.+?) (.+?)</p>""".toRegex(DOT_MATCHES_ALL) """<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

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

View File

@ -55,13 +55,20 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) {
} }
when { when {
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text) text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN
text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text) text.contains("<message>Nie odnaleziono wiadomości.</message>") -> ERROR_LIBRUS_MESSAGES_NOT_FOUND
text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) text.contains("stop.png") -> ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED
text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text) text.contains("eAccessDeny") -> ERROR_LIBRUS_MESSAGES_ACCESS_DENIED
text.contains("<status>error</status>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text) text.contains("OffLine") -> ERROR_LIBRUS_MESSAGES_MAINTENANCE
text.contains("<type>eVarWhitThisNameNotExists</type>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) text.contains("<status>error</status>") -> ERROR_LIBRUS_MESSAGES_ERROR
text.contains("<error>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text) 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 { try {
@ -139,13 +146,20 @@ open class LibrusMessages(open val data: DataLibrus, open val lastSync: Long?) {
} }
when { when {
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text) text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN
text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text) text.contains("<message>Nie odnaleziono wiadomości.</message>") -> ERROR_LIBRUS_MESSAGES_NOT_FOUND
text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) text.contains("stop.png") -> ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED
text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text) text.contains("eAccessDeny") -> ERROR_LIBRUS_MESSAGES_ACCESS_DENIED
text.contains("<status>error</status>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text) text.contains("OffLine") -> ERROR_LIBRUS_MESSAGES_MAINTENANCE
text.contains("<type>eVarWhitThisNameNotExists</type>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text) text.contains("<status>error</status>") -> ERROR_LIBRUS_MESSAGES_ERROR
text.contains("<error>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text) 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 { try {

View File

@ -42,7 +42,7 @@ class LibrusApiAttendances(override val data: DataLibrus,
val teacherId = attendance.getJsonObject("AddedBy")?.getLong("Id") val teacherId = attendance.getJsonObject("AddedBy")?.getLong("Id")
val semester = attendance.getInt("Semester") ?: return@forEach val semester = attendance.getInt("Semester") ?: return@forEach
val type = attendance.getJsonObject("Type")?.getLong("Id") ?: 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 topic = typeObject?.name ?: ""
val startTime = data.lessonRanges.get(lessonNo).startTime val startTime = data.lessonRanges.get(lessonNo).startTime
@ -60,13 +60,13 @@ class LibrusApiAttendances(override val data: DataLibrus,
topic, topic,
lessonDate, lessonDate,
startTime, startTime,
typeObject.type typeObject?.type ?: Attendance.TYPE_CUSTOM
) )
val addedDate = Date.fromIso(attendance.getString("AddDate") ?: return@forEach) val addedDate = Date.fromIso(attendance.getString("AddDate") ?: return@forEach)
data.attendanceList.add(attendanceObject) data.attendanceList.add(attendanceObject)
if(typeObject.type != Attendance.TYPE_PRESENT) { if(typeObject?.type != Attendance.TYPE_PRESENT) {
data.metadataList.add(Metadata( data.metadataList.add(Metadata(
profileId, profileId,
Metadata.TYPE_ATTENDANCE, Metadata.TYPE_ATTENDANCE,

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,23 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
val profile val profile
get() = data.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( fun webGet(
tag: String, tag: String,
endpoint: String, endpoint: String,
@ -65,6 +82,12 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
return return
} }
if (text.contains("<h2>Problemy z wydajnością</h2>")) {
data.error(ApiError(TAG, ERROR_MOBIDZIENNIK_WEB_SERVER_PROBLEM)
.withResponse(response))
return
}
try { try {
onSuccess(text) onSuccess(text)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -118,7 +118,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik,
// this needs to be at the end // this needs to be at the end
message.apply { message.apply {
this.body = body.html().replace("\n", "<br>") this.body = body.html()
clearAttachments() clearAttachments()
content.select("ul li").map { it.select("a").first() }.forEach { 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 doc = Jsoup.parse(text)
val listElement = doc.getElementsByClass("spis").first() val listElement = doc.getElementsByClass("spis")?.first()
if (listElement == null) { if (listElement == null) {
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY) data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY)
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL)
return@webGet return@webGet
} }
val list = listElement.getElementsByClass("podswietl") val list = listElement.getElementsByClass("podswietl")
for (item in list) { list?.forEach { item ->
val id = item.attr("rel").replace("[^\\d]".toRegex(), "").toLongOrNull() ?: continue val id = item.attr("rel").replace("[^\\d]".toRegex(), "").toLongOrNull() ?: return@forEach
val subjectEl = item.select("td:eq(0) div").first() val subjectEl = item.select("td:eq(0) div").first()
val subject = subjectEl.text() val subject = subjectEl.text()

View File

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

View File

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

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.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.isNotNullNorEmpty 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) { 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" override fun generateUserCode() = "$schoolName:$studentId"
/** /**
@ -188,11 +210,11 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"SZ9" -> "http://hack.szkolny.eu" "SZ9" -> "http://hack.szkolny.eu"
else -> null 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? val fullApiUrl: String?
get() { 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.*
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.models.ApiError 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 pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.util.* import java.util.*
@ -38,26 +36,10 @@ open class VulcanApi(open val data: DataVulcan, open val lastSync: Long?) {
baseUrl: Boolean = false, baseUrl: Boolean = false,
onSuccess: (json: JsonObject, response: Response?) -> Unit 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") 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() val finalPayload = JsonObject()
parameters.map { (name, value) -> parameters.map { (name, value) ->
when (value) { when (value) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ class MyFirebaseService : FirebaseService(), CoroutineScope {
putString(System.currentTimeMillis().toString(), message.toString()) putString(System.currentTimeMillis().toString(), message.toString())
apply() apply()
} }
val profiles = app.db.profileDao().profilesForSyncNow val profiles = app.db.profileDao().profilesForFirebaseNow
when (message.from) { when (message.from) {
"640759989760" -> SzkolnyAppFirebase(app, profiles, message) "640759989760" -> SzkolnyAppFirebase(app, profiles, message)
"747285019373" -> SzkolnyMobidziennikFirebase(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>() val notificationList = mutableListOf<Notification>()
teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team -> 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( val event = Event(
team.profileId, team.profileId,
json.getLong("id") ?: return, json.getLong("id") ?: return,
@ -116,12 +118,9 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
team.id 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.sharedBy = json.getString("sharedBy")
event.sharedByName = json.getString("sharedByName") event.sharedByName = json.getString("sharedByName")
if (profile?.userCode == event.sharedBy) event.sharedBy = "self" if (profile.userCode == event.sharedBy) event.sharedBy = "self"
val metadata = Metadata( val metadata = Metadata(
event.profileId, event.profileId,
@ -132,18 +131,6 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
json.getLong("addedDate") ?: System.currentTimeMillis() 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 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 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), title = app.getNotificationTitle(type),
text = message, text = message,
type = type, type = type,
profileId = profile?.id, profileId = profile.id,
profileName = profile?.name, profileName = profile.name,
viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA,
addedDate = metadata.addedDate addedDate = metadata.addedDate
).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong()) ).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>() val notificationList = mutableListOf<Notification>()
teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team -> 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 val notificationFilter = app.config.getFor(team.profileId).sync.notificationFilter
if (!notificationFilter.contains(Notification.TYPE_REMOVED_SHARED_EVENT)) { if (!notificationFilter.contains(Notification.TYPE_REMOVED_SHARED_EVENT)) {
val notification = Notification( val notification = Notification(
id = Notification.buildId(profile?.id id = Notification.buildId(profile.id
?: 0, Notification.TYPE_REMOVED_SHARED_EVENT, eventId), ?: 0, Notification.TYPE_REMOVED_SHARED_EVENT, eventId),
title = app.getNotificationTitle(Notification.TYPE_REMOVED_SHARED_EVENT), title = app.getNotificationTitle(Notification.TYPE_REMOVED_SHARED_EVENT),
text = message, text = message,
type = Notification.TYPE_REMOVED_SHARED_EVENT, type = Notification.TYPE_REMOVED_SHARED_EVENT,
profileId = profile?.id, profileId = profile.id,
profileName = profile?.name, profileName = profile.name,
viewId = MainActivity.DRAWER_ITEM_AGENDA viewId = MainActivity.DRAWER_ITEM_AGENDA
) )
notificationList += notification notificationList += notification

View File

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

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

View File

@ -29,9 +29,11 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding 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.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS
import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.TextInputDropDown
@ -62,12 +64,12 @@ class EventManualDialog(
private val app by lazy { activity.application as App } private val app by lazy { activity.application as App }
private lateinit var b: DialogEventManualV2Binding private lateinit var b: DialogEventManualV2Binding
private lateinit var dialog: AlertDialog private lateinit var dialog: AlertDialog
private var profile: Profile? = null
private var customColor: Int? = null private var customColor: Int? = null
private val editingShared = editingEvent?.sharedBy != null private val editingShared = editingEvent?.sharedBy != null
private val editingOwn = editingEvent?.sharedBy == "self" private val editingOwn = editingEvent?.sharedBy == "self"
private var removeEventDialog: AlertDialog? = null private var removeEventDialog: AlertDialog? = null
private var defaultLoaded = false
private val api by lazy { private val api by lazy {
SzkolnyApi(app) SzkolnyApi(app)
@ -76,6 +78,8 @@ class EventManualDialog(
private var enqueuedWeekDialog: AlertDialog? = null private var enqueuedWeekDialog: AlertDialog? = null
private var enqueuedWeekStart = Date.getToday() private var enqueuedWeekStart = Date.getToday()
private var progressDialog: AlertDialog? = null
init { run { init { run {
if (activity.isFinishing) if (activity.isFinishing)
return@run return@run
@ -95,6 +99,8 @@ class EventManualDialog(
.setOnDismissListener { .setOnDismissListener {
onDismissListener?.invoke(TAG) onDismissListener?.invoke(TAG)
EventBus.getDefault().unregister(this@EventManualDialog) EventBus.getDefault().unregister(this@EventManualDialog)
enqueuedWeekDialog?.dismiss()
progressDialog?.dismiss()
} }
.setCancelable(false) .setCancelable(false)
.create() .create()
@ -168,7 +174,7 @@ class EventManualDialog(
enqueuedWeekStart = weekStart enqueuedWeekStart = weekStart
EdziennikTask.syncProfile( EdziennikTask.syncProfile(
profileId = App.profileId, profileId = profileId,
viewIds = listOf( viewIds = listOf(
MainActivity.DRAWER_ITEM_TIMETABLE to 0 MainActivity.DRAWER_ITEM_TIMETABLE to 0
), ),
@ -178,13 +184,40 @@ class EventManualDialog(
).enqueue(activity) ).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) @Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) { fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
if (event.profileId == App.profileId) { if (event.profileId == profileId) {
enqueuedWeekDialog?.dismiss() enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null enqueuedWeekDialog = null
progressDialog?.dismiss()
launch { launch {
b.timeDropdown.loadItems() b.timeDropdown.loadItems()
b.timeDropdown.selectDefault(editingEvent?.startTime)
b.timeDropdown.selectDefault(defaultLesson?.displayStartTime ?: defaultTime)
} }
} }
} }
@ -193,19 +226,22 @@ class EventManualDialog(
fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) { fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) {
enqueuedWeekDialog?.dismiss() enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null enqueuedWeekDialog = null
progressDialog?.dismiss()
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) { fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) {
dialog.dismiss()
enqueuedWeekDialog?.dismiss() enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null enqueuedWeekDialog = null
progressDialog?.dismiss()
} }
private fun loadLists() { launch { private fun loadLists() { launch {
profile = withContext(Dispatchers.Default) { app.db.profileDao().getByIdNow(profileId) }
with (b.dateDropdown) { with (b.dateDropdown) {
db = app.db db = app.db
profileId = App.profileId profileId = profileId
showWeekDays = false showWeekDays = false
showDays = true showDays = true
showOtherDate = true showOtherDate = true
@ -233,7 +269,7 @@ class EventManualDialog(
with (b.timeDropdown) { with (b.timeDropdown) {
db = app.db db = app.db
profileId = App.profileId profileId = profileId
showAllDay = true showAllDay = true
showCustomTime = true showCustomTime = true
lessonsDate = b.dateDropdown.getSelected() as? Date ?: Date.getToday() lessonsDate = b.dateDropdown.getSelected() as? Date ?: Date.getToday()
@ -241,6 +277,8 @@ class EventManualDialog(
if (!loadItems()) if (!loadItems())
syncTimetable(lessonsDate ?: Date.getToday()) syncTimetable(lessonsDate ?: Date.getToday())
selectDefault(editingEvent?.startTime) selectDefault(editingEvent?.startTime)
if (editingEvent != null && editingEvent.startTime == null)
select(0L)
selectDefault(defaultLesson?.displayStartTime ?: defaultTime) selectDefault(defaultLesson?.displayStartTime ?: defaultTime)
onLessonSelected = { lesson -> onLessonSelected = { lesson ->
lesson.displaySubjectId?.let { b.subjectDropdown.selectSubject(it) } ?: b.subjectDropdown.deselect() lesson.displaySubjectId?.let { b.subjectDropdown.selectSubject(it) } ?: b.subjectDropdown.deselect()
@ -251,7 +289,7 @@ class EventManualDialog(
with (b.teamDropdown) { with (b.teamDropdown) {
db = app.db db = app.db
profileId = App.profileId profileId = profileId
showNoTeam = true showNoTeam = true
loadItems() loadItems()
selectTeamClass() selectTeamClass()
@ -261,7 +299,7 @@ class EventManualDialog(
with (b.subjectDropdown) { with (b.subjectDropdown) {
db = app.db db = app.db
profileId = App.profileId profileId = profileId
showNoSubject = true showNoSubject = true
showCustomSubject = false showCustomSubject = false
loadItems() loadItems()
@ -271,7 +309,7 @@ class EventManualDialog(
with (b.teacherDropdown) { with (b.teacherDropdown) {
db = app.db db = app.db
profileId = App.profileId profileId = profileId
showNoTeacher = true showNoTeacher = true
loadItems() loadItems()
selectDefault(editingEvent?.teacherId) selectDefault(editingEvent?.teacherId)
@ -323,7 +361,7 @@ class EventManualDialog(
} }
b.typeColor.onClick { 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() val colorPickerDialog = ColorPickerDialog.newBuilder()
.setColor(currentColor) .setColor(currentColor)
.create() .create()
@ -365,16 +403,24 @@ class EventManualDialog(
private fun saveEvent() { private fun saveEvent() {
val date = b.dateDropdown.getSelected() as? Date val date = b.dateDropdown.getSelected() as? Date
val startTimePair = b.timeDropdown.getSelected() as? Pair<*, *> val timeSelected = b.timeDropdown.getSelected()
val startTime = startTimePair?.first as? Time
val teamId = b.teamDropdown.getSelected() as? Long val teamId = b.teamDropdown.getSelected() as? Long
val type = b.typeDropdown.selected?.id val type = b.typeDropdown.selected?.id
val topic = b.topic.text?.toString() val topic = b.topic.text?.toString()
val subjectId = b.subjectDropdown.getSelected() as? Long val subjectId = b.subjectDropdown.getSelected() as? Long
val teacherId = b.teacherDropdown.getSelected() as? Long val teacherId = b.teacherDropdown.getSelected()
val share = b.shareSwitch.isChecked 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.dateDropdown.error = null
b.teamDropdown.error = null b.teamDropdown.error = null
b.typeDropdown.error = null b.typeDropdown.error = null
@ -384,24 +430,39 @@ class EventManualDialog(
if (date == null) { if (date == null) {
b.dateDropdown.error = app.getString(R.string.dialog_event_manual_date_choose) 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 isError = true
} }
if (share && teamId == null) { if (share && teamId == null) {
b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose) b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose)
if (!isError) b.teamDropdown.parent.requestChildFocus(b.teamDropdown, b.teamDropdown)
isError = true isError = true
} }
if (type == null) { if (type == null) {
b.typeDropdown.error = app.getString(R.string.dialog_event_manual_type_choose) b.typeDropdown.error = app.getString(R.string.dialog_event_manual_type_choose)
if (!isError) b.typeDropdown.requestFocus()
isError = true isError = true
} }
if (topic.isNullOrBlank()) { if (topic.isNullOrBlank()) {
b.topic.error = app.getString(R.string.dialog_event_manual_topic_choose) b.topic.error = app.getString(R.string.dialog_event_manual_topic_choose)
if (!isError) b.topic.requestFocus()
isError = true isError = true
} }
val startTime = if (timeSelected == 0L)
null
else
(timeSelected as? Pair<*, *>)?.first as? Time
if (isError) return if (isError) return
val id = System.currentTimeMillis() val id = System.currentTimeMillis()
@ -436,7 +497,7 @@ class EventManualDialog(
val profile = app.db.profileDao().getByIdNow(profileId) val profile = app.db.profileDao().getByIdNow(profileId)
if (!share && !editingShared) { 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) finishAdding(eventObject, metadataObject)
} }
else if (editingShared && !editingOwn) { else if (editingShared && !editingOwn) {
@ -444,7 +505,7 @@ class EventManualDialog(
// TODO // TODO
} }
else if (!share && editingShared) { else if (!share && editingShared) {
Toast.makeText(activity, R.string.event_manual_unshare, Toast.LENGTH_SHORT).show() showSharingProgressDialog()
eventObject.apply { eventObject.apply {
sharedBy = null sharedBy = null
@ -453,13 +514,16 @@ class EventManualDialog(
api.runCatching(activity) { api.runCatching(activity) {
unshareEvent(eventObject) unshareEvent(eventObject)
} ?: return@launch } ?: run {
progressDialog?.dismiss()
return@launch
}
eventObject.sharedByName = null eventObject.sharedByName = null
finishAdding(eventObject, metadataObject) finishAdding(eventObject, metadataObject)
} }
else if (share) { else if (share) {
Toast.makeText(activity, R.string.event_manual_share, Toast.LENGTH_SHORT).show() showSharingProgressDialog()
eventObject.apply { eventObject.apply {
sharedBy = profile?.userCode sharedBy = profile?.userCode
@ -470,7 +534,10 @@ class EventManualDialog(
api.runCatching(activity) { api.runCatching(activity) {
shareEvent(eventObject.withMetadata(metadataObject)) shareEvent(eventObject.withMetadata(metadataObject))
} ?: return@launch } ?: run {
progressDialog?.dismiss()
return@launch
}
eventObject.sharedBy = "self" eventObject.sharedBy = "self"
finishAdding(eventObject, metadataObject) finishAdding(eventObject, metadataObject)
@ -478,6 +545,7 @@ class EventManualDialog(
else { else {
Toast.makeText(activity, "Unknown action :(", Toast.LENGTH_SHORT).show() Toast.makeText(activity, "Unknown action :(", Toast.LENGTH_SHORT).show()
} }
progressDialog?.dismiss()
} }
} }
@ -485,11 +553,14 @@ class EventManualDialog(
launch { launch {
if (editingShared && editingOwn) { if (editingShared && editingOwn) {
// unshare + remove own event // unshare + remove own event
Toast.makeText(activity, R.string.event_manual_unshare_remove, Toast.LENGTH_SHORT).show() showRemovingProgressDialog()
api.runCatching(activity) { api.runCatching(activity) {
unshareEvent(editingEvent!!) unshareEvent(editingEvent!!)
} ?: return@launch } ?: run {
progressDialog?.dismiss()
return@launch
}
finishRemoving() finishRemoving()
} else if (editingShared && !editingOwn) { } else if (editingShared && !editingOwn) {
@ -498,9 +569,10 @@ class EventManualDialog(
// TODO // TODO
} else { } else {
// remove event // 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() finishRemoving()
} }
progressDialog?.dismiss()
} }
} }

View File

@ -58,7 +58,7 @@ class GradeDetailsDialog(
b.weightText = manager.getWeightString(app, grade) b.weightText = manager.getWeightString(app, grade)
b.commentVisible = false b.commentVisible = false
b.devMode = App.debugMode b.devMode = App.debugMode
b.gradeName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.3) 0x99000000.toInt() else 0x99ffffff.toInt()) b.gradeName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt())
b.gradeName.background.setTintColor(gradeColor) b.gradeName.background.setTintColor(gradeColor)
b.gradeValue = if (grade.weight == 0f || grade.value < 0f) -1f else manager.getGradeValue(grade) b.gradeValue = if (grade.weight == 0f || grade.value < 0f) -1f else manager.getGradeValue(grade)

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_AVG
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_SEM 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 pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
import java.util.*
class GradesConfigDialog( class GradesConfigDialog(
val activity: AppCompatActivity, val activity: AppCompatActivity,
@ -88,14 +89,34 @@ class GradesConfigDialog(
else -> null else -> null
}?.isChecked = true }?.isChecked = true
b.dontCountZeroToAverage.isChecked = !profileConfig.countZeroToAvg b.dontCountGrades.isChecked = profileConfig.dontCountEnabled && profileConfig.dontCountGrades.isNotEmpty()
b.hideImproved.isChecked = profileConfig.hideImproved b.hideImproved.isChecked = profileConfig.hideImproved
b.averageWithoutWeight.isChecked = profileConfig.averageWithoutWeight 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() { private fun saveConfig() {
profileConfig.plusValue = if (b.customPlusCheckBox.isChecked) b.customPlusValue.progress else null profileConfig.plusValue = if (b.customPlusCheckBox.isChecked) b.customPlusValue.progress else null
profileConfig.minusValue = if (b.customMinusCheckBox.isChecked) b.customMinusValue.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() { private fun initView() {
@ -127,7 +148,6 @@ class GradesConfigDialog(
b.gradeAverageMode2.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_AVG_2_SEM } b.gradeAverageMode2.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_AVG_2_SEM }
b.gradeAverageMode3.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_SEM_2_SEM } b.gradeAverageMode3.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_SEM_2_SEM }
b.dontCountZeroToAverage.onChange { _, isChecked -> profileConfig.countZeroToAvg = !isChecked }
b.hideImproved.onChange { _, isChecked -> profileConfig.hideImproved = isChecked } b.hideImproved.onChange { _, isChecked -> profileConfig.hideImproved = isChecked }
b.averageWithoutWeight.onChange { _, isChecked -> profileConfig.averageWithoutWeight = isChecked } b.averageWithoutWeight.onChange { _, isChecked -> profileConfig.averageWithoutWeight = isChecked }

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,8 +18,8 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
@ -116,18 +116,16 @@ class GenerateBlockTimetableDialog(
}} }}
private fun selectDate() { private fun selectDate() {
MaterialDatePicker.Builder val date = Date.getToday()
.datePicker() DatePickerDialog
.setSelection(Date.getToday().inMillis) .newInstance({ _, year, monthOfYear, dayOfMonth ->
.build() val dateSelected = Date(year, monthOfYear, dayOfMonth)
generateBlockTimetable(dateSelected.weekStart, dateSelected.weekEnd)
}, date.year, date.month, date.day)
.apply { .apply {
addOnPositiveButtonClickListener { dateInMillis -> accentColor = R.attr.colorPrimary.resolveAttr(this@GenerateBlockTimetableDialog.activity)
dismiss() show(this@GenerateBlockTimetableDialog.activity.supportFragmentManager, "DatePickerDialog")
val selectedDate = Date.fromMillis(dateInMillis)
generateBlockTimetable(selectedDate.weekStart, selectedDate.weekEnd)
}
} }
.show(activity.supportFragmentManager, "MaterialDatePicker")
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)

View File

@ -33,6 +33,7 @@ import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration;
import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem; 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.LoginStore.LOGIN_TYPE_LIBRUS;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT; import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT;
@ -90,6 +91,18 @@ public class AnnouncementsFragment extends Fragment {
recyclerView.setLayoutManager(linearLayoutManager); recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addItemDecoration(new SimpleDividerItemDecoration(view.getContext())); 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 -> { app.db.announcementDao().getAll(App.Companion.getProfileId()).observe(this, announcements -> {
if (app == null || activity == null || b == null || !isAdded()) if (app == null || activity == null || b == null || !isAdded())
return; return;

View File

@ -21,6 +21,7 @@ import androidx.core.graphics.ColorUtils;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial; 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.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem; 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;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_ABSENT_EXCUSED; import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_ABSENT_EXCUSED;
import static pl.szczodrzynski.edziennik.data.db.entity.Attendance.TYPE_BELATED; 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.setLayoutManager(linearLayoutManager);
b.attendanceView.addItemDecoration(new SimpleDividerItemDecoration(getContext())); 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 -> { App.db.attendanceDao().getAll(App.Companion.getProfileId()).observe(this, attendance -> {
if (app == null || activity == null || b == null || !isAdded()) if (app == null || activity == null || b == null || !isAdded())
return; return;

View File

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

View File

@ -15,6 +15,7 @@ import androidx.appcompat.widget.PopupMenu;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial; 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.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem; 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; import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_NOTICE;
public class BehaviourFragment extends Fragment { public class BehaviourFragment extends Fragment {
@ -97,6 +99,18 @@ public class BehaviourFragment extends Fragment {
b.noticesView.setHasFixedSize(true); b.noticesView.setHasFixedSize(true);
b.noticesView.setLayoutManager(linearLayoutManager); 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 -> { app.db.noticeDao().getAll(App.Companion.getProfileId()).observe(this, notices -> {
if (app == null || activity == null || b == null || !isAdded()) if (app == null || activity == null || b == null || !isAdded())
return; return;

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

View File

@ -77,9 +77,9 @@ class GradeView : AppCompatTextView {
TYPE_SEMESTER2_PROPOSED, TYPE_SEMESTER2_PROPOSED,
TYPE_YEAR_PROPOSED -> android.R.attr.textColorPrimary.resolveAttr(context) TYPE_YEAR_PROPOSED -> android.R.attr.textColorPrimary.resolveAttr(context)
else -> if (ColorUtils.calculateLuminance(gradeColor) > 0.3) else -> if (ColorUtils.calculateLuminance(gradeColor) > 0.3)
0x99000000.toInt() 0xaa000000.toInt()
else else
0x99ffffff.toInt() 0xccffffff.toInt()
}) })
//typeface = Typeface.create("sans-serif-light", Typeface.NORMAL) //typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)

View File

@ -13,6 +13,8 @@ import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager 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
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2 import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -53,6 +55,7 @@ class GradesFragment : Fragment(), CoroutineScope {
GradesAdapter(activity) GradesAdapter(activity)
} }
private val manager by lazy { app.gradesManager } private val manager by lazy { app.gradesManager }
private val dontCountEnabled by lazy { manager.dontCountEnabled }
private val dontCountGrades by lazy { manager.dontCountGrades } private val dontCountGrades by lazy { manager.dontCountGrades }
private var expandSubjectId = 0L private var expandSubjectId = 0L
@ -80,6 +83,16 @@ class GradesFragment : Fragment(), CoroutineScope {
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
//addItemDecoration(SimpleDividerItemDecoration(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
}
}
})
} }
} }
@ -299,7 +312,7 @@ class GradesFragment : Fragment(), CoroutineScope {
private fun countGrade(grade: Grade, averages: GradesAverages) { private fun countGrade(grade: Grade, averages: GradesAverages) {
val value = manager.getGradeValue(grade) val value = manager.getGradeValue(grade)
val weight = manager.getGradeWeight(dontCountGrades, grade) val weight = manager.getGradeWeight(dontCountEnabled, dontCountGrades, grade)
when (grade.type) { when (grade.type) {
Grade.TYPE_NORMAL -> { Grade.TYPE_NORMAL -> {
if (grade.value > 0f) { if (grade.value > 0f) {

View File

@ -3,7 +3,6 @@ package pl.szczodrzynski.edziennik.ui.modules.grades.editor
import android.content.Context import android.content.Context
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -49,8 +48,7 @@ class GradesEditorAdapter(
holder.gradesListName.text = editorGrade.name holder.gradesListName.text = editorGrade.name
holder.gradesListName.isSelected = true holder.gradesListName.isSelected = true
holder.gradesListName.setTypeface(null, Typeface.BOLD) holder.gradesListName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.25) 0xaa000000.toInt() else 0xccffffff.toInt())
holder.gradesListName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.25) -0x1000000 else -0x1)
holder.gradesListName.background.colorFilter = PorterDuffColorFilter(gradeColor, PorterDuff.Mode.MULTIPLY) holder.gradesListName.background.colorFilter = PorterDuffColorFilter(gradeColor, PorterDuff.Mode.MULTIPLY)
holder.gradesListCategory.text = editorGrade.category holder.gradesListCategory.text = editorGrade.category
if (editorGrade.weight < 0) { 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.data.db.entity.Grade
import pl.szczodrzynski.edziennik.databinding.FragmentGradesEditorBinding import pl.szczodrzynski.edziennik.databinding.FragmentGradesEditorBinding
import pl.szczodrzynski.edziennik.utils.Colors 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_AVG
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM 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 import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG
@ -60,16 +59,13 @@ class GradesEditorFragment : Fragment() {
if (context == null) if (context == null)
return null return null
app = activity.application as App 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 // activity, context and profile is valid
b = FragmentGradesEditorBinding.inflate(inflater) b = FragmentGradesEditorBinding.inflate(inflater)
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (app.profile == null || !isAdded) if (!isAdded)
return return
subjectId = arguments.getLong("subjectId", -1) subjectId = arguments.getLong("subjectId", -1)
@ -110,7 +106,7 @@ class GradesEditorFragment : Fragment() {
continue continue
} }
var weight = editorGrade.weight var weight = editorGrade.weight
if (!config.countZeroToAvg && editorGrade.name == "0") { if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.toLowerCase().trim())) {
weight = 0f weight = 0f
} }
val value = editorGrade.value * weight val value = editorGrade.value * weight
@ -175,7 +171,7 @@ class GradesEditorFragment : Fragment() {
averageSemester = 0f averageSemester = 0f
for (editorGrade in editorGrades) { for (editorGrade in editorGrades) {
var weight = editorGrade.weight var weight = editorGrade.weight
if (!config.countZeroToAvg && editorGrade.name == "0") { if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.toLowerCase().trim())) {
weight = 0f weight = 0f
} }
val value = editorGrade.value * weight val value = editorGrade.value * weight
@ -218,7 +214,7 @@ class GradesEditorFragment : Fragment() {
continue continue
} }
var weight = grade.weight var weight = grade.weight
if (!config.countZeroToAvg && grade.name == "0") { if (config.dontCountEnabled && config.dontCountGrades.contains(grade.name.toLowerCase().trim())) {
weight = 0f weight = 0f
} }
val value = grade.value * weight val value = grade.value * weight

View File

@ -102,7 +102,7 @@ class StatsViewHolder(
.show() .show()
} }
b.customValueDivider.isVisible = manager.plusValue != null || manager.minusValue != null b.customValueDivider.isVisible = manager.dontCountEnabled || manager.plusValue != null || manager.minusValue != null
b.customValueLayout.isVisible = b.customValueDivider.isVisible b.customValueLayout.isVisible = b.customValueDivider.isVisible
b.customValueButton.onClick { b.customValueButton.onClick {
GradesConfigDialog(activity, reloadOnDismiss = true) GradesConfigDialog(activity, reloadOnDismiss = true)

View File

@ -16,11 +16,8 @@ import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.GradesItemSubjectBinding import pl.szczodrzynski.edziennik.databinding.GradesItemSubjectBinding
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.setText
import pl.szczodrzynski.edziennik.ui.modules.grades.GradeView import pl.szczodrzynski.edziennik.ui.modules.grades.GradeView
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED
@ -38,7 +35,7 @@ class SubjectViewHolder(
override fun onBind(activity: AppCompatActivity, app: App, item: GradesSubject, position: Int, adapter: GradesAdapter) { override fun onBind(activity: AppCompatActivity, app: App, item: GradesSubject, position: Int, adapter: GradesAdapter) {
val manager = app.gradesManager val manager = app.gradesManager
val contextWrapper = ContextThemeWrapper(activity, Themes.themeInt) val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.subjectName.text = item.subjectName b.subjectName.text = item.subjectName
b.dropdownIcon.rotation = when (item.state) { b.dropdownIcon.rotation = when (item.state) {
@ -62,6 +59,7 @@ class SubjectViewHolder(
if (firstSemester.number != item.semester) { if (firstSemester.number != item.semester) {
b.gradesContainer.addView(TextView(contextWrapper).apply { b.gradesContainer.addView(TextView(contextWrapper).apply {
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
setText(R.string.grades_preview_other_semester, firstSemester.number) setText(R.string.grades_preview_other_semester, firstSemester.number)
setPadding(0, 0, 5.dp, 0) setPadding(0, 0, 5.dp, 0)
maxLines = 1 maxLines = 1
@ -88,6 +86,7 @@ class SubjectViewHolder(
} }
b.previewContainer.addView(TextView(contextWrapper).apply { b.previewContainer.addView(TextView(contextWrapper).apply {
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number) text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number)
//gravity = Gravity.END //gravity = Gravity.END
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {

View File

@ -5,16 +5,18 @@
package pl.szczodrzynski.edziennik.ui.modules.home package pl.szczodrzynski.edziennik.ui.modules.home
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.*
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment.Companion.removeCard
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment.Companion.swapCards import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment.Companion.swapCards
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, private val refreshLayout: SwipeRefreshLayoutNoIndicator?) : ItemTouchHelper.Callback() { class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, private val refreshLayout: SwipeRefreshLayoutNoIndicator?) : ItemTouchHelper.Callback() {
companion object { companion object {
private const val TAG = "CardItemTouchHelperCallback" private const val TAG = "CardItemTouchHelperCallback"
private const val DRAG_FLAGS = ItemTouchHelper.UP or ItemTouchHelper.DOWN private const val DRAG_FLAGS = UP or DOWN
private const val SWIPE_FLAGS = 0 private const val SWIPE_FLAGS = LEFT
} }
private var dragCardView: MaterialCardView? = null private var dragCardView: MaterialCardView? = null
@ -27,20 +29,24 @@ class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, priv
val fromPosition = viewHolder.adapterPosition val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition val toPosition = target.adapterPosition
swapCards(fromPosition, toPosition, cardAdapter) return swapCards(fromPosition, toPosition, cardAdapter)
return true
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
removeCard(viewHolder.adapterPosition)
cardAdapter.items.removeAt(viewHolder.adapterPosition)
cardAdapter.notifyItemRemoved(viewHolder.adapterPosition)
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState) super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) { if (viewHolder != null && (actionState == ACTION_STATE_DRAG || actionState == ACTION_STATE_SWIPE)) {
dragCardView = viewHolder.itemView as MaterialCardView dragCardView = viewHolder.itemView as MaterialCardView
dragCardView?.isDragged = true dragCardView?.isDragged = true
refreshLayout?.isEnabled = false refreshLayout?.isEnabled = false
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && dragCardView != null) { }
else if (actionState == ACTION_STATE_IDLE && dragCardView != null) {
refreshLayout?.isEnabled = true refreshLayout?.isEnabled = true
dragCardView?.isDragged = false dragCardView?.isDragged = false
dragCardView = null dragCardView = null

View File

@ -0,0 +1,80 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-11.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_EVENTS
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_GRADES
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_TIMETABLE
import kotlin.collections.set
class HomeConfigDialog(
val activity: AppCompatActivity,
private val reloadOnDismiss: Boolean = true,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) {
companion object {
const val TAG = "HomeConfigDialog"
}
private val app by lazy { activity.application as App }
private val profileConfig by lazy { app.config.getFor(app.profileId).ui }
private lateinit var dialog: AlertDialog
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
val ids = listOf(
CARD_LUCKY_NUMBER,
CARD_TIMETABLE,
CARD_GRADES,
CARD_EVENTS
)
val items = listOf(
app.getString(R.string.card_type_lucky_number),
app.getString(R.string.card_type_timetable),
app.getString(R.string.card_type_grades),
app.getString(R.string.card_type_events)
)
val checkedItems = ids.map { it to false }.toMap().toMutableMap()
val profileId = App.profileId
val homeCards = profileConfig.homeCards
.filter { it.profileId == profileId }
.toMutableList()
homeCards.forEach {
checkedItems[it.cardId] = true
}
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.home_configure_add_remove)
.setMultiChoiceItems(items.toTypedArray(), checkedItems.values.toBooleanArray()) { _, which, isChecked ->
if (isChecked) {
homeCards += HomeCardModel(profileId, ids[which])
}
else {
homeCards.removeAll { it.profileId == profileId && it.cardId == ids[which] }
}
}
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.setOnDismissListener {
profileConfig.homeCards = homeCards
onDismissListener?.invoke(TAG)
if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget()
}
.show()
}}
}

View File

@ -13,6 +13,7 @@ import android.widget.Toast
import androidx.core.view.AccessibilityDelegateCompat import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -25,8 +26,12 @@ import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog
import pl.szczodrzynski.edziennik.ui.modules.home.cards.* import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeEventsCard
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeGradesCard
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeLuckyNumberCard
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeTimetableCard
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
@ -36,12 +41,12 @@ class HomeFragment : Fragment(), CoroutineScope {
companion object { companion object {
private const val TAG = "HomeFragment" private const val TAG = "HomeFragment"
fun swapCards(fromPosition: Int, toPosition: Int, cardAdapter: HomeCardAdapter) { fun swapCards(fromPosition: Int, toPosition: Int, cardAdapter: HomeCardAdapter): Boolean {
val fromCard = cardAdapter.items[fromPosition] val fromCard = cardAdapter.items[fromPosition]
val toCard = cardAdapter.items[toPosition] val toCard = cardAdapter.items[toPosition]
if (fromCard.id == 100 || toCard.id == 100) { if (fromCard.id == 100 || toCard.id == 100) {
// debug card is not swappable // debug card is not swappable
return return false
} }
cardAdapter.items[fromPosition] = cardAdapter.items[toPosition] cardAdapter.items[fromPosition] = cardAdapter.items[toPosition]
cardAdapter.items[toPosition] = fromCard cardAdapter.items[toPosition] = fromCard
@ -52,6 +57,15 @@ class HomeFragment : Fragment(), CoroutineScope {
homeCards[fromPosition] = homeCards[toPosition] homeCards[fromPosition] = homeCards[toPosition]
homeCards[toPosition] = fromPair homeCards[toPosition] = fromPair
App.config.forProfile().ui.homeCards = homeCards App.config.forProfile().ui.homeCards = homeCards
return true
}
fun removeCard(position: Int) {
val homeCards = App.config.forProfile().ui.homeCards.toMutableList()
if (position >= homeCards.size)
return
homeCards.removeAt(position)
App.config.forProfile().ui.homeCards = homeCards
} }
} }
@ -76,10 +90,17 @@ class HomeFragment : Fragment(), CoroutineScope {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null // TODO check if app, activity, b can be null
if (app.profile == null || !isAdded) if (!isAdded)
return return
activity.bottomSheet.prependItems( activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_add_remove_cards)
.withIcon(Icon.cmd_card_bulleted_settings_outline)
.withOnClickListener(OnClickListener {
activity.bottomSheet.close()
HomeConfigDialog(activity, reloadOnDismiss = true)
}),
BottomSheetPrimaryItem(true) BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_set_student_number) .withTitle(R.string.menu_set_student_number)
.withIcon(SzkolnyFont.Icon.szf_clipboard_list_outline) .withIcon(SzkolnyFont.Icon.szf_clipboard_list_outline)
@ -106,6 +127,13 @@ class HomeFragment : Fragment(), CoroutineScope {
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
}) })
) )
b.configureCards.onClick {
HomeConfigDialog(activity, reloadOnDismiss = true)
}
b.scrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int ->
b.refreshLayout.isEnabled = scrollY == 0
}
val showUnified = false val showUnified = false
@ -130,8 +158,8 @@ class HomeFragment : Fragment(), CoroutineScope {
else -> null else -> null
} }
} }
if (App.devMode) //if (App.devMode)
items += HomeDebugCard(100, app, activity, this, app.profile) // items += HomeDebugCard(100, app, activity, this, app.profile)
val adapter = HomeCardAdapter(items) val adapter = HomeCardAdapter(items)
val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout)) val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))

View File

@ -73,7 +73,7 @@ class HomeDebugCard(
} }
b.syncReceivers.onClick { b.syncReceivers.onClick {
EdziennikTask.recipientListGet(App.profileId).enqueue(activity) EdziennikTask.recipientListGet(profile.id).enqueue(activity)
} }

View File

@ -77,7 +77,7 @@ class HomeEventsCard(
} }
) )
app.db.eventDao().getAllNearest(App.profileId, Date.getToday(), 4).observe(activity, Observer { events -> app.db.eventDao().getAllNearest(profile.id, Date.getToday(), 4).observe(activity, Observer { events ->
adapter.items = events adapter.items = events
if (b.eventsView.adapter == null) { if (b.eventsView.adapter == null) {
b.eventsView.adapter = adapter b.eventsView.adapter = adapter

View File

@ -60,7 +60,7 @@ class HomeLuckyNumberCard(
R.string.home_lucky_number_details R.string.home_lucky_number_details
b.subText.setText(subTextRes, profile.name ?: "", profile.studentNumber) b.subText.setText(subTextRes, profile.name ?: "", profile.studentNumber)
app.db.luckyNumberDao().getNearestFuture(App.profileId, todayValue).observe(fragment, Observer { luckyNumber -> app.db.luckyNumberDao().getNearestFuture(profile.id, todayValue).observe(fragment, Observer { luckyNumber ->
val isYours = luckyNumber?.number == profile.studentNumber val isYours = luckyNumber?.number == profile.studentNumber
val res: Pair<Int, Array<out Any>> = when { val res: Pair<Int, Array<out Any>> = when {
luckyNumber == null -> R.string.home_lucky_number_no_info to emptyArray() luckyNumber == null -> R.string.home_lucky_number_no_info to emptyArray()

View File

@ -137,13 +137,13 @@ class HomeTimetableCard(
} }
private fun update() { launch { private fun update() { launch {
var checkedDays = 0
val deferred = async(Dispatchers.Default) { val deferred = async(Dispatchers.Default) {
// get the current bell-synced time // get the current bell-synced time
val now = syncedNow val now = syncedNow
// search for lessons to display // search for lessons to display
val timetableDate = Date.getToday() val timetableDate = Date.getToday()
var checkedDays = 0
lessons = allLessons.filter { lessons = allLessons.filter {
it.profileId == profile.id it.profileId == profile.id
&& it.displayDate == timetableDate && it.displayDate == timetableDate
@ -163,11 +163,14 @@ class HomeTimetableCard(
lessons = allLessons.filter { lessons = allLessons.filter {
it.profileId == profile.id it.profileId == profile.id
&& it.displayDate == timetableDate && it.displayDate == timetableDate
&& !(it.isCancelled && ignoreCancelled)
} }
//if (lessons.isEmpty() && timetableDate.weekDay <= 5) if (lessons.isEmpty())
// break break
/*lessons = lessons.filterNot {
it.isCancelled && ignoreCancelled
}*/
checkedDays++ checkedDays++
} }
@ -176,7 +179,7 @@ class HomeTimetableCard(
timetableDate = deferred.await() timetableDate = deferred.await()
if (lessons.isEmpty()) { if (lessons.isEmpty() && checkedDays < 7) {
// timetable is not downloaded yet // timetable is not downloaded yet
b.timetableLayout.visibility = View.GONE b.timetableLayout.visibility = View.GONE
b.noTimetableLayout.visibility = View.VISIBLE b.noTimetableLayout.visibility = View.VISIBLE
@ -189,7 +192,7 @@ class HomeTimetableCard(
b.noTimetableSync.onClick { b.noTimetableSync.onClick {
it.isEnabled = false it.isEnabled = false
EdziennikTask.syncProfile( EdziennikTask.syncProfile(
profileId = App.profileId, profileId = profile.id,
viewIds = listOf( viewIds = listOf(
MainActivity.DRAWER_ITEM_TIMETABLE to 0 MainActivity.DRAWER_ITEM_TIMETABLE to 0
), ),
@ -198,14 +201,18 @@ class HomeTimetableCard(
) )
).enqueue(activity) ).enqueue(activity)
} }
timetableDate = Date.getToday()
return@launch return@launch
} }
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { if (lessons.none { !it.isCancelled } || lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
// in next 7 days only NO_LESSONS is found // in next 7 days only NO_LESSONS is found
b.timetableLayout.visibility = View.GONE b.timetableLayout.visibility = View.GONE
b.noTimetableLayout.visibility = View.GONE b.noTimetableLayout.visibility = View.GONE
b.noLessonsLayout.visibility = View.VISIBLE b.noLessonsLayout.visibility = View.VISIBLE
timetableDate = timetableDate.weekStart timetableDate = timetableDate.weekStart
timetableDate = Date.getToday()
return@launch return@launch
} }

View File

@ -85,9 +85,7 @@ class HomeworkFragment : Fragment() {
b.viewPager.clearOnPageChangeListeners() b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) { override fun onPageScrollStateChanged(state: Int) {
if (b.refreshLayout != null) { b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
} }
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {

View File

@ -9,12 +9,10 @@ import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.HomeworkListBinding
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.databinding.HomeworkListBinding
import pl.szczodrzynski.edziennik.getInt import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.Themes
class HomeworkListFragment : Fragment() { class HomeworkListFragment : Fragment() {
@ -26,20 +24,15 @@ class HomeworkListFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
if (context == null) context ?: return null
return null
app = activity.application as App 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 = HomeworkListBinding.inflate(inflater) b = HomeworkListBinding.inflate(inflater)
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null // TODO check if app, activity, b can be null
if (app.profile == null || !isAdded) if (!isAdded)
return return
if (arguments != null) { if (arguments != null) {
@ -47,21 +40,21 @@ class HomeworkListFragment : Fragment() {
} }
val layoutManager = LinearLayoutManager(context) val layoutManager = LinearLayoutManager(context)
layoutManager.reverseLayout = true layoutManager.reverseLayout = homeworkDate == HomeworkDate.PAST
layoutManager.stackFromEnd = true layoutManager.stackFromEnd = homeworkDate == HomeworkDate.PAST
b.homeworkView.setHasFixedSize(true) b.homeworkView.setHasFixedSize(true)
b.homeworkView.layoutManager = layoutManager b.homeworkView.layoutManager = layoutManager
val filter = when(homeworkDate) { val filter = when(homeworkDate) {
HomeworkDate.CURRENT -> "eventDate > '" + Date.getToday().stringY_m_d + "'" HomeworkDate.CURRENT -> "eventDate >= '" + Date.getToday().stringY_m_d + "'"
else -> "eventDate <= '" + Date.getToday().stringY_m_d + "'" else -> "eventDate < '" + Date.getToday().stringY_m_d + "'"
} }
app.db.eventDao() app.db.eventDao()
.getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter) .getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter)
.observe(this, Observer { homeworkList -> .observe(this, Observer { homeworkList ->
if (app.profile == null || !isAdded) return@Observer if (!isAdded) return@Observer
if (homeworkList != null && homeworkList.size > 0) { if (homeworkList != null && homeworkList.size > 0) {
val adapter = HomeworkAdapter(context, homeworkList) val adapter = HomeworkAdapter(context, homeworkList)

View File

@ -4,19 +4,27 @@
package pl.szczodrzynski.edziennik.ui.modules.login package pl.szczodrzynski.edziennik.ui.modules.login
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN
import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_LIBRUS_JST import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_LIBRUS_JST
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.databinding.FragmentLoginLibrusJstBinding import pl.szczodrzynski.edziennik.databinding.FragmentLoginLibrusJstBinding
import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -47,12 +55,26 @@ class LoginLibrusJstFragment : Fragment(), CoroutineScope {
activity.lastError = null activity.lastError = null
startCoroutineTimer(delayMillis = 100) { startCoroutineTimer(delayMillis = 100) {
when (error.errorCode) { when (error.errorCode) {
ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN -> ERROR_LOGIN_LIBRUS_API_INVALID_LOGIN,
ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST ->
b.loginCodeLayout.error = getString(R.string.login_error_incorrect_code_or_pin) b.loginCodeLayout.error = getString(R.string.login_error_incorrect_code_or_pin)
} }
} }
} }
b.loginQrScan.setImageDrawable(IconicsDrawable(activity)
.icon(CommunityMaterial.Icon2.cmd_qrcode_scan)
.colorInt(Color.BLACK)
.sizeDp(72))
b.loginQrScan.onClick {
QrScannerDialog(activity, { code ->
b.loginCode.setText(code)
if (b.loginPin.requestFocus()) {
activity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
})
}
b.helpButton.onClick { nav.navigate(R.id.loginLibrusHelpFragment, null, LoginActivity.navOptions) } b.helpButton.onClick { nav.navigate(R.id.loginLibrusHelpFragment, null, LoginActivity.navOptions) }
b.backButton.onClick { nav.navigateUp() } b.backButton.onClick { nav.navigateUp() }

View File

@ -4,17 +4,25 @@
package pl.szczodrzynski.edziennik.ui.modules.login package pl.szczodrzynski.edziennik.ui.modules.login
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.databinding.FragmentLoginVulcanBinding import pl.szczodrzynski.edziennik.databinding.FragmentLoginVulcanBinding
import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog
import pl.szczodrzynski.edziennik.utils.Utils
import java.util.* import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -57,6 +65,26 @@ class LoginVulcanFragment : Fragment(), CoroutineScope {
} }
} }
b.loginQrScan.setImageDrawable(IconicsDrawable(activity)
.icon(CommunityMaterial.Icon2.cmd_qrcode_scan)
.colorInt(Color.BLACK)
.sizeDp(72))
b.loginQrScan.onClick {
QrScannerDialog(activity, { code ->
try {
val data = Utils.VulcanQrEncryptionUtils.decode(code)
"CERT#https?://.+?/([A-z]+)/mobile-api#([A-z0-9]+)#ENDCERT".toRegex().find(data)?.let {
b.loginToken.setText(it[2])
b.loginSymbol.setText(it[1])
if (b.loginPin.requestFocus()) {
activity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
}
}
catch (_: Exception) {}
})
}
b.helpButton.onClick { nav.navigate(R.id.loginVulcanHelpFragment, null, LoginActivity.navOptions) } b.helpButton.onClick { nav.navigate(R.id.loginVulcanHelpFragment, null, LoginActivity.navOptions) }
b.backButton.onClick { nav.navigateUp() } b.backButton.onClick { nav.navigateUp() }

View File

@ -41,6 +41,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding
import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.getStringFromFile import pl.szczodrzynski.edziennik.utils.Utils.getStringFromFile
@ -198,7 +199,7 @@ class MessageFragment : Fragment(), CoroutineScope {
} }
private fun showMessage() { private fun showMessage() {
b.body.text = MessagesUtils.htmlToSpannable(message.body ?: "") b.body.text = MessagesUtils.htmlToSpannable(activity, message.body ?: "")
b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM) b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM)
val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10) val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10)
@ -251,6 +252,9 @@ class MessageFragment : Fragment(), CoroutineScope {
showAttachments() showAttachments()
BetterLink.attach(b.subject)
BetterLink.attach(b.body)
b.progress.visibility = View.GONE b.progress.visibility = View.GONE
Anim.fadeIn(b.content, 200, null) Anim.fadeIn(b.content, 200, null)
MessagesFragment.pageSelection = min(message.type, 1) MessagesFragment.pageSelection = min(message.type, 1)

View File

@ -41,7 +41,7 @@ class MessagesAdapter(private val app: App, private val onItemClickListener: OnI
b.messageAttachmentImage.visibility = if (message.hasAttachments()) View.VISIBLE else View.GONE b.messageAttachmentImage.visibility = if (message.hasAttachments()) View.VISIBLE else View.GONE
val text = message.body?.substring(0, message.body!!.length.coerceAtMost(200)) ?: "" val text = message.body?.substring(0, message.body!!.length.coerceAtMost(200)) ?: ""
b.messageBody.text = MessagesUtils.htmlToSpannable(text) b.messageBody.text = MessagesUtils.htmlToSpannable(b.root.context, text)
if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT || message.seen) { if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT || message.seen) {
b.messageSender.setTextAppearance(b.messageSender.context, R.style.NavView_TextView_Small) b.messageSender.setTextAppearance(b.messageSender.context, R.style.NavView_TextView_Small)

View File

@ -380,7 +380,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
span.replace(0, 0, "\n\n") span.replace(0, 0, "\n\n")
subject = "Fwd: ${msg.subject}" subject = "Fwd: ${msg.subject}"
} }
body = MessagesUtils.htmlToSpannable(msg.body ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace("<br\\s?/?>".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.") body = MessagesUtils.htmlToSpannable(activity,msg.body ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace("<br\\s?/?>".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.")
} }
b.recipients.addTextWithChips(chipList) b.recipients.addTextWithChips(chipList)

View File

@ -1,8 +1,10 @@
package pl.szczodrzynski.edziennik.ui.modules.messages package pl.szczodrzynski.edziennik.ui.modules.messages
import android.graphics.* import android.content.Context
import android.os.Build import android.graphics.Bitmap
import android.text.Html import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.Spanned import android.text.Spanned
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
@ -12,8 +14,8 @@ import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.fixName import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.getNameInitials import pl.szczodrzynski.edziennik.getNameInitials
import pl.szczodrzynski.edziennik.utils.Colors import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.math.roundToInt import kotlin.math.roundToInt
object MessagesUtils { object MessagesUtils {
@ -180,32 +182,7 @@ object MessagesUtils {
class MessageInfo(var profileImage: Bitmap?, var profileName: String?) class MessageInfo(var profileImage: Bitmap?, var profileName: String?)
@JvmStatic @JvmStatic
fun htmlToSpannable(html: String): Spanned { fun htmlToSpannable(context: Context, html: String): Spanned {
val hexPattern = "(#[a-fA-F0-9]{6})" return BetterHtml.fromHtml(context, html)
val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})"
.toRegex(RegexOption.IGNORE_CASE)
var text = html
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
.replace("background-color: ?$hexPattern;".toRegex(), "")
colorRegex.findAll(text).forEach { result ->
val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach
val color = Color.parseColor(group.value)
val luminance = ColorUtils.calculateLuminance(color)
if (Themes.isDark && luminance <= 0.5) {
text = text.replaceRange(group.range, "#FFFFFF")
} else if (!Themes.isDark && luminance > 0.5) {
text = text.replaceRange(group.range, "#000000")
}
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(text, Html.FROM_HTML_MODE_COMPACT)
} else {
Html.fromHtml(text)
}
} }
} }

View File

@ -1104,7 +1104,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.icon(SzkolnyFont.Icon.szf_discord_outline) .icon(SzkolnyFont.Icon.szf_discord_outline)
.color(IconicsColor.colorInt(primaryTextOnPrimaryBg)) .color(IconicsColor.colorInt(primaryTextOnPrimaryBg))
.size(IconicsSize.dp(iconSizeDp))) .size(IconicsSize.dp(iconSizeDp)))
.setOnClickAction(ConvenienceBuilder.createWebsiteOnClickAction(activity, Uri.parse("https://discord.gg/n9e8pWr"))) .setOnClickAction(ConvenienceBuilder.createWebsiteOnClickAction(activity, Uri.parse("https://szkolny.eu/discord")))
.build()); .build());
items.add(new MaterialAboutActionItem.Builder() items.add(new MaterialAboutActionItem.Builder()

View File

@ -16,16 +16,17 @@ import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.google.android.material.datepicker.MaterialDatePicker
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2 import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
@ -169,16 +170,15 @@ class TimetableFragment : Fragment(), CoroutineScope {
.withIcon(SzkolnyFont.Icon.szf_calendar_today_outline) .withIcon(SzkolnyFont.Icon.szf_calendar_today_outline)
.withOnClickListener(View.OnClickListener { .withOnClickListener(View.OnClickListener {
activity.bottomSheet.close() activity.bottomSheet.close()
MaterialDatePicker.Builder val date = Date.getToday()
.datePicker() DatePickerDialog
.setSelection(Date.getToday().inMillis) .newInstance({ _, year, monthOfYear, dayOfMonth ->
.build() val dateSelected = Date(year, monthOfYear, dayOfMonth)
b.tabLayout.setCurrentItem(items.indexOfFirst { it == dateSelected }, true)
}, date.year, date.month, date.day)
.apply { .apply {
addOnPositiveButtonClickListener { dateInMillis -> accentColor = R.attr.colorPrimary.resolveAttr(this@TimetableFragment.activity)
val dateSelected = Date.fromMillis(dateInMillis) show(this@TimetableFragment.activity.supportFragmentManager, "DatePickerDialog")
b.tabLayout.setCurrentItem(items.indexOfFirst { it == dateSelected }, true)
}
show(this@TimetableFragment.activity.supportFragmentManager, "MaterialDatePicker")
} }
}), }),
BottomSheetPrimaryItem(true) BottomSheetPrimaryItem(true)

View File

@ -10,13 +10,14 @@ import android.util.AttributeSet
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.google.android.material.datepicker.MaterialDatePicker import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.observeOnce import pl.szczodrzynski.edziennik.observeOnce
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week import pl.szczodrzynski.edziennik.utils.models.Week
@ -176,23 +177,18 @@ class DateDropdown : TextInputDropDown {
} }
fun pickerDialog() { fun pickerDialog() {
MaterialDatePicker.Builder val date = getSelected() as? Date ?: Date.getToday()
.datePicker()
.setSelection( DatePickerDialog
if (selected?.tag is Date) .newInstance({ _, year, monthOfYear, dayOfMonth ->
(selected?.tag as Date).inMillis val dateSelected = Date(year, monthOfYear+1, dayOfMonth)
else selectDate(dateSelected)
Date.getToday().inMillis onDateSelected?.invoke(dateSelected, null)
) }, date.year, date.month-1, date.day)
.build()
.apply { .apply {
addOnPositiveButtonClickListener {
val dateSelected = Date.fromMillis(it)
selectDate(dateSelected)
onDateSelected?.invoke(dateSelected, null)
}
this@DateDropdown.activity ?: return@apply this@DateDropdown.activity ?: return@apply
show(this@DateDropdown.activity!!.supportFragmentManager, "MaterialDatePicker") accentColor = R.attr.colorPrimary.resolveAttr(this@DateDropdown.activity)
show(this@DateDropdown.activity!!.supportFragmentManager, "DatePickerDialog")
} }
} }

View File

@ -8,6 +8,7 @@ import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.wdullaer.materialdatetimepicker.time.TimePickerDialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
@ -174,19 +175,19 @@ class TimeDropdown : TextInputDropDown {
} }
fun pickerDialog() { fun pickerDialog() {
/*MaterialDatePicker.Builder val time = (getSelected() as? Pair<*, *>)?.first as? Time ?: Time.getNow()
.datePicker()
.setSelection((selectedId?.let { Date.fromValue(it.toInt()) } TimePickerDialog
?: Date.getToday()).inMillis) .newInstance({ _, hourOfDay, minute, second ->
.build() val timeSelected = Time(hourOfDay, minute, second)
selectTime(timeSelected)
onTimeSelected?.invoke(timeSelected, null, null)
}, time.hour, time.minute, 0, true)
.apply { .apply {
addOnPositiveButtonClickListener { this@TimeDropdown.activity ?: return@apply
val dateSelected = Date.fromMillis(it) accentColor = R.attr.colorPrimary.resolveAttr(this@TimeDropdown.activity)
selectDate(dateSelected) show(this@TimeDropdown.activity!!.supportFragmentManager, "TimePickerDialog")
} }
this@DateDropdown.activity ?: return@apply
show(this@DateDropdown.activity!!.supportFragmentManager, "MaterialDatePicker")
}*/
} }
fun selectTime(time: Time) { fun selectTime(time: Time) {
@ -208,12 +209,13 @@ class TimeDropdown : TextInputDropDown {
* Get the currently selected time. * Get the currently selected time.
* ### Returns: * ### Returns:
* - null if no valid time is selected * - null if no valid time is selected
* - 0L if 'all day' is selected
* - a [Pair] of [Time] and [Time]? - the selected time object, if [displayMode] == [DISPLAY_LESSONS] or [showCustomTime] * - a [Pair] of [Time] and [Time]? - the selected time object, if [displayMode] == [DISPLAY_LESSONS] or [showCustomTime]
* - [LessonRange] - the selected lesson range object, if [displayMode] == [DISPLAY_LESSON_RANGES] * - [LessonRange] - the selected lesson range object, if [displayMode] == [DISPLAY_LESSON_RANGES]
*/ */
fun getSelected(): Any? { fun getSelected(): Any? {
return when (val tag = selected?.tag) { return when (val tag = selected?.tag) {
0L -> null 0L -> 0L
is LessonFull -> is LessonFull ->
if (tag.displayStartTime != null) if (tag.displayStartTime != null)
tag.displayStartTime!! to tag.displayEndTime tag.displayStartTime!! to tag.displayEndTime

View File

@ -116,6 +116,8 @@ class WebPushFragment : Fragment(), CoroutineScope {
} }
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
app.config.sync.webPushEnabled = browsers.isNotEmpty()
if (browsers.isNotEmpty()) { if (browsers.isNotEmpty()) {
b.browsersView.visibility = View.VISIBLE b.browsersView.visibility = View.VISIBLE
b.browsersNoData.visibility = View.GONE b.browsersNoData.visibility = View.GONE

View File

@ -226,11 +226,14 @@ class WidgetTimetableProvider : AppWidgetProvider() {
lessons = lessonList.filter { lessons = lessonList.filter {
it.profileId == profile.id it.profileId == profile.id
&& it.displayDate == timetableDate && it.displayDate == timetableDate
&& !(it.isCancelled && ignoreCancelled)
} }
//if (lessons.isEmpty() && timetableDate.weekDay <= 5) if (lessons.isEmpty())
// break break
/*lessons = lessons.filterNot {
it.isCancelled && ignoreCancelled
}*/
checkedDays++ checkedDays++
} }
@ -248,35 +251,39 @@ class WidgetTimetableProvider : AppWidgetProvider() {
models.add(separator) models.add(separator)
} }
views.setViewVisibility(R.id.widgetTimetableListView, View.VISIBLE)
views.setViewVisibility(R.id.widgetTimetableNoTimetable, View.GONE)
views.setViewVisibility(R.id.widgetTimetableNoLessons, View.GONE)
// set the displayingDate to show in the header // set the displayingDate to show in the header
if (!unified) { if (!unified) {
if (lessons.isNotEmpty()) if (lessons.isNotEmpty())
displayingDate = timetableDate displayingDate = timetableDate
profileId = profile.id profileId = profile.id
if (lessons.isEmpty()) { if (lessons.isEmpty() && checkedDays < 7) {
views.setViewVisibility(R.id.widgetTimetableListView, View.GONE) views.setViewVisibility(R.id.widgetTimetableListView, View.GONE)
views.setViewVisibility(R.id.widgetTimetableNoTimetable, View.VISIBLE) views.setViewVisibility(R.id.widgetTimetableNoTimetable, View.VISIBLE)
} }
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { else if (lessons.none { !it.isCancelled } || lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
views.setViewVisibility(R.id.widgetTimetableListView, View.GONE) views.setViewVisibility(R.id.widgetTimetableListView, View.GONE)
views.setViewVisibility(R.id.widgetTimetableNoLessons, View.VISIBLE) views.setViewVisibility(R.id.widgetTimetableNoLessons, View.VISIBLE)
} }
} }
else { else {
if (lessons.isEmpty()) { if (lessons.isEmpty() && checkedDays < 7) {
val separator = ItemWidgetTimetableModel() val separator = ItemWidgetTimetableModel()
separator.profileId = profile.id separator.profileId = profile.id
separator.bigStyle = widgetConfig.bigStyle separator.bigStyle = widgetConfig.bigStyle
separator.darkTheme = widgetConfig.darkTheme separator.darkTheme = widgetConfig.darkTheme
separator.isNoTimetableItem = true; separator.isNoTimetableItem = true
models.add(separator) models.add(separator)
} }
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { if (lessons.none { !it.isCancelled } || lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
val separator = ItemWidgetTimetableModel() val separator = ItemWidgetTimetableModel()
separator.profileId = profile.id separator.profileId = profile.id
separator.bigStyle = widgetConfig.bigStyle separator.bigStyle = widgetConfig.bigStyle
separator.darkTheme = widgetConfig.darkTheme separator.darkTheme = widgetConfig.darkTheme
separator.isNoLessonsItem = true; separator.isNoLessonsItem = true
models.add(separator) models.add(separator)
} }
} }

View File

@ -0,0 +1,200 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-18.
*/
@file:Suppress("INACCESSIBLE_TYPE")
package pl.szczodrzynski.edziennik.utils
import android.annotation.SuppressLint
import android.graphics.Color
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.URLSpan
import android.text.util.Linkify
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuPopupHelper
import pl.szczodrzynski.edziennik.Intent
import pl.szczodrzynski.edziennik.copyToClipboard
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.getTextPosition
import pl.szczodrzynski.edziennik.utils.models.Date
object BetterLink {
@SuppressLint("RestrictedApi")
fun attach(textView: TextView, onActionSelected: (() -> Unit)? = null) {
textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES
BetterLinkMovementMethod.linkify(textView.autoLinkMask, textView).setOnLinkClickListener { v, span: BetterLinkMovementMethod.ClickableSpanWithText ->
val url = span.text()
val c = v.context
val s = v.text as Spanned
val start = s.getSpanStart(span.span())
val end = s.getSpanEnd(span.span())
val parent = v.rootView.findViewById<ViewGroup>(android.R.id.content)
val parentLocation = intArrayOf(0, 0)
parent.getLocationOnScreen(parentLocation)
val rect = textView.getTextPosition(start..end)
val view = View(c)
view.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height())
view.setBackgroundColor(Color.TRANSPARENT)
parent.addView(view)
view.x = rect.left.toFloat() - parentLocation[0]
view.y = rect.top.toFloat() - parentLocation[1]
val menu = MenuBuilder(c)
val helper = MenuPopupHelper(c, menu, view)
val popup = helper.popup
var menuTitle = url.substringAfter(":")
var date: Date? = null
var urlItem: MenuItem? = null
var createEventItem: MenuItem? = null
//var goToTimetableItem: MenuItem? = null // TODO 2020-03-19: implement this
var mailItem: MenuItem? = null
var copyItem: MenuItem? = null
when {
url.startsWith("mailto:") -> {
mailItem = menu.add(1, 20, 2, "Napisz e-mail")
}
url.startsWith("dateYmd:") -> {
createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie")
//goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji")
date = parseDateYmd(menuTitle)
}
url.startsWith("dateDmy:") -> {
createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie")
//goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji")
date = parseDateDmy(menuTitle)
}
url.startsWith("dateAbs:") -> {
createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie")
//goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji")
date = parseDateAbs(menuTitle)
}
url.startsWith("dateRel:") -> {
createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie")
//goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji")
date = parseDateRel(menuTitle)
}
else -> {
urlItem = menu.add(1, 1, 2, "Otwórz w przeglądarce")
menuTitle = url
}
}
copyItem = menu.add(1, 1000, 1000, "Kopiuj tekst")
helper.setOnDismissListener { parent.removeView(view) }
urlItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true }
mailItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true }
copyItem?.setOnMenuItemClickListener { menuTitle.copyToClipboard(c); true }
createEventItem?.setOnMenuItemClickListener {
onActionSelected?.invoke()
val intent = Intent(
android.content.Intent.ACTION_MAIN,
"action" to "createManualEvent",
"eventDate" to date?.stringY_m_d
)
c.sendBroadcast(intent)
true
}
menu::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let {
it.isAccessible = true
it.invoke(menu, menuTitle)
}
popup::class.java.getDeclaredField("mShowTitle").let {
it.isAccessible = true
it.set(popup, true)
}
helper::class.java.getDeclaredMethod("showPopup", Int::class.java, Int::class.java, Boolean::class.java, Boolean::class.java).let {
it.isAccessible = true
it.invoke(helper, 0, 0, false, true)
}
true
}
val spanned = textView.text as? Spannable ?: {
SpannableString(textView.text)
}()
Regexes.LINKIFY_DATE_YMD.findAll(textView.text).forEach { match ->
val span = URLSpan("dateYmd:" + match.value)
spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
Regexes.LINKIFY_DATE_DMY.findAll(textView.text).forEach { match ->
val span = URLSpan("dateDmy:" + match.value)
spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
Regexes.LINKIFY_DATE_ABSOLUTE.findAll(textView.text).forEach { match ->
val span = URLSpan("dateAbs:" + match.value)
spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
Regexes.LINKIFY_DATE_RELATIVE.findAll(textView.text).forEach { match ->
val span = URLSpan("dateRel:" + match.value)
spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
//Linkify.addLinks(textView, LINKIFY_DATE_ABSOLUTE.toPattern(), "dateAbs:")
//Linkify.addLinks(textView, LINKIFY_DATE_RELATIVE.toPattern(), "dateRel:")
}
private val monthNames = listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru")
private fun parseDateYmd(text: String): Date? {
return Regexes.LINKIFY_DATE_YMD.find(text)?.let {
val year = it[1].toIntOrNull() ?: Date.getToday().year
val month = it[2].toIntOrNull() ?: 1
val day = it[3].toIntOrNull() ?: 1
Date(year, month, day)
}
}
private fun parseDateDmy(text: String): Date? {
return Regexes.LINKIFY_DATE_DMY.find(text)?.let {
val day = it[1].toIntOrNull() ?: 1
val month = it[2].toIntOrNull() ?: 1
var year = it[3].toIntOrNull() ?: Date.getToday().year
if (year < 50)
year += 2000
Date(year, month, day)
}
}
private fun parseDateAbs(text: String): Date? {
return Regexes.LINKIFY_DATE_ABSOLUTE.find(text)?.let {
val year = it[3].toIntOrNull() ?: Date.getToday().year
val month = monthNames.indexOf(it[2]) + 1
val day = it[1].toIntOrNull() ?: 1
Date(year, month.coerceAtLeast(1), day)
}
}
private fun parseDateRel(text: String): Date? {
return Regexes.LINKIFY_DATE_RELATIVE.find(text)?.let {
val date = Date.getToday()
val amount = it[1].toIntOrNull() ?: 1
val unitInDays = when (it[2]) {
"dni", "dzień" -> 1
"tydzień", "tygodnie" -> 7
else -> 1
}
date.stepForward(0, 0, amount*unitInDays)
}
}
}

View File

@ -0,0 +1,458 @@
package pl.szczodrzynski.edziennik.utils;
import android.app.Activity;
import android.graphics.RectF;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.TextView;
import pl.szczodrzynski.edziennik.R;
/**
* Handles URL clicks on TextViews. Unlike the default implementation, this:
* <p>
* <ul>
* <li>Reliably applies a highlight color on links when they're touched.</li>
* <li>Let's you handle single and long clicks on URLs</li>
* <li>Correctly identifies focused URLs (Unlike the default implementation where a click is registered even if it's
* made outside of the URL's bounds if there is no more text in that direction.)</li>
* </ul>
*/
public class BetterLinkMovementMethod extends LinkMovementMethod {
private static BetterLinkMovementMethod singleInstance;
private static final int LINKIFY_NONE = -2;
private OnLinkClickListener onLinkClickListener;
private OnLinkLongClickListener onLinkLongClickListener;
private final RectF touchedLineBounds = new RectF();
private boolean isUrlHighlighted;
private ClickableSpan clickableSpanUnderTouchOnActionDown;
private int activeTextViewHashcode;
private LongPressTimer ongoingLongPressTimer;
private boolean wasLongPressRegistered;
public interface OnLinkClickListener {
/**
* @param textView The TextView on which a click was registered.
* @param span The clicked URL span.
* @return True if this click was handled. False to let Android handle the URL.
*/
boolean onClick(TextView textView, ClickableSpanWithText span);
}
public interface OnLinkLongClickListener {
/**
* @param textView The TextView on which a long-click was registered.
* @param span The long-clicked URL span.
* @return True if this long-click was handled. False to let Android handle the URL (as a short-click).
*/
boolean onLongClick(TextView textView, ClickableSpanWithText span);
}
/**
* Return a new instance of BetterLinkMovementMethod.
*/
public static BetterLinkMovementMethod newInstance() {
return new BetterLinkMovementMethod();
}
/**
* @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES},
* {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}.
* @param textViews The TextViews on which a {@link BetterLinkMovementMethod} should be registered.
* @return The registered {@link BetterLinkMovementMethod} on the TextViews.
*/
public static BetterLinkMovementMethod linkify(int linkifyMask, TextView... textViews) {
BetterLinkMovementMethod movementMethod = newInstance();
for (TextView textView : textViews) {
addLinks(linkifyMask, movementMethod, textView);
}
return movementMethod;
}
/**
* Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links.
*
* @param textViews The TextViews on which a {@link BetterLinkMovementMethod} should be registered.
* @return The registered {@link BetterLinkMovementMethod} on the TextViews.
*/
public static BetterLinkMovementMethod linkifyHtml(TextView... textViews) {
return linkify(LINKIFY_NONE, textViews);
}
/**
* Recursively register a {@link BetterLinkMovementMethod} on every TextView inside a layout.
*
* @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES},
* {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}.
* @return The registered {@link BetterLinkMovementMethod} on the TextViews.
*/
public static BetterLinkMovementMethod linkify(int linkifyMask, ViewGroup viewGroup) {
BetterLinkMovementMethod movementMethod = newInstance();
rAddLinks(linkifyMask, viewGroup, movementMethod);
return movementMethod;
}
/**
* Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links.
*
* @return The registered {@link BetterLinkMovementMethod} on the TextViews.
*/
@SuppressWarnings("unused")
public static BetterLinkMovementMethod linkifyHtml(ViewGroup viewGroup) {
return linkify(LINKIFY_NONE, viewGroup);
}
/**
* Recursively register a {@link BetterLinkMovementMethod} on every TextView inside a layout.
*
* @param linkifyMask One of {@link Linkify#ALL}, {@link Linkify#PHONE_NUMBERS}, {@link Linkify#MAP_ADDRESSES},
* {@link Linkify#WEB_URLS} and {@link Linkify#EMAIL_ADDRESSES}.
* @return The registered {@link BetterLinkMovementMethod} on the TextViews.
*/
public static BetterLinkMovementMethod linkify(int linkifyMask, Activity activity) {
// Find the layout passed to setContentView().
ViewGroup activityLayout = ((ViewGroup) ((ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT)).getChildAt(0));
BetterLinkMovementMethod movementMethod = newInstance();
rAddLinks(linkifyMask, activityLayout, movementMethod);
return movementMethod;
}
/**
* Like {@link #linkify(int, TextView...)}, but can be used for TextViews with HTML links.
*
* @return The registered {@link BetterLinkMovementMethod} on the TextViews.
*/
@SuppressWarnings("unused")
public static BetterLinkMovementMethod linkifyHtml(Activity activity) {
return linkify(LINKIFY_NONE, activity);
}
/**
* Get a static instance of BetterLinkMovementMethod. Do note that registering a click listener on the returned
* instance is not supported because it will potentially be shared on multiple TextViews.
*/
@SuppressWarnings("unused")
public static BetterLinkMovementMethod getInstance() {
if (singleInstance == null) {
singleInstance = new BetterLinkMovementMethod();
}
return singleInstance;
}
protected BetterLinkMovementMethod() {
}
/**
* Set a listener that will get called whenever any link is clicked on the TextView.
*/
public BetterLinkMovementMethod setOnLinkClickListener(OnLinkClickListener clickListener) {
if (this == singleInstance) {
throw new UnsupportedOperationException("Setting a click listener on the instance returned by getInstance() is not supported to avoid memory " +
"leaks. Please use newInstance() or any of the linkify() methods instead.");
}
this.onLinkClickListener = clickListener;
return this;
}
/**
* Set a listener that will get called whenever any link is clicked on the TextView.
*/
public BetterLinkMovementMethod setOnLinkLongClickListener(OnLinkLongClickListener longClickListener) {
if (this == singleInstance) {
throw new UnsupportedOperationException("Setting a long-click listener on the instance returned by getInstance() is not supported to avoid " +
"memory leaks. Please use newInstance() or any of the linkify() methods instead.");
}
this.onLinkLongClickListener = longClickListener;
return this;
}
// ======== PUBLIC APIs END ======== //
private static void rAddLinks(int linkifyMask, ViewGroup viewGroup, BetterLinkMovementMethod movementMethod) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup) {
// Recursively find child TextViews.
rAddLinks(linkifyMask, ((ViewGroup) child), movementMethod);
} else if (child instanceof TextView) {
TextView textView = (TextView) child;
addLinks(linkifyMask, movementMethod, textView);
}
}
}
private static void addLinks(int linkifyMask, BetterLinkMovementMethod movementMethod, TextView textView) {
textView.setMovementMethod(movementMethod);
if (linkifyMask != LINKIFY_NONE) {
Linkify.addLinks(textView, linkifyMask);
}
}
@Override
public boolean onTouchEvent(final TextView textView, Spannable text, MotionEvent event) {
if (activeTextViewHashcode != textView.hashCode()) {
// Bug workaround: TextView stops calling onTouchEvent() once any URL is highlighted.
// A hacky solution is to reset any "autoLink" property set in XML. But we also want
// to do this once per TextView.
activeTextViewHashcode = textView.hashCode();
textView.setAutoLinkMask(0);
}
final ClickableSpan clickableSpanUnderTouch = findClickableSpanUnderTouch(textView, text, event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
clickableSpanUnderTouchOnActionDown = clickableSpanUnderTouch;
}
final boolean touchStartedOverAClickableSpan = clickableSpanUnderTouchOnActionDown != null;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (clickableSpanUnderTouch != null) {
highlightUrl(textView, clickableSpanUnderTouch, text);
}
if (touchStartedOverAClickableSpan && onLinkLongClickListener != null) {
LongPressTimer.OnTimerReachedListener longClickListener = new LongPressTimer.OnTimerReachedListener() {
@Override
public void onTimerReached() {
wasLongPressRegistered = true;
textView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
removeUrlHighlightColor(textView);
dispatchUrlLongClick(textView, clickableSpanUnderTouch);
}
};
startTimerForRegisteringLongClick(textView, longClickListener);
}
return touchStartedOverAClickableSpan;
case MotionEvent.ACTION_UP:
// Register a click only if the touch started and ended on the same URL.
if (!wasLongPressRegistered && touchStartedOverAClickableSpan && clickableSpanUnderTouch == clickableSpanUnderTouchOnActionDown) {
dispatchUrlClick(textView, clickableSpanUnderTouch);
}
cleanupOnTouchUp(textView);
// Consume this event even if we could not find any spans to avoid letting Android handle this event.
// Android's TextView implementation has a bug where links get clicked even when there is no more text
// next to the link and the touch lies outside its bounds in the same direction.
return touchStartedOverAClickableSpan;
case MotionEvent.ACTION_CANCEL:
cleanupOnTouchUp(textView);
return false;
case MotionEvent.ACTION_MOVE:
// Stop listening for a long-press as soon as the user wanders off to unknown lands.
if (clickableSpanUnderTouch != clickableSpanUnderTouchOnActionDown) {
removeLongPressCallback(textView);
}
if (!wasLongPressRegistered) {
// Toggle highlight.
if (clickableSpanUnderTouch != null) {
highlightUrl(textView, clickableSpanUnderTouch, text);
} else {
removeUrlHighlightColor(textView);
}
}
return touchStartedOverAClickableSpan;
default:
return false;
}
}
private void cleanupOnTouchUp(TextView textView) {
wasLongPressRegistered = false;
clickableSpanUnderTouchOnActionDown = null;
removeUrlHighlightColor(textView);
removeLongPressCallback(textView);
}
/**
* Determines the touched location inside the TextView's text and returns the ClickableSpan found under it (if any).
*
* @return The touched ClickableSpan or null.
*/
protected ClickableSpan findClickableSpanUnderTouch(TextView textView, Spannable text, MotionEvent event) {
// So we need to find the location in text where touch was made, regardless of whether the TextView
// has scrollable text. That is, not the entire text is currently visible.
int touchX = (int) event.getX();
int touchY = (int) event.getY();
// Ignore padding.
touchX -= textView.getTotalPaddingLeft();
touchY -= textView.getTotalPaddingTop();
// Account for scrollable text.
touchX += textView.getScrollX();
touchY += textView.getScrollY();
final Layout layout = textView.getLayout();
final int touchedLine = layout.getLineForVertical(touchY);
final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX);
touchedLineBounds.left = layout.getLineLeft(touchedLine);
touchedLineBounds.top = layout.getLineTop(touchedLine);
touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left;
touchedLineBounds.bottom = layout.getLineBottom(touchedLine);
if (touchedLineBounds.contains(touchX, touchY)) {
// Find a ClickableSpan that lies under the touched area.
final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class);
for (final Object span : spans) {
if (span instanceof ClickableSpan) {
return (ClickableSpan) span;
}
}
// No ClickableSpan found under the touched location.
return null;
} else {
// Touch lies outside the line's horizontal bounds where no spans should exist.
return null;
}
}
/**
* Adds a background color span at <var>clickableSpan</var>'s location.
*/
protected void highlightUrl(TextView textView, ClickableSpan clickableSpan, Spannable text) {
if (isUrlHighlighted) {
return;
}
isUrlHighlighted = true;
int spanStart = text.getSpanStart(clickableSpan);
int spanEnd = text.getSpanEnd(clickableSpan);
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(textView.getHighlightColor());
text.setSpan(highlightSpan, spanStart, spanEnd, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
textView.setTag(R.id.bettermovementmethod_highlight_background_span, highlightSpan);
Selection.setSelection(text, spanStart, spanEnd);
}
/**
* Removes the highlight color under the Url.
*/
protected void removeUrlHighlightColor(TextView textView) {
if (!isUrlHighlighted) {
return;
}
isUrlHighlighted = false;
Spannable text = (Spannable) textView.getText();
BackgroundColorSpan highlightSpan = (BackgroundColorSpan) textView.getTag(R.id.bettermovementmethod_highlight_background_span);
text.removeSpan(highlightSpan);
Selection.removeSelection(text);
}
protected void startTimerForRegisteringLongClick(TextView textView, LongPressTimer.OnTimerReachedListener longClickListener) {
ongoingLongPressTimer = new LongPressTimer();
ongoingLongPressTimer.setOnTimerReachedListener(longClickListener);
textView.postDelayed(ongoingLongPressTimer, ViewConfiguration.getLongPressTimeout());
}
/**
* Remove the long-press detection timer.
*/
protected void removeLongPressCallback(TextView textView) {
if (ongoingLongPressTimer != null) {
textView.removeCallbacks(ongoingLongPressTimer);
ongoingLongPressTimer = null;
}
}
protected void dispatchUrlClick(TextView textView, ClickableSpan clickableSpan) {
ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan);
boolean handled = onLinkClickListener != null && onLinkClickListener.onClick(textView, clickableSpanWithText);
if (!handled) {
// Let Android handle this click.
clickableSpanWithText.span().onClick(textView);
}
}
protected void dispatchUrlLongClick(TextView textView, ClickableSpan clickableSpan) {
ClickableSpanWithText clickableSpanWithText = ClickableSpanWithText.ofSpan(textView, clickableSpan);
boolean handled = onLinkLongClickListener != null && onLinkLongClickListener.onLongClick(textView, clickableSpanWithText);
if (!handled) {
// Let Android handle this long click as a short-click.
clickableSpanWithText.span().onClick(textView);
}
}
protected static final class LongPressTimer implements Runnable {
private OnTimerReachedListener onTimerReachedListener;
protected interface OnTimerReachedListener {
void onTimerReached();
}
@Override
public void run() {
onTimerReachedListener.onTimerReached();
}
public void setOnTimerReachedListener(OnTimerReachedListener listener) {
onTimerReachedListener = listener;
}
}
/**
* A wrapper to support all {@link ClickableSpan}s that may or may not provide URLs.
*/
protected static class ClickableSpanWithText {
private ClickableSpan span;
private String text;
protected static ClickableSpanWithText ofSpan(TextView textView, ClickableSpan span) {
Spanned s = (Spanned) textView.getText();
String text;
if (span instanceof URLSpan) {
text = ((URLSpan) span).getURL();
} else {
int start = s.getSpanStart(span);
int end = s.getSpanEnd(span);
text = s.subSequence(start, end).toString();
}
return new ClickableSpanWithText(span, text);
}
protected ClickableSpanWithText(ClickableSpan span, String text) {
this.span = span;
this.text = text;
}
protected ClickableSpan span() {
return span;
}
protected String text() {
return text;
}
}
}

View File

@ -1,14 +1,9 @@
package pl.szczodrzynski.edziennik.utils package pl.szczodrzynski.edziennik.utils
import android.content.Context import android.content.Context
import android.graphics.Color
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.core.graphics.ColorUtils
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.navlib.getColorFromAttr import pl.szczodrzynski.navlib.getColorFromAttr
object Themes { object Themes {
@ -49,7 +44,7 @@ object Themes {
val appThemeNoDisplay: Int val appThemeNoDisplay: Int
get() = if (theme.isDark) R.style.AppThemeDark_NoDisplay else R.style.AppTheme_NoDisplay get() = if (theme.isDark) R.style.AppTheme_Dark_NoDisplay else R.style.AppTheme_Light_NoDisplay
val appTheme: Int val appTheme: Int

View File

@ -0,0 +1,89 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-17.
*/
package pl.szczodrzynski.edziennik.utils.html
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.BulletSpan
import androidx.core.graphics.ColorUtils
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.navlib.blendColors
object BetterHtml {
@JvmStatic
fun fromHtml(context: Context, html: String): Spanned {
val hexPattern = "(#[a-fA-F0-9]{6})"
val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})"
.toRegex(RegexOption.IGNORE_CASE)
var text = html
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
.replace("background-color: ?$hexPattern;".toRegex(), "")
val colorBackground = android.R.attr.colorBackground.resolveAttr(context)
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff
colorRegex.findAll(text).forEach { result ->
val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach
val color = Color.parseColor(group.value)
var newColor = 0xff000000.toInt() or color
var blendAmount = 1
var numIterations = 0
while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) {
blendAmount += 2
newColor = blendColors(color, blendAmount shl 24 or textColorPrimary)
numIterations++
}
text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16))
}
/*val olRegex = """<ol>(.+?)</\s*?ol>"""
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
olRegex.findAll(text).forEach {
text.replaceRange(
it.range,
text.slice(it.range).replace("li>", "_li>")
)
}*/
@Suppress("DEPRECATION")
val htmlSpannable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(
text,
Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
null,
LiTagHandler()
)
} else {
Html.fromHtml(text, null, LiTagHandler())
}
val spannableBuilder = SpannableStringBuilder(htmlSpannable)
val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java)
bulletSpans.forEach {
val start = spannableBuilder.getSpanStart(it)
val end = spannableBuilder.getSpanEnd(it)
spannableBuilder.removeSpan(it)
spannableBuilder.setSpan(
ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp),
start,
end,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
}
return spannableBuilder
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-17.
*/
/**
* https://github.com/davidbilik/bullet-span-sample/blob/master/app/src/main/java/cz/davidbilik/bulletsample/ImprovedBulletSpan.kt
*/
package pl.szczodrzynski.edziennik.utils.html
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Path.Direction
import android.text.Layout
import android.text.Spanned
import android.text.style.LeadingMarginSpan
/**
* Copy of [android.text.style.BulletSpan] from android SDK 28 with removed internal code
*/
class ImprovedBulletSpan(
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
val startWidth: Int = STANDARD_GAP_WIDTH,
val gapWidth: Int = STANDARD_GAP_WIDTH,
val color: Int = STANDARD_COLOR
) : LeadingMarginSpan {
companion object {
// Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
private const val STANDARD_BULLET_RADIUS = 4
private const val STANDARD_GAP_WIDTH = 2
private const val STANDARD_COLOR = 0
}
private var mBulletPath: Path? = null
override fun getLeadingMargin(first: Boolean): Int {
return startWidth + 2 * bulletRadius + gapWidth
}
override fun drawLeadingMargin(
canvas: Canvas, paint: Paint, x: Int, dir: Int,
top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int,
first: Boolean,
layout: Layout?
) {
if ((text as Spanned).getSpanStart(this) == start) {
val style = paint.style
paint.style = Paint.Style.FILL
val yPosition = if (layout != null) {
val line = layout.getLineForOffset(start)
layout.getLineBaseline(line).toFloat() - bulletRadius * 2f
} else {
(top + bottom) / 2f
}
val xPosition = startWidth + (x + dir * bulletRadius).toFloat()
if (canvas.isHardwareAccelerated) {
if (mBulletPath == null) {
mBulletPath = Path()
mBulletPath!!.addCircle(0.0f, 0.0f, bulletRadius.toFloat(), Direction.CW)
}
canvas.save()
canvas.translate(xPosition, yPosition)
canvas.drawPath(mBulletPath!!, paint)
canvas.restore()
} else {
canvas.drawCircle(xPosition, yPosition, bulletRadius.toFloat(), paint)
}
paint.style = style
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-17.
*/
/**
* https://github.com/davidbilik/bullet-span-sample/blob/master/app/src/main/java/cz/davidbilik/bulletsample/LiTagHandler.kt
*/
package pl.szczodrzynski.edziennik.utils.html
import android.text.Editable
import android.text.Html
import android.text.Spannable
import android.text.Spanned
import android.text.style.BulletSpan
import org.xml.sax.XMLReader
/**
* [Html.TagHandler] implementation that processes <ul> and <li> tags and creates bullets.
*
* Note: This class is only applied on SDK < 25 and processes only one-level list, nested lists do not work.
*/
class LiTagHandler : Html.TagHandler {
/**
* Helper marker class. Idea stolen from [Html.fromHtml] implementation
*/
class Bullet
override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
if (tag == "li" && opening) {
output.setSpan(Bullet(), output.length, output.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
if (tag == "li" && !opening) {
output.append("\n")
val lastMark = output.getSpans(0, output.length, Bullet::class.java).lastOrNull()
lastMark?.let {
val start = output.getSpanStart(it)
output.removeSpan(it)
if (start != output.length) {
output.setSpan(BulletSpan(), start, output.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
}
}
}
}

View File

@ -53,6 +53,8 @@ class GradesManager(val app: App) : CoroutineScope {
get() = app.config.forProfile().grades.plusValue get() = app.config.forProfile().grades.plusValue
val minusValue val minusValue
get() = app.config.forProfile().grades.minusValue get() = app.config.forProfile().grades.minusValue
val dontCountEnabled
get() = app.config.forProfile().grades.dontCountEnabled
val dontCountGrades val dontCountGrades
get() = app.config.forProfile().grades.dontCountGrades get() = app.config.forProfile().grades.dontCountGrades
val hideImproved val hideImproved
@ -100,8 +102,10 @@ class GradesManager(val app: App) : CoroutineScope {
return grade.value return grade.value
} }
fun getGradeWeight(dontCountGrades: List<String>, grade: Grade): Float { fun getGradeWeight(dontCountEnabled: Boolean, dontCountGrades: List<String>, grade: Grade): Float {
if (grade.name.toLowerCase() in dontCountGrades) if (!dontCountEnabled)
return grade.weight
if (grade.name.toLowerCase().trim() in dontCountGrades)
return 0f return 0f
return grade.weight return grade.weight
} }

View File

@ -1,150 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/feedbackFragment"
xmlns:tools="http://schemas.android.com/tools"> android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.modules.feedback.FeedbackActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:minHeight="?attr/actionBarSize" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/targetDeviceLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/targetDeviceDropDown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint=""
android:paddingEnd="6.0dip"
android:paddingRight="6.0dip" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/faqText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:autoLink="all"
android:background="?selectableItemBackground"
android:text="@string/feedback_faq"
android:textSize="20sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/faqButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp"
android:text="@string/feedback_faq_button"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:id="@+id/inputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/feedback_title"
android:textSize="20sp" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:hint="@string/feedback_ask_a_question"
app:errorEnabled="true"
app:hintAnimationEnabled="true"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textShortMessage|textMultiLine|textPersonName|textCapSentences|textAutoComplete"
android:maxLines="5"
android:singleLine="false" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/sendButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/feedback_send" />
</LinearLayout>
<LinearLayout
android:id="@+id/chatLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:text="@string/feedback_history"
android:textSize="20sp" />
<com.github.bassaer.chatmessageview.view.ChatView
android:id="@+id/chat_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -84,16 +84,32 @@
android:background="@drawable/divider"/> android:background="@drawable/divider"/>
<CheckBox <CheckBox
android:id="@+id/dontCountZeroToAverage" android:id="@+id/dontCountGrades"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="0dp" android:minHeight="0dp"
android:text="@string/settings_register_dont_count_zero_text"/> android:text="@string/grades_config_dont_count_grades"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:hint="@string/grades_config_dont_count_hint"
app:placeholderText="@string/grades_config_dont_count_placeholder"
android:enabled="@{dontCountGrades.checked}">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/dontCountGradesText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="nb, 0, +, -, bz"/>
</com.google.android.material.textfield.TextInputLayout>
<CheckBox <CheckBox
android:id="@+id/hideImproved" android:id="@+id/hideImproved"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:minHeight="32dp" android:minHeight="32dp"
android:text="@string/grades_config_dont_show_improved"/> android:text="@string/grades_config_dont_show_improved"/>

View File

@ -141,12 +141,14 @@
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:textAppearance="@style/NavView.TextView.Helper" android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_event_details_topic"/> android:text="@string/dialog_event_details_topic"/>
<TextView <TextView
android:id="@+id/topic"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@{event.topic}" android:text="@{event.topic}"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Medium" android:textAppearance="@style/NavView.TextView.Medium"
android:textIsSelectable="true"
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia." /> tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia." />
<View <View

View File

@ -8,6 +8,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<ScrollView <ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -43,6 +44,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:enabled="false" android:enabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
tools:text="8:10 - język polski"/> tools:text="8:10 - język polski"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -58,6 +61,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:enabled="false" android:enabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
tools:text="2b3T" /> tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -94,6 +99,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:enabled="false" android:enabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
tools:text="2b3T" /> tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -123,6 +130,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textLongMessage|textMultiLine|textImeMultiLine" android:inputType="textLongMessage|textMultiLine|textImeMultiLine"
android:minLines="2" android:minLines="2"
android:focusable="true"
android:focusableInTouchMode="true"
tools:text="2b3T" /> tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View File

@ -3,18 +3,70 @@
~ Copyright (c) Kuba Szczodrzyński 2019-11-23. ~ Copyright (c) Kuba Szczodrzyński 2019-11-23.
--> -->
<layout xmlns:tools="http://schemas.android.com/tools" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:tools="http://schemas.android.com/tools">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator <pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout" android:id="@+id/refreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.core.widget.NestedScrollView
android:id="@+id/list" android:id="@+id/scrollView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
tools:listitem="@layout/card_home" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:itemCount="1"
tools:listitem="@layout/card_home_timetable" />
<View
android:id="@+id/configHintDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginVertical="8dp"
android:background="@drawable/divider" />
<LinearLayout
android:id="@+id/configHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginBottom="16dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_weight="1"
android:text="@string/home_configure_notice"
android:textAppearance="@style/NavView.TextView.Helper"
android:textSize="12sp"
android:textStyle="italic" />
<Button
android:id="@+id/configureCards"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:minHeight="0dp"
android:text="@string/home_configure_add_remove" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator> </pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout> </layout>

View File

@ -67,24 +67,48 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="16dp" /> android:layout_height="16dp" />
<com.google.android.material.textfield.TextInputLayout <LinearLayout
android:id="@+id/login_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_marginLeft="16dp" android:orientation="horizontal">
android:layout_marginRight="16dp"
android:hint="@string/login_hint_token"
app:errorEnabled="true"
app:hintAnimationEnabled="true"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/login_code" android:id="@+id/login_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textNoSuggestions|textEmailAddress" /> android:layout_marginLeft="16dp"
</com.google.android.material.textfield.TextInputLayout> android:layout_weight="1"
android:hint="@string/login_hint_token"
app:errorEnabled="true"
app:hintAnimationEnabled="true"
app:hintEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/login_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions|textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/loginQrScan"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:padding="4dp"
android:layout_marginRight="16dp"
android:layout_marginLeft="8dp"
android:scaleType="centerInside"
tools:srcCompat="@tools:sample/avatars"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/login_vulcan_qr" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginPinLayout" android:id="@+id/loginPinLayout"
@ -167,4 +191,4 @@
</LinearLayout> </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View File

@ -315,7 +315,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/grades_stats_custom_value_notice" android:text="@string/grades_stats_custom_config_notice"
android:textAppearance="@style/NavView.TextView.Helper" android:textAppearance="@style/NavView.TextView.Helper"
android:textSize="12sp" android:textSize="12sp"
android:textStyle="italic" /> android:textStyle="italic" />

View File

@ -141,7 +141,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:autoLink="all"
android:minHeight="250dp" android:minHeight="250dp"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp" android:paddingRight="16dp"
@ -359,4 +358,4 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>
</layout> </layout>

View File

@ -14,11 +14,9 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:background="@drawable/bg_rounded_8dp" android:background="@drawable/bg_rounded_8dp"
android:fontFamily="serif-monospace"
android:gravity="center" android:gravity="center"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="24sp" android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View File

@ -551,7 +551,7 @@
<string name="settings_about_licenses_text">Open-source licenses</string> <string name="settings_about_licenses_text">Open-source licenses</string>
<string name="settings_about_privacy_policy_text">Privacy policy</string> <string name="settings_about_privacy_policy_text">Privacy policy</string>
<string name="settings_about_register_title_text">E-register</string> <string name="settings_about_register_title_text">E-register</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - January 2020</string> <string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - March 2020</string>
<string name="settings_about_update_subtext">Click to check for updates</string> <string name="settings_about_update_subtext">Click to check for updates</string>
<string name="settings_about_update_text">Update</string> <string name="settings_about_update_text">Update</string>
<string name="settings_about_version_text">Version</string> <string name="settings_about_version_text">Version</string>
@ -960,4 +960,6 @@
<string name="other">Other</string> <string name="other">Other</string>
<string name="menu_grades_config">Grades settings</string> <string name="menu_grades_config">Grades settings</string>
<string name="dialog_day_lessons_info">%s - %s (%s lessons - %s hours %s minutes)</string> <string name="dialog_day_lessons_info">%s - %s (%s lessons - %s hours %s minutes)</string>
<string name="event_sharing_text">Sharing the event…</string>
<string name="event_removing_text">Removing shared event…</string>
</resources> </resources>

View File

@ -13,6 +13,9 @@
<color name="dividerColor">#3e7f7f7f</color> <color name="dividerColor">#3e7f7f7f</color>
<color name="mdtp_accent_color">#2196f3</color>
<color name="mdtp_accent_color_dark">#64b5f6</color>
<!-- LIGHT THEME --> <!-- LIGHT THEME -->
<color name="windowBackgroundLight">#ffffffff</color> <color name="windowBackgroundLight">#ffffffff</color>

View File

@ -96,6 +96,8 @@
<string name="error_183" translatable="false">ERROR_LIBRUS_API_NOTICEBOARD_PROBLEM</string> <string name="error_183" translatable="false">ERROR_LIBRUS_API_NOTICEBOARD_PROBLEM</string>
<string name="error_184" translatable="false">ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED</string> <string name="error_184" translatable="false">ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED</string>
<string name="error_185" translatable="false">ERROR_LIBRUS_API_DEVICE_REGISTERED</string> <string name="error_185" translatable="false">ERROR_LIBRUS_API_DEVICE_REGISTERED</string>
<string name="error_186" translatable="false">ERROR_LIBRUS_MESSAGES_NOT_FOUND</string>
<string name="error_187" translatable="false">ERROR_LOGIN_LIBRUS_API_INVALID_REQUEST</string>
<string name="error_201" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN</string> <string name="error_201" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_202" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD</string> <string name="error_202" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD</string>
@ -110,6 +112,7 @@
<string name="error_213" translatable="false">ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID</string> <string name="error_213" translatable="false">ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID</string>
<string name="error_214" translatable="false">ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE</string> <string name="error_214" translatable="false">ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE</string>
<string name="error_215" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID</string> <string name="error_215" translatable="false">ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID</string>
<string name="error_218" translatable="false">ERROR_MOBIDZIENNIK_WEB_SERVER_PROBLEM</string>
<string name="error_301" translatable="false">ERROR_LOGIN_VULCAN_INVALID_SYMBOL</string> <string name="error_301" translatable="false">ERROR_LOGIN_VULCAN_INVALID_SYMBOL</string>
<string name="error_302" translatable="false">ERROR_LOGIN_VULCAN_INVALID_TOKEN</string> <string name="error_302" translatable="false">ERROR_LOGIN_VULCAN_INVALID_TOKEN</string>
@ -180,20 +183,20 @@
<string name="error_10_reason">Nie udało się wysłać wiadomości: nowa wiadomość nie została odnaleziona na liście wiadomości wysłanych</string> <string name="error_10_reason">Nie udało się wysłać wiadomości: nowa wiadomość nie została odnaleziona na liście wiadomości wysłanych</string>
<string name="error_50_reason">Błąd odpowiedzi serwera</string> <string name="error_50_reason">Błąd odpowiedzi serwera</string>
<string name="error_51_reason">Błąd serwera: nieprawidłowe zapytanie</string> <string name="error_51_reason">Błąd serwera: nieprawidłowe zapytanie. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_52_reason">Błąd serwera: odmowa dostępu</string> <string name="error_52_reason">Błąd serwera: odmowa dostępu</string>
<string name="error_53_reason">Błąd serwera: dostęp zabroniony</string> <string name="error_53_reason">Błąd serwera: dostęp zabroniony</string>
<string name="error_54_reason">Błąd serwera: plik nie znaleziony</string> <string name="error_54_reason">Błąd serwera: plik nie znaleziony</string>
<string name="error_55_reason">Błąd serwera: nieprawidłowa metoda zapytania</string> <string name="error_55_reason">Błąd serwera: nieprawidłowa metoda zapytania. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_56_reason">Błąd serwera: odpowiedź niedostępna</string> <string name="error_56_reason">Błąd serwera: odpowiedź niedostępna. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_57_reason">Błąd serwera: niespełnione zależności</string> <string name="error_57_reason">Błąd serwera: niespełnione zależności. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_58_reason">Wewnętrzny błąd serwera</string> <string name="error_58_reason">Wewnętrzny błąd serwera. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_59_reason">Dziennik jest tymczasowo niedostępny</string> <string name="error_59_reason">Dziennik jest tymczasowo niedostępny</string>
<string name="error_60_reason">Brak internetu: nie znaleziono adresu serwera</string> <string name="error_60_reason">Brak internetu: nie znaleziono adresu serwera</string>
<string name="error_61_reason">Brak internetu: przekroczono czas oczekiwania</string> <string name="error_61_reason">Brak internetu: przekroczono czas oczekiwania. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_62_reason">Brak internetu</string> <string name="error_62_reason">Brak internetu</string>
<string name="error_63_reason">Brak internetu: połączenie SSL nie powiodło się</string> <string name="error_63_reason">Brak internetu: połączenie SSL nie powiodło się</string>
<string name="error_100_reason">Brak odpowiedzi serwera</string> <string name="error_100_reason">Brak odpowiedzi serwera. Dziennik może być przeciążony lub mieć przerwę techniczną.</string>
<string name="error_101_reason">Dane logowania niekompletne</string> <string name="error_101_reason">Dane logowania niekompletne</string>
<string name="error_102_reason">Nieprawidłowe dane logowania</string> <string name="error_102_reason">Nieprawidłowe dane logowania</string>
<string name="error_105_reason">Profil nie został ustawiony</string> <string name="error_105_reason">Profil nie został ustawiony</string>
@ -244,7 +247,7 @@
<string name="error_159_reason">API Portalu Librus wyłączone</string> <string name="error_159_reason">API Portalu Librus wyłączone</string>
<string name="error_160_reason">Konto Synergia zostało rozłączone</string> <string name="error_160_reason">Konto Synergia zostało rozłączone</string>
<string name="error_161_reason">Inny błąd Portalu Librus</string> <string name="error_161_reason">Inny błąd Portalu Librus</string>
<string name="error_162_reason">Nie znaleziono konta Synergia</string> <string name="error_162_reason">Nie znaleziono konta Synergia. Zaloguj się na stronie portal.librus.pl, a następnie powiąż swoje konto Synergia do konta Librus Portal.</string>
<string name="error_163_reason">Inny błąd logowania do Portalu Librus</string> <string name="error_163_reason">Inny błąd logowania do Portalu Librus</string>
<string name="error_164_reason">ERROR_LOGIN_LIBRUS_PORTAL_CODE_EXPIRED</string> <string name="error_164_reason">ERROR_LOGIN_LIBRUS_PORTAL_CODE_EXPIRED</string>
<string name="error_165_reason">ERROR_LOGIN_LIBRUS_PORTAL_CODE_REVOKED</string> <string name="error_165_reason">ERROR_LOGIN_LIBRUS_PORTAL_CODE_REVOKED</string>
@ -259,8 +262,8 @@
<string name="error_174_reason">ERROR_LIBRUS_SYNERGIA_OTHER</string> <string name="error_174_reason">ERROR_LIBRUS_SYNERGIA_OTHER</string>
<string name="error_175_reason">Librus Synergia: przerwa techniczna</string> <string name="error_175_reason">Librus Synergia: przerwa techniczna</string>
<string name="error_176_reason">Librus Wiadomości: przerwa techniczna</string> <string name="error_176_reason">Librus Wiadomości: przerwa techniczna</string>
<string name="error_177_reason">ERROR_LIBRUS_MESSAGES_ERROR</string> <string name="error_177_reason">Librus Wiadomości: serwer zwrócił błąd. Prześlij zgłoszenie błędu.</string>
<string name="error_178_reason">ERROR_LIBRUS_MESSAGES_OTHER</string> <string name="error_178_reason">Librus Wiadomości: serwer zwrócił nieznany błąd. Prześlij zgłoszenie błędu.</string>
<string name="error_179_reason">Librus Wiadomości: nieprawidłowe dane logowania</string> <string name="error_179_reason">Librus Wiadomości: nieprawidłowe dane logowania</string>
<string name="error_180_reason">Librus Portal: nieprawidłowe dane logowania</string> <string name="error_180_reason">Librus Portal: nieprawidłowe dane logowania</string>
<string name="error_181_reason">Librus API: przerwa techniczna</string> <string name="error_181_reason">Librus API: przerwa techniczna</string>
@ -268,6 +271,8 @@
<string name="error_183_reason">Wystąpił problem z tablicą ogłoszeń</string> <string name="error_183_reason">Wystąpił problem z tablicą ogłoszeń</string>
<string name="error_184_reason">Librus: Sesja logowania wygasła. Zaloguj się ponownie.</string> <string name="error_184_reason">Librus: Sesja logowania wygasła. Zaloguj się ponownie.</string>
<string name="error_185_reason">Urządzenie jest już zarejestrowane</string> <string name="error_185_reason">Urządzenie jest już zarejestrowane</string>
<string name="error_186_reason">Nie znaleziono wiadomości. Mogła zostać usunięta.</string>
<string name="error_187_reason">Nieprawidłowe dane dostępu. Sprawdź poprawność wprowadzonych danych.</string>
<string name="error_201_reason">Nieprawidłowy login lub hasło</string> <string name="error_201_reason">Nieprawidłowy login lub hasło</string>
<string name="error_202_reason">Podano stare hasło</string> <string name="error_202_reason">Podano stare hasło</string>
@ -282,6 +287,7 @@
<string name="error_213_reason">MobiDziennik: brak identyfikatora serwera</string> <string name="error_213_reason">MobiDziennik: brak identyfikatora serwera</string>
<string name="error_214_reason">MobiDziennik: błąd odpowiedzi serwera</string> <string name="error_214_reason">MobiDziennik: błąd odpowiedzi serwera</string>
<string name="error_215_reason">Brak identyfikatora sesji przy logowaniu</string> <string name="error_215_reason">Brak identyfikatora sesji przy logowaniu</string>
<string name="error_218_reason">MobiDziennik: problemy z wydajnością serwerów. Spróbuj ponownie później.</string>
<string name="error_301_reason">Nieprawidłowy symbol</string> <string name="error_301_reason">Nieprawidłowy symbol</string>
<string name="error_302_reason">Nieprawidłowy token</string> <string name="error_302_reason">Nieprawidłowy token</string>
@ -332,7 +338,7 @@
<string name="error_901_reason">EXCEPTION_LOGIN_LIBRUS_API_TOKEN</string> <string name="error_901_reason">EXCEPTION_LOGIN_LIBRUS_API_TOKEN</string>
<string name="error_902_reason">EXCEPTION_LOGIN_LIBRUS_PORTAL_TOKEN</string> <string name="error_902_reason">EXCEPTION_LOGIN_LIBRUS_PORTAL_TOKEN</string>
<string name="error_903_reason">EXCEPTION_LIBRUS_PORTAL_SYNERGIA_TOKEN</string> <string name="error_903_reason">EXCEPTION_LIBRUS_PORTAL_SYNERGIA_TOKEN</string>
<string name="error_904_reason">EXCEPTION_LIBRUS_API_REQUEST</string> <string name="error_904_reason">Zgłoś błąd: wyjątek w API Librus</string>
<string name="error_905_reason">EXCEPTION_LIBRUS_SYNERGIA_REQUEST</string> <string name="error_905_reason">EXCEPTION_LIBRUS_SYNERGIA_REQUEST</string>
<string name="error_906_reason">EXCEPTION_MOBIDZIENNIK_WEB_REQUEST</string> <string name="error_906_reason">EXCEPTION_MOBIDZIENNIK_WEB_REQUEST</string>
<string name="error_907_reason">EXCEPTION_VULCAN_API_REQUEST</string> <string name="error_907_reason">EXCEPTION_VULCAN_API_REQUEST</string>

View File

@ -6,4 +6,5 @@
<resources> <resources>
<item name="move_card_down_action" type="id"/> <item name="move_card_down_action" type="id"/>
<item name="move_card_up_action" type="id"/> <item name="move_card_up_action" type="id"/>
<item name="bettermovementmethod_highlight_background_span" type="id" />
</resources> </resources>

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