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

View File

@ -1,4 +1,4 @@
<h3>Wersja 4.0-beta.12, 2020-03-10</h3>
<h3>Wersja 4.0-beta.14, 2020-03-24</h3>
<ul>
<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>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,11 @@ class ConfigSync(private val config: Config) {
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true }
set(value) { config.set("syncEnabled", value); mSyncEnabled = value }
private var mWebPushEnabled: Boolean? = null
var webPushEnabled: Boolean
get() { mWebPushEnabled = mWebPushEnabled ?: config.values.get("webPushEnabled", true); return mWebPushEnabled ?: true }
set(value) { config.set("webPushEnabled", value); mWebPushEnabled = value }
private var mSyncOnlyWifi: Boolean? = null
var onlyWifi: Boolean
get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates }
@ -35,6 +40,11 @@ class ConfigSync(private val config: Config) {
get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true }
set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: config.values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { config.set("lastAppSync", value); mLastAppSync = value }
/* ____ _ _ _
/ __ \ (_) | | | |
| | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,23 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
val profile
get() = data.profile
/* TODO
add error handling:
<!-- CONTENT -->
<div id="content">
<a name="tresc"></a>
<h1>Ważna informacja!</h1>
<div class="okienko_informacyjne">
Korzystasz z hasła, które zostało wygenerowane automatycznie.<hr/>
Pamiętaj, aby je zmienić oraz uzupełnić dane w swoim profilu.<br/><br/>
Obie te operacje można wykonać uruchamiając opcję
<a href="https://poznan.mobidziennik.pl/dziennik/edytujprofil" title="Edycja profilu, możliwość zmiany hasła">Moje konto->Edycja profilu</a>.
</div> </div>
*/
fun webGet(
tag: String,
endpoint: String,
@ -65,6 +82,12 @@ open class MobidziennikWeb(open val data: DataMobidziennik, open val lastSync: L
return
}
if (text.contains("<h2>Problemy z wydajnością</h2>")) {
data.error(ApiError(TAG, ERROR_MOBIDZIENNIK_WEB_SERVER_PROBLEM)
.withResponse(response))
return
}
try {
onSuccess(text)
} catch (e: Exception) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ class GradeDetailsDialog(
b.weightText = manager.getWeightString(app, grade)
b.commentVisible = false
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.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_SEM
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
import java.util.*
class GradesConfigDialog(
val activity: AppCompatActivity,
@ -88,14 +89,34 @@ class GradesConfigDialog(
else -> null
}?.isChecked = true
b.dontCountZeroToAverage.isChecked = !profileConfig.countZeroToAvg
b.dontCountGrades.isChecked = profileConfig.dontCountEnabled && profileConfig.dontCountGrades.isNotEmpty()
b.hideImproved.isChecked = profileConfig.hideImproved
b.averageWithoutWeight.isChecked = profileConfig.averageWithoutWeight
if (profileConfig.dontCountGrades.isEmpty()) {
b.dontCountGradesText.setText("nb, 0, bz, bd")
}
else {
b.dontCountGradesText.setText(profileConfig.dontCountGrades.join(", "))
}
}
private fun saveConfig() {
profileConfig.plusValue = if (b.customPlusCheckBox.isChecked) b.customPlusValue.progress else null
profileConfig.minusValue = if (b.customMinusCheckBox.isChecked) b.customMinusValue.progress else null
b.dontCountGradesText.setText(
b.dontCountGradesText
.text
?.toString()
?.toLowerCase(Locale.getDefault())
?.replace(", ", ",")
)
profileConfig.dontCountEnabled = b.dontCountGrades.isChecked
profileConfig.dontCountGrades = b.dontCountGradesText.text
?.split(",")
?.map { it.trim() }
?: listOf()
}
private fun initView() {
@ -127,7 +148,6 @@ class GradesConfigDialog(
b.gradeAverageMode2.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_AVG_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.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.cardview.widget.CardView
import androidx.core.content.FileProvider
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@ -116,18 +116,16 @@ class GenerateBlockTimetableDialog(
}}
private fun selectDate() {
MaterialDatePicker.Builder
.datePicker()
.setSelection(Date.getToday().inMillis)
.build()
val date = Date.getToday()
DatePickerDialog
.newInstance({ _, year, monthOfYear, dayOfMonth ->
val dateSelected = Date(year, monthOfYear, dayOfMonth)
generateBlockTimetable(dateSelected.weekStart, dateSelected.weekEnd)
}, date.year, date.month, date.day)
.apply {
addOnPositiveButtonClickListener { dateInMillis ->
dismiss()
val selectedDate = Date.fromMillis(dateInMillis)
generateBlockTimetable(selectedDate.weekStart, selectedDate.weekEnd)
}
accentColor = R.attr.colorPrimary.resolveAttr(this@GenerateBlockTimetableDialog.activity)
show(this@GenerateBlockTimetableDialog.activity.supportFragmentManager, "DatePickerDialog")
}
.show(activity.supportFragmentManager, "MaterialDatePicker")
}
@Subscribe(threadMode = ThreadMode.MAIN)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,8 @@ import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2
import kotlinx.coroutines.*
@ -53,6 +55,7 @@ class GradesFragment : Fragment(), CoroutineScope {
GradesAdapter(activity)
}
private val manager by lazy { app.gradesManager }
private val dontCountEnabled by lazy { manager.dontCountEnabled }
private val dontCountGrades by lazy { manager.dontCountGrades }
private var expandSubjectId = 0L
@ -80,6 +83,16 @@ class GradesFragment : Fragment(), CoroutineScope {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
//addItemDecoration(SimpleDividerItemDecoration(context))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.isEnabled = false
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.isEnabled = true
}
}
})
}
}
@ -299,7 +312,7 @@ class GradesFragment : Fragment(), CoroutineScope {
private fun countGrade(grade: Grade, averages: GradesAverages) {
val value = manager.getGradeValue(grade)
val weight = manager.getGradeWeight(dontCountGrades, grade)
val weight = manager.getGradeWeight(dontCountEnabled, dontCountGrades, grade)
when (grade.type) {
Grade.TYPE_NORMAL -> {
if (grade.value > 0f) {

View File

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

View File

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

View File

@ -102,7 +102,7 @@ class StatsViewHolder(
.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.customValueButton.onClick {
GradesConfigDialog(activity, reloadOnDismiss = true)

View File

@ -16,11 +16,8 @@ import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.*
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.GradesAdapter
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) {
val manager = app.gradesManager
val contextWrapper = ContextThemeWrapper(activity, Themes.themeInt)
val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme)
b.subjectName.text = item.subjectName
b.dropdownIcon.rotation = when (item.state) {
@ -62,6 +59,7 @@ class SubjectViewHolder(
if (firstSemester.number != item.semester) {
b.gradesContainer.addView(TextView(contextWrapper).apply {
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
setText(R.string.grades_preview_other_semester, firstSemester.number)
setPadding(0, 0, 5.dp, 0)
maxLines = 1
@ -88,6 +86,7 @@ class SubjectViewHolder(
}
b.previewContainer.addView(TextView(contextWrapper).apply {
setTextColor(android.R.attr.textColorSecondary.resolveAttr(context))
text = manager.getAverageString(app, firstSemester.averages, nameSemester = true, showSemester = firstSemester.number)
//gravity = Gravity.END
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {

View File

@ -5,16 +5,18 @@
package pl.szczodrzynski.edziennik.ui.modules.home
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.*
import androidx.recyclerview.widget.RecyclerView
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.utils.SwipeRefreshLayoutNoIndicator
class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, private val refreshLayout: SwipeRefreshLayoutNoIndicator?) : ItemTouchHelper.Callback() {
companion object {
private const val TAG = "CardItemTouchHelperCallback"
private const val DRAG_FLAGS = ItemTouchHelper.UP or ItemTouchHelper.DOWN
private const val SWIPE_FLAGS = 0
private const val DRAG_FLAGS = UP or DOWN
private const val SWIPE_FLAGS = LEFT
}
private var dragCardView: MaterialCardView? = null
@ -27,20 +29,24 @@ class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, priv
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
swapCards(fromPosition, toPosition, cardAdapter)
return true
return swapCards(fromPosition, toPosition, cardAdapter)
}
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) {
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?.isDragged = true
refreshLayout?.isEnabled = false
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && dragCardView != null) {
}
else if (actionState == ACTION_STATE_IDLE && dragCardView != null) {
refreshLayout?.isEnabled = true
dragCardView?.isDragged = false
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.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -25,8 +26,12 @@ import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding
import pl.szczodrzynski.edziennik.onClick
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.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
@ -36,12 +41,12 @@ class HomeFragment : Fragment(), CoroutineScope {
companion object {
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 toCard = cardAdapter.items[toPosition]
if (fromCard.id == 100 || toCard.id == 100) {
// debug card is not swappable
return
return false
}
cardAdapter.items[fromPosition] = cardAdapter.items[toPosition]
cardAdapter.items[toPosition] = fromCard
@ -52,6 +57,15 @@ class HomeFragment : Fragment(), CoroutineScope {
homeCards[fromPosition] = homeCards[toPosition]
homeCards[toPosition] = fromPair
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?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
if (!isAdded)
return
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)
.withTitle(R.string.menu_set_student_number)
.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()
})
)
b.configureCards.onClick {
HomeConfigDialog(activity, reloadOnDismiss = true)
}
b.scrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int ->
b.refreshLayout.isEnabled = scrollY == 0
}
val showUnified = false
@ -130,8 +158,8 @@ class HomeFragment : Fragment(), CoroutineScope {
else -> null
}
}
if (App.devMode)
items += HomeDebugCard(100, app, activity, this, app.profile)
//if (App.devMode)
// items += HomeDebugCard(100, app, activity, this, app.profile)
val adapter = HomeCardAdapter(items)
val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))

View File

@ -73,7 +73,7 @@ class HomeDebugCard(
}
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
if (b.eventsView.adapter == null) {
b.eventsView.adapter = adapter

View File

@ -60,7 +60,7 @@ class HomeLuckyNumberCard(
R.string.home_lucky_number_details
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 res: Pair<Int, Array<out Any>> = when {
luckyNumber == null -> R.string.home_lucky_number_no_info to emptyArray()

View File

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

View File

@ -85,9 +85,7 @@ class HomeworkFragment : Fragment() {
b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
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 onPageSelected(position: Int) {

View File

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

View File

@ -4,19 +4,27 @@
package pl.szczodrzynski.edziennik.ui.modules.login
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
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.Dispatchers
import kotlinx.coroutines.Job
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_REQUEST
import pl.szczodrzynski.edziennik.data.api.LOGIN_MODE_LIBRUS_JST
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.databinding.FragmentLoginLibrusJstBinding
import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog
import java.util.*
import kotlin.coroutines.CoroutineContext
@ -47,12 +55,26 @@ class LoginLibrusJstFragment : Fragment(), CoroutineScope {
activity.lastError = null
startCoroutineTimer(delayMillis = 100) {
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.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.backButton.onClick { nav.navigateUp() }

View File

@ -4,17 +4,25 @@
package pl.szczodrzynski.edziennik.ui.modules.login
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
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.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.databinding.FragmentLoginVulcanBinding
import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog
import pl.szczodrzynski.edziennik.utils.Utils
import java.util.*
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.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.databinding.MessageFragmentBinding
import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.getStringFromFile
@ -198,7 +199,7 @@ class MessageFragment : Fragment(), CoroutineScope {
}
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)
val messageInfo = MessagesUtils.getMessageInfo(app, message, 40, 20, 14, 10)
@ -251,6 +252,9 @@ class MessageFragment : Fragment(), CoroutineScope {
showAttachments()
BetterLink.attach(b.subject)
BetterLink.attach(b.body)
b.progress.visibility = View.GONE
Anim.fadeIn(b.content, 200, null)
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
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) {
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")
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)

View File

@ -1,8 +1,10 @@
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.graphics.*
import android.os.Build
import android.text.Html
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.Spanned
import androidx.core.graphics.ColorUtils
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.getNameInitials
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.math.roundToInt
object MessagesUtils {
@ -180,32 +182,7 @@ object MessagesUtils {
class MessageInfo(var profileImage: Bitmap?, var profileName: String?)
@JvmStatic
fun htmlToSpannable(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(), "")
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)
}
fun htmlToSpannable(context: Context, html: String): Spanned {
return BetterHtml.fromHtml(context, html)
}
}

View File

@ -1104,7 +1104,7 @@ public class SettingsNewFragment extends MaterialAboutFragment {
.icon(SzkolnyFont.Icon.szf_discord_outline)
.color(IconicsColor.colorInt(primaryTextOnPrimaryBg))
.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());
items.add(new MaterialAboutActionItem.Builder()

View File

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

View File

@ -10,13 +10,14 @@ import android.util.AttributeSet
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.datepicker.MaterialDatePicker
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.observeOnce
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
@ -176,23 +177,18 @@ class DateDropdown : TextInputDropDown {
}
fun pickerDialog() {
MaterialDatePicker.Builder
.datePicker()
.setSelection(
if (selected?.tag is Date)
(selected?.tag as Date).inMillis
else
Date.getToday().inMillis
)
.build()
val date = getSelected() as? Date ?: Date.getToday()
DatePickerDialog
.newInstance({ _, year, monthOfYear, dayOfMonth ->
val dateSelected = Date(year, monthOfYear+1, dayOfMonth)
selectDate(dateSelected)
onDateSelected?.invoke(dateSelected, null)
}, date.year, date.month-1, date.day)
.apply {
addOnPositiveButtonClickListener {
val dateSelected = Date.fromMillis(it)
selectDate(dateSelected)
onDateSelected?.invoke(dateSelected, null)
}
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.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
import com.wdullaer.materialdatetimepicker.time.TimePickerDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pl.szczodrzynski.edziennik.*
@ -174,19 +175,19 @@ class TimeDropdown : TextInputDropDown {
}
fun pickerDialog() {
/*MaterialDatePicker.Builder
.datePicker()
.setSelection((selectedId?.let { Date.fromValue(it.toInt()) }
?: Date.getToday()).inMillis)
.build()
val time = (getSelected() as? Pair<*, *>)?.first as? Time ?: Time.getNow()
TimePickerDialog
.newInstance({ _, hourOfDay, minute, second ->
val timeSelected = Time(hourOfDay, minute, second)
selectTime(timeSelected)
onTimeSelected?.invoke(timeSelected, null, null)
}, time.hour, time.minute, 0, true)
.apply {
addOnPositiveButtonClickListener {
val dateSelected = Date.fromMillis(it)
selectDate(dateSelected)
}
this@DateDropdown.activity ?: return@apply
show(this@DateDropdown.activity!!.supportFragmentManager, "MaterialDatePicker")
}*/
this@TimeDropdown.activity ?: return@apply
accentColor = R.attr.colorPrimary.resolveAttr(this@TimeDropdown.activity)
show(this@TimeDropdown.activity!!.supportFragmentManager, "TimePickerDialog")
}
}
fun selectTime(time: Time) {
@ -208,12 +209,13 @@ class TimeDropdown : TextInputDropDown {
* Get the currently selected time.
* ### Returns:
* - 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]
* - [LessonRange] - the selected lesson range object, if [displayMode] == [DISPLAY_LESSON_RANGES]
*/
fun getSelected(): Any? {
return when (val tag = selected?.tag) {
0L -> null
0L -> 0L
is LessonFull ->
if (tag.displayStartTime != null)
tag.displayStartTime!! to tag.displayEndTime

View File

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

View File

@ -226,11 +226,14 @@ class WidgetTimetableProvider : AppWidgetProvider() {
lessons = lessonList.filter {
it.profileId == profile.id
&& it.displayDate == timetableDate
&& !(it.isCancelled && ignoreCancelled)
}
//if (lessons.isEmpty() && timetableDate.weekDay <= 5)
// break
if (lessons.isEmpty())
break
/*lessons = lessons.filterNot {
it.isCancelled && ignoreCancelled
}*/
checkedDays++
}
@ -248,35 +251,39 @@ class WidgetTimetableProvider : AppWidgetProvider() {
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
if (!unified) {
if (lessons.isNotEmpty())
displayingDate = timetableDate
profileId = profile.id
if (lessons.isEmpty()) {
if (lessons.isEmpty() && checkedDays < 7) {
views.setViewVisibility(R.id.widgetTimetableListView, View.GONE)
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.widgetTimetableNoLessons, View.VISIBLE)
}
}
else {
if (lessons.isEmpty()) {
if (lessons.isEmpty() && checkedDays < 7) {
val separator = ItemWidgetTimetableModel()
separator.profileId = profile.id
separator.bigStyle = widgetConfig.bigStyle
separator.darkTheme = widgetConfig.darkTheme
separator.isNoTimetableItem = true;
separator.isNoTimetableItem = true
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()
separator.profileId = profile.id
separator.bigStyle = widgetConfig.bigStyle
separator.darkTheme = widgetConfig.darkTheme
separator.isNoLessonsItem = true;
separator.isNoLessonsItem = true
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
import android.content.Context
import android.graphics.Color
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.core.graphics.ColorUtils
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.navlib.getColorFromAttr
object Themes {
@ -49,7 +44,7 @@ object Themes {
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

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

View File

@ -1,150 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
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>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/feedbackFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -84,16 +84,32 @@
android:background="@drawable/divider"/>
<CheckBox
android:id="@+id/dontCountZeroToAverage"
android:id="@+id/dontCountGrades"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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
android:id="@+id/hideImproved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:minHeight="32dp"
android:text="@string/grades_config_dont_show_improved"/>

View File

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

View File

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

View File

@ -3,18 +3,70 @@
~ Copyright (c) Kuba Szczodrzyński 2019-11-23.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/card_home" />
android:layout_height="match_parent">
<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>
</layout>
</layout>

View File

@ -67,24 +67,48 @@
android:layout_width="match_parent"
android:layout_height="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/login_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:hint="@string/login_hint_token"
app:errorEnabled="true"
app:hintAnimationEnabled="true"
app:hintEnabled="true">
android:layout_height="match_parent"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/login_code"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/login_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions|textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginLeft="16dp"
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
android:id="@+id/loginPinLayout"
@ -167,4 +191,4 @@
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
</layout>

View File

@ -315,7 +315,7 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
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:textSize="12sp"
android:textStyle="italic" />

View File

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

View File

@ -14,11 +14,9 @@
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@drawable/bg_rounded_8dp"
android:fontFamily="serif-monospace"
android:gravity="center"
android:textColor="@color/black"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="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_privacy_policy_text">Privacy policy</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_text">Update</string>
<string name="settings_about_version_text">Version</string>
@ -960,4 +960,6 @@
<string name="other">Other</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="event_sharing_text">Sharing the event…</string>
<string name="event_removing_text">Removing shared event…</string>
</resources>

View File

@ -13,6 +13,9 @@
<color name="dividerColor">#3e7f7f7f</color>
<color name="mdtp_accent_color">#2196f3</color>
<color name="mdtp_accent_color_dark">#64b5f6</color>
<!-- LIGHT THEME -->
<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_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_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_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_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_218" translatable="false">ERROR_MOBIDZIENNIK_WEB_SERVER_PROBLEM</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>
@ -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_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_53_reason">Błąd serwera: dostęp zabroniony</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_56_reason">Błąd serwera: odpowiedź niedostępna</string>
<string name="error_57_reason">Błąd serwera: niespełnione zależności</string>
<string name="error_58_reason">Wewnętrzny błąd serwera</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. Dziennik może być przeciążony lub mieć przerwę techniczną.</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. 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_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_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_102_reason">Nieprawidłowe dane logowania</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_160_reason">Konto Synergia zostało rozłączone</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_164_reason">ERROR_LOGIN_LIBRUS_PORTAL_CODE_EXPIRED</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_175_reason">Librus Synergia: 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_178_reason">ERROR_LIBRUS_MESSAGES_OTHER</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">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_180_reason">Librus Portal: nieprawidłowe dane logowania</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_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_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_202_reason">Podano stare hasło</string>
@ -282,6 +287,7 @@
<string name="error_213_reason">MobiDziennik: brak identyfikatora 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_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_302_reason">Nieprawidłowy token</string>
@ -332,7 +338,7 @@
<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_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_906_reason">EXCEPTION_MOBIDZIENNIK_WEB_REQUEST</string>
<string name="error_907_reason">EXCEPTION_VULCAN_API_REQUEST</string>

View File

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

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