Compare commits

...

22 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
49 changed files with 1206 additions and 190 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.13, 2020-03-15</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] = {
0xc1, 0x4d, 0xcc, 0x2e, 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
@ -1094,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
@ -723,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) {
@ -1141,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

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

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

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

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

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

@ -36,7 +36,7 @@ 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")

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

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

View File

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

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
@ -159,6 +160,11 @@ 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() {

View File

@ -174,7 +174,7 @@ class EventManualDialog(
enqueuedWeekStart = weekStart
EdziennikTask.syncProfile(
profileId = App.profileId,
profileId = profileId,
viewIds = listOf(
MainActivity.DRAWER_ITEM_TIMETABLE to 0
),
@ -210,7 +210,7 @@ class EventManualDialog(
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
if (event.profileId == App.profileId) {
if (event.profileId == profileId) {
enqueuedWeekDialog?.dismiss()
enqueuedWeekDialog = null
progressDialog?.dismiss()
@ -241,7 +241,7 @@ class EventManualDialog(
with (b.dateDropdown) {
db = app.db
profileId = App.profileId
profileId = profileId
showWeekDays = false
showDays = true
showOtherDate = true
@ -269,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()
@ -289,7 +289,7 @@ class EventManualDialog(
with (b.teamDropdown) {
db = app.db
profileId = App.profileId
profileId = profileId
showNoTeam = true
loadItems()
selectTeamClass()
@ -299,7 +299,7 @@ class EventManualDialog(
with (b.subjectDropdown) {
db = app.db
profileId = App.profileId
profileId = profileId
showNoSubject = true
showCustomSubject = false
loadItems()
@ -309,7 +309,7 @@ class EventManualDialog(
with (b.teacherDropdown) {
db = app.db
profileId = App.profileId
profileId = profileId
showNoTeacher = true
loadItems()
selectDefault(editingEvent?.teacherId)
@ -497,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) {
@ -569,7 +569,7 @@ 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

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

View File

@ -313,8 +313,6 @@ class GradesFragment : Fragment(), CoroutineScope {
private fun countGrade(grade: Grade, averages: GradesAverages) {
val value = manager.getGradeValue(grade)
val weight = manager.getGradeWeight(dontCountEnabled, dontCountGrades, grade)
if (weight == 0f)
return
when (grade.type) {
Grade.TYPE_NORMAL -> {
if (grade.value > 0f) {

View File

@ -16,7 +16,7 @@ class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, priv
companion object {
private const val TAG = "CardItemTouchHelperCallback"
private const val DRAG_FLAGS = UP or DOWN
private const val SWIPE_FLAGS = RIGHT
private const val SWIPE_FLAGS = LEFT
}
private var dragCardView: MaterialCardView? = null

View File

@ -94,6 +94,13 @@ class HomeFragment : Fragment(), CoroutineScope {
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)

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

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

@ -1,17 +1,21 @@
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.content.Context
import android.graphics.*
import android.os.Build
import android.text.Html
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.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Message
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.Utils
import pl.szczodrzynski.navlib.blendColors
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.math.roundToInt
object MessagesUtils {
@ -179,39 +183,6 @@ object MessagesUtils {
@JvmStatic
fun htmlToSpannable(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))
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
} else {
Html.fromHtml(text)
}
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

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

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

@ -29,12 +29,14 @@
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"

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

@ -112,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>
@ -182,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>
@ -286,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>

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>

View File

@ -1261,7 +1261,7 @@
<string name="grades_config_dont_count_grades">Wyklucz wybrane oceny ze średniej</string>
<string name="grades_config_dont_count_hint">Oceny oddziel przecinkiem</string>
<string name="grades_config_dont_count_placeholder">Podaj oceny...</string>
<string name="home_configure_notice">Możesz usunąć karty przesuwając w prawo lub zmienić ich kolejność, przytrzymując na kartę.</string>
<string name="home_configure_notice">Możesz usunąć karty przesuwając w lewo lub zmienić ich kolejność, przytrzymując na kartę.</string>
<string name="home_configure_add_remove">Dodaj/usuń karty</string>
<string name="card_type_lucky_number">Szczęśliwy numerek</string>
<string name="card_type_timetable">Plan lekcji</string>
@ -1275,4 +1275,5 @@
<string name="registration_enable_progress_text">Pobieranie udostępnionych wydarzeń...</string>
<string name="registration_enable_dialog_title">Rejestracja na serwerze</string>
<string name="registration_enable_dialog_text">Rejestracja jest automatyczna, jeśli ta opcja jest włączona. Pozwala na tworzenie i odbieranie wydarzeń udostępnionych innym uczniom z Twojej klasy. Dzięki temu, można dodawać do dziennika pozycje nie zapisane przez nauczyciela.\n\nUpewnij się, że zapoznałeś się z warunkami <a href="http://szkolny.eu/privacy-policy">Polityki prywatności</a> i akceptujesz jej postanowienia.</string>
<string name="menu_add_remove_cards">Dodaj lub usuń karty</string>
</resources>

View File

@ -10,63 +10,65 @@
<item name="android:windowBackground">@drawable/dead_background</item>
</style>
<style name="AppTheme.NoDisplay" parent="Theme.MaterialComponents.Light.Dialog">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorSection">@color/colorSection</item>
<style name="AppTheme.Light.NoDisplay" parent="AppTheme.Light">
<item name="android:colorBackground">?attr/colorBackgroundFloating</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:textColor">@color/primaryTextLight</item>
<item name="android:textColorPrimary">@color/primaryTextLight</item>
<item name="material_drawer_primary_text">@color/primaryTextLight</item>
<item name="android:textColorSecondary">@color/secondaryTextLight</item>
<item name="material_drawer_secondary_text">@color/secondaryTextLight</item>
<item name="android:textColorTertiary">@color/secondaryTextLight</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat</item>
<item name="android:windowTitleBackgroundStyle">@style/Base.DialogWindowTitleBackground.AppCompat</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowNoTitle">true</item>
<item name="md_dark_theme">false</item>
<item name="md_title_color">?android:textColorPrimary</item>
<item name="md_content_color">?android:textColorPrimary</item>
<item name="md_link_color">?colorAccent</item>
<item name="md_item_color">?android:textColorPrimary</item>
<item name="windowActionBar">false</item>
<item name="windowActionModeOverlay">true</item>
<item name="listPreferredItemPaddingLeft">24dip</item>
<item name="listPreferredItemPaddingRight">24dip</item>
<item name="android:listDivider">@null</item>
<item name="android:buttonBarStyle">@style/Widget.AppCompat.ButtonBar.AlertDialog</item>
<item name="android:borderlessButtonStyle">@style/Widget.AppCompat.Button.Borderless</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:popupBackground">@color/windowBackgroundLight</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="AppThemeDark.NoDisplay" parent="Theme.MaterialComponents.Dialog">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="colorSection">@color/colorSection</item>
<item name="android:textColor">@color/primaryTextDark</item>
<item name="android:textColorPrimary">@color/primaryTextDark</item>
<item name="material_drawer_primary_text">@color/primaryTextDark</item>
<item name="android:textColorSecondary">@color/secondaryTextDark</item>
<item name="material_drawer_secondary_text">@color/secondaryTextDark</item>
<item name="android:textColorTertiary">@color/secondaryTextDark</item>
<item name="md_dark_theme">true</item>
<item name="md_title_color">?android:textColorPrimary</item>
<item name="md_content_color">?android:textColorPrimary</item>
<item name="md_link_color">?colorAccent</item>
<item name="md_item_color">?android:textColorPrimary</item>
<item name="android:popupBackground">@color/windowBackgroundDark</item>
<style name="AppTheme.Dark.NoDisplay" parent="AppTheme.Dark">
<item name="android:colorBackground">?attr/colorBackgroundFloating</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat</item>
<item name="android:windowTitleBackgroundStyle">@style/Base.DialogWindowTitleBackground.AppCompat</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="windowActionModeOverlay">true</item>
<item name="listPreferredItemPaddingLeft">24dip</item>
<item name="listPreferredItemPaddingRight">24dip</item>
<item name="android:listDivider">@null</item>
<item name="android:buttonBarStyle">@style/Widget.AppCompat.ButtonBar.AlertDialog</item>
<item name="android:borderlessButtonStyle">@style/Widget.AppCompat.Button.Borderless</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:popupBackground">@color/windowBackgroundDark</item>
</style>
<style name="AppTheme.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.3.61'
release = [
versionName: "4.0-beta.13",
versionCode: 4000013
versionName: "4.0-beta.14",
versionCode: 4000014
]
setup = [