Compare commits

..

15 Commits

Author SHA1 Message Date
9b7aca745a [3.9.9-dev] 2019-11-21 19:18:04 +01:00
82852389fa [UI] Update indentation, again. Fix manual event color selecting. 2019-11-21 18:43:01 +01:00
ce06084e6f [Timetable] Fix lessons removing, again. 2019-11-21 18:24:02 +01:00
3ca051983f [APIv2/Timetable] Add searching for the next lesson by team id 2019-11-20 22:43:48 +01:00
cd379e4175 [APIv2/Librus] Add getting grade comments 2019-11-20 21:16:18 +01:00
62fdfa2b6f [UI] Update manual event dialog. Fix timetable errors. 2019-11-20 21:13:43 +01:00
9866017f7e [APIv2/Vulcan] Fix timetable teams issue. Fix missing login data error. 2019-11-20 19:51:50 +01:00
67f98b08c6 [Update] Update update code to allow update from direct download. 2019-11-20 17:11:08 +01:00
fdb5f7ec02 [APIv2/Mobidziennik] Implement getting message details. 2019-11-18 22:57:23 +01:00
04c3c7ca6e [APIv2] Handle Librus Portal maintenance. 2019-11-18 18:55:52 +01:00
f424315d97 [3.9.8-dev] 2019-11-17 23:17:37 +01:00
c907a8df37 [APIv2] Librus: better error handling. Timetable: fix widget crashing with NPE. 2019-11-17 23:16:13 +01:00
37ea65e3fc [Timetable] Add SwipeToRefresh. Select start&end hours based on lesson ranges. 2019-11-16 21:16:18 +01:00
a3e5f824c8 [UI] Fix messages & homework refresh layout sensitivity. 2019-11-16 18:46:01 +01:00
e0c850a455 [Gradle] Update Tachyon to support creating DayView programmatically. 2019-11-16 17:07:07 +01:00
48 changed files with 1495 additions and 748 deletions

View File

@ -169,7 +169,7 @@ dependencies {
implementation 'com.github.kuba2k2:RecyclerTabLayout:700f980584'
implementation 'com.linkedin.android.tachyon:tachyon:1.0.2'
implementation 'com.github.kuba2k2:Tachyon:551943a6b5'
}
repositories {
mavenCentral()

View File

@ -4,7 +4,9 @@ import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.text.*
@ -13,9 +15,10 @@ import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.util.LongSparseArray
import android.util.SparseArray
import android.util.TypedValue
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.annotation.*
import androidx.core.app.ActivityCompat
import androidx.core.util.forEach
import androidx.lifecycle.LifecycleOwner
@ -446,3 +449,49 @@ fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observ
}
})
}
/**
* Convert a value in dp to pixels.
*/
val Int.dp: Int
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
/**
* Convert a value in pixels to dp.
*/
val Int.px: Int
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
@ColorInt
fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int {
val typedValue = TypedValue()
context?.theme?.resolveAttribute(this, typedValue, true)
return typedValue.data
}
@ColorInt
fun @receiver:ColorRes Int.resolveColor(context: Context): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
context.resources.getColor(this, context.theme)
}
else {
context.resources.getColor(this)
}
}
fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
context.resources.getDrawable(this, context.theme)
}
else {
context.resources.getDrawable(this)
}
}
fun View.findParentById(targetId: Int): View? {
if (id == targetId) {
return this
}
val viewParent = this.parent ?: return null
if (viewParent is View) {
return viewParent.findParentById(targetId)
}
return null
}

View File

@ -311,13 +311,14 @@ public class Notifier {
\____/| .__/ \__,_|\__,_|\__\___||___/
| |
|*/
public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename) {
public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename, boolean updateDirect) {
if (!app.appConfig.notifyAboutUpdates)
return;
Intent notificationIntent = new Intent(app.getContext(), BootReceiver.NotificationActionService.class)
.putExtra("update_version", updateVersion)
.putExtra("update_url", updateUrl)
.putExtra("update_filename", updateFilename);
.putExtra("update_filename", updateFilename)
.putExtra("update_direct", updateDirect);
PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0,
notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);

View File

@ -214,8 +214,8 @@ class WidgetTimetable : AppWidgetProvider() {
model.lessonId = lesson.id
model.lessonDate = timetableDate
model.startTime = lesson.startTime
model.endTime = lesson.endTime
model.startTime = lesson.displayStartTime
model.endTime = lesson.displayEndTime
// check if the lesson has already passed or it's currently in progress
if (lesson.displayDate == today) {

View File

@ -39,9 +39,11 @@ const val ERROR_REQUEST_HTTP_404 = 54
const val ERROR_REQUEST_HTTP_405 = 55
const val ERROR_REQUEST_HTTP_410 = 56
const val ERROR_REQUEST_HTTP_500 = 57
const val ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND = 60
const val ERROR_REQUEST_FAILURE_TIMEOUT = 61
const val ERROR_REQUEST_FAILURE_NO_INTERNET = 62
const val ERROR_RESPONSE_EMPTY = 100
const val ERROR_LOGIN_DATA_MISSING = 101
const val ERROR_LOGIN_DATA_INVALID = 102
const val ERROR_PROFILE_MISSING = 105
const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
@ -99,6 +101,13 @@ const val ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_INVALID = 172
const val ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED = 173
const val ERROR_LIBRUS_SYNERGIA_OTHER = 174
const val ERROR_LIBRUS_SYNERGIA_MAINTENANCE = 175
const val ERROR_LIBRUS_MESSAGES_MAINTENANCE = 176
const val ERROR_LIBRUS_MESSAGES_ERROR = 177
const val ERROR_LIBRUS_MESSAGES_OTHER = 178
const val ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN = 179
const val ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN = 180
const val ERROR_LIBRUS_API_MAINTENANCE = 181
const val ERROR_LIBRUS_PORTAL_MAINTENANCE = 182
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202

View File

@ -37,6 +37,16 @@ object Regexes {
"""events: (.+),$""".toRegex(RegexOption.MULTILINE)
}
val MOBIDZIENNIK_MESSAGE_READ_DATE by lazy {
"""czas przeczytania:.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_MESSAGE_SENT_READ_DATE by lazy {
""".+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_MESSAGE_ATTACHMENT by lazy {
"""href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)"(?:.+?<small.+?\(([0-9.]+)\s(M|K|G|)B\))*""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
@ -61,6 +71,8 @@ object Regexes {
"""<option.*?value="([0-9]+)"\sdata-id-ucznia="([A-z0-9]+?)".*?>(.+?)\s(.+?)\s*\((.+?),\s*(.+?)\)</option>""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val VULCAN_SHITFT_ANNOTATION by lazy {
"""\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex()
}

View File

@ -24,6 +24,7 @@ const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_GC = 1023
const val ENDPOINT_LIBRUS_API_TEXT_GC = 1024
const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GC = 1025
const val ENDPOINT_LIBRUS_API_BEHAVIOUR_GC = 1026
const val ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS = 1030
const val ENDPOINT_LIBRUS_API_NORMAL_GRADES = 1031
const val ENDPOINT_LIBRUS_API_POINT_GRADES = 1032
const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES = 1033
@ -97,6 +98,7 @@ val LibrusFeatures = listOf(
ENDPOINT_LIBRUS_API_TEXT_GC to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GC to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_BEHAVIOUR_GC to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_NORMAL_GRADES to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_POINT_GRADES to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES to LOGIN_METHOD_LIBRUS_API,

View File

@ -32,6 +32,13 @@ open class LibrusApi(open val data: DataLibrus) {
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (response?.code() == HTTP_UNAVAILABLE) {
data.error(ApiError(tag, ERROR_LIBRUS_API_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (json == null && response?.parserErrorBody == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
@ -104,6 +111,7 @@ open class LibrusApi(open val data: DataLibrus) {
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_UNAVAILABLE)
.callback(callback)
.build()
.enqueue()

View File

@ -85,6 +85,10 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
LibrusApiGrades(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_grade_comments)
LibrusApiGradeComments(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NORMAL_GC -> {
data.startProgress(R.string.edziennik_progress_endpoint_grade_categories)
LibrusApiGradeCategories(data, onSuccess)

View File

@ -15,7 +15,6 @@ import org.jsoup.parser.Parser
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
@ -43,19 +42,19 @@ open class LibrusMessages(open val data: DataLibrus) {
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (text.isNullOrEmpty()) {
data.error(ApiError(LibrusSynergia.TAG, ERROR_RESPONSE_EMPTY)
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
// TODO: Finish error handling
if ("error" in text) {
when ("<type>(.*)</type>".toRegex().find(text)?.get(1)) {
"eAccessDeny" -> data.error(ApiError(tag, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED)
.withResponse(response)
.withApiResponse(text))
}
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)
}
try {

View File

@ -14,7 +14,7 @@ import pl.szczodrzynski.edziennik.utils.Utils.d
open class LibrusSynergia(open val data: DataLibrus) {
companion object {
const val TAG = "LibrusSynergia"
private const val TAG = "LibrusSynergia"
}
val profileId

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-20
*/
package pl.szczodrzynski.edziennik.api.v2.librus.data.api
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
class LibrusApiGradeComments(override val data: DataLibrus,
val onSuccess: () -> Unit) : LibrusApi(data) {
companion object {
const val TAG = "LibrusApiGradeComments"
}
init {
apiGet(TAG, "Grades/Comments") { json ->
json.getJsonArray("Comments")?.asJsonObjectList()?.forEach { comment ->
val id = comment.getLong("Id") ?: return@forEach
val text = comment.getString("Text")
val gradeCategoryObject = GradeCategory(
profileId,
id,
-1f,
-1,
text
).apply {
type = GradeCategory.TYPE_COMMENT
}
data.gradeCategories.put(id, gradeCategoryObject)
}
data.setSyncNext(ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS, SYNC_ALWAYS)
onSuccess()
}
}
}

View File

@ -6,6 +6,7 @@ import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade
import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
@ -42,12 +43,21 @@ class LibrusApiGrades(override val data: DataLibrus,
weight = 0f
}
val description = grade.getJsonArray("Comments")?.asJsonObjectList()?.let { comments ->
if (comments.isNotEmpty()) {
data.gradeCategories.singleOrNull {
it.type == GradeCategory.TYPE_COMMENT
&& it.categoryId == comments[0].asJsonObject.getLong("Id")
}?.text
} else null
} ?: ""
val gradeObject = Grade(
profileId,
id,
categoryName,
color,
"",
description,
name,
value,
weight,

View File

@ -16,8 +16,7 @@ import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
import java.net.HttpURLConnection.*
class LibrusLoginApi {
companion object {
@ -117,6 +116,13 @@ class LibrusLoginApi {
private val tokenCallback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (response?.code() == HTTP_UNAVAILABLE) {
data.error(ApiError(TAG, ERROR_LIBRUS_API_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (json == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
@ -176,6 +182,7 @@ class LibrusLoginApi {
.post()
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_UNAVAILABLE)
.callback(tokenCallback)
.build()
.enqueue()

View File

@ -6,20 +6,57 @@ package pl.szczodrzynski.edziennik.api.v2.librus.login
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "LoginLibrusMessages"
}
private val callback by lazy { object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
val location = response?.headers()?.get("Location")
when {
location?.contains("MultiDomainLogon") == true -> loginWithSynergia(location)
location?.contains("AutoLogon") == true -> {
saveSessionId(response, text)
onSuccess()
}
text?.contains("<status>ok</status>") == true -> {
saveSessionId(response, text)
onSuccess()
}
text?.contains("<message>Niepoprawny login i/lub hasło.</message>") == true -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text?.contains("stop.png") == true -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text?.contains("eAccessDeny") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("OffLine") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text?.contains("<status>error</status>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text?.contains("<type>eVarWhitThisNameNotExists</type>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("<error>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}}
init { run {
if (data.profile == null) {
data.error(ApiError(TAG, ERROR_PROFILE_MISSING))
@ -41,7 +78,7 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_SYNERGIA)) {
loginWithSynergia()
}
else if (data.apiLogin != null && data.apiPassword != null && false) {
else if (data.apiLogin != null && data.apiPassword != null) {
loginWithCredentials()
}
else {
@ -54,7 +91,44 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
* XML (Flash messages website) login method. Uses a Synergia login and password.
*/
private fun loginWithCredentials() {
d(TAG, "Request: Librus/Login/Messages - $LIBRUS_MESSAGES_URL/Login")
val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val doc = docBuilder.newDocument()
val serviceElement = doc.createElement("service")
val headerElement = doc.createElement("header")
val dataElement = doc.createElement("data")
val loginElement = doc.createElement("login")
loginElement.appendChild(doc.createTextNode(data.apiLogin))
dataElement.appendChild(loginElement)
val passwordElement = doc.createElement("login")
passwordElement.appendChild(doc.createTextNode(data.apiPassword))
dataElement.appendChild(passwordElement)
val keyStrokeElement = doc.createElement("KeyStroke")
val keysElement = doc.createElement("Keys")
val upElement = doc.createElement("Up")
keysElement.appendChild(upElement)
val downElement = doc.createElement("Down")
keysElement.appendChild(downElement)
keyStrokeElement.appendChild(keysElement)
dataElement.appendChild(keyStrokeElement)
serviceElement.appendChild(headerElement)
serviceElement.appendChild(dataElement)
doc.appendChild(serviceElement)
val transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
val stringWriter = StringWriter()
transformer.transform(DOMSource(doc), StreamResult(stringWriter))
val requestXml = stringWriter.toString()
Request.builder()
.url("$LIBRUS_MESSAGES_URL/Login")
.userAgent(SYNERGIA_USER_AGENT)
.setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML)
.post()
.callback(callback)
.build()
.enqueue()
}
/**
@ -63,37 +137,6 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
private fun loginWithSynergia(url: String = "https://synergia.librus.pl/wiadomosci2") {
d(TAG, "Request: Librus/Login/Messages - $url")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
val location = response?.headers()?.get("Location")
when {
location?.contains("MultiDomainLogon") == true -> loginWithSynergia(location)
location?.contains("AutoLogon") == true -> {
var sessionId = data.app.cookieJar.getCookie("wiadomosci.librus.pl", "DZIENNIKSID")
sessionId = sessionId?.replace("-MAINT", "")
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID)
.withResponse(response)
.withApiResponse(text))
return
}
data.messagesSessionId = sessionId
data.messagesSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */
onSuccess()
}
text?.contains("eAccessDeny") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("stop.png") == true -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(url)
.userAgent(SYNERGIA_USER_AGENT)
@ -103,4 +146,17 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
.build()
.enqueue()
}
private fun saveSessionId(response: Response?, text: String?) {
var sessionId = data.app.cookieJar.getCookie("wiadomosci.librus.pl", "DZIENNIKSID")
sessionId = sessionId?.replace("-MAINT", "") // dunno what's this
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID)
.withResponse(response)
.withApiResponse(text))
return
}
data.messagesSessionId = sessionId
data.messagesSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */
}
}

View File

@ -99,6 +99,14 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.post()
.callback(object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) {
val location = response.headers()?.get("Location")
if (location == "http://localhost/bar?command=close") {
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (json == null) {
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
@ -120,7 +128,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
override fun onFailure(response: Response, throwable: Throwable) {
if (response.code() == 403 || response.code() == 401) {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_INVALID)
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN)
.withResponse(response)
.withThrowable(throwable))
return

View File

@ -7,7 +7,6 @@ package pl.szczodrzynski.edziennik.api.v2.librus.login
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.api.v2.*
@ -16,7 +15,6 @@ import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection
@ -86,6 +84,13 @@ class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Un
val callback = object : TextCallbackHandler() {
override fun onSuccess(json: String?, response: Response?) {
val location = response?.headers()?.get("Location")
if (location?.endsWith("przerwa_techniczna") == true) {
data.error(ApiError(TAG, ERROR_LIBRUS_SYNERGIA_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (location?.endsWith("centrum_powiadomien") == true) {
val sessionId = data.app.cookieJar.getCookie("synergia.librus.pl", "DZIENNIKSID")
if (sessionId == null) {

View File

@ -10,8 +10,10 @@ import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.MobidziennikData
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.web.MobidziennikWebGetMessage
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.firstlogin.MobidziennikFirstLogin
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.login.MobidziennikLogin
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.login.MobidziennikLoginWeb
import pl.szczodrzynski.edziennik.api.v2.mobidziennikLoginMethods
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.prepare
@ -63,7 +65,11 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
}
override fun getMessage(message: MessageFull) {
MobidziennikLoginWeb(data) {
MobidziennikWebGetMessage(data, message) {
completed()
}
}
}
override fun markAllAnnouncementsAsRead() {

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-18.
*/
package pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.web
import org.greenrobot.eventbus.EventBus
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.api.v2.Regexes
import pl.szczodrzynski.edziennik.api.v2.events.MessageGetEvent
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.Utils.monthFromName
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikWebGetMessage(
override val data: DataMobidziennik,
private val message: MessageFull,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebGetMessage"
}
init {
val typeUrl = if (message.type == Message.TYPE_SENT)
"wiadwyslana"
else
"wiadodebrana"
webGet(TAG, "/dziennik/$typeUrl/?id=${message.id}") { text ->
MobidziennikLuckyNumberExtractor(data, text)
val messageRecipientList = mutableListOf<MessageRecipientFull>()
val doc = Jsoup.parse(text)
val content = doc.select("#content").first()
val body = content.select(".wiadomosc_tresc").first()
if (message.type == TYPE_RECEIVED) {
var readDate = System.currentTimeMillis()
Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(body.html())?.let {
val date = Date(
it[3].toIntOrNull() ?: 2019,
monthFromName(it[2]),
it[1].toIntOrNull() ?: 1
)
val time = Time.fromH_m_s(
it[4] // TODO blank string safety
)
readDate = date.combineWith(time)
}
val recipient = MessageRecipientFull(
profileId,
-1,
-1,
readDate,
message.id
)
recipient.fullName = profile?.accountNameLong ?: profile?.studentNameLong
messageRecipientList.add(recipient)
} else {
message.senderId = -1
message.senderReplyId = -1
content.select("table.spis tr:has(td)")?.forEach { recipientEl ->
val senderEl = recipientEl.select("td:eq(0)").first()
val senderName = senderEl.text()
val teacher = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }
val receiverId = teacher?.id ?: -1
var readDate = 0L
val isReadEl = recipientEl.select("td:eq(2)").first()
if (isReadEl.ownText() != "NIE") {
val readDateEl = recipientEl.select("td:eq(3) small").first()
Regexes.MOBIDZIENNIK_MESSAGE_SENT_READ_DATE.find(readDateEl.ownText())?.let {
val date = Date(
it[3].toIntOrNull() ?: 2019,
monthFromName(it[2]),
it[1].toIntOrNull() ?: 1
)
val time = Time.fromH_m_s(
it[4] // TODO blank string safety
)
readDate = date.combineWith(time)
}
}
val recipient = MessageRecipientFull(
profileId,
receiverId,
-1,
readDate,
message.id
)
recipient.fullName = teacher?.fullName ?: "?"
messageRecipientList.add(recipient)
}
}
// this line removes the sender and read date details
body.select("div").remove()
// this needs to be at the end
message.apply {
this.body = body.html()
clearAttachments()
content.select("ul li").map { it.select("a").first() }.forEach {
val attachmentName = it.ownText()
Regexes.MOBIDZIENNIK_MESSAGE_ATTACHMENT.find(it.outerHtml())?.let { match ->
val attachmentId = match[1].toLong()
var size = match[2].toFloatOrNull() ?: 0f
when (match[3]) {
"K" -> size *= 1024f
"M" -> size *= 1024f * 1024f
"G" -> size *= 1024f * 1024f * 1024f
}
message.addAttachment(attachmentId, attachmentName, size.toLong())
}
}
}
if (!message.seen) { // TODO discover why this monstrosity instead of MetadataDao.setSeen
data.messageMetadataList.add(Metadata(
message.profileId,
Metadata.TYPE_MESSAGE,
message.id,
true,
true,
message.addedDate
))
}
message.recipients = messageRecipientList
data.messageRecipientList.addAll(messageRecipientList)
data.messageList.add(message)
EventBus.getDefault().postSticky(MessageGetEvent(message))
onSuccess()
}
}
}

View File

@ -7,15 +7,14 @@ package pl.szczodrzynski.edziennik.api.v2.vulcan
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.api.v2.models.Data
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isApiLoginValid() = apiCertificateExpiryTime-30 > currentTimeUnix()
&& apiCertificateKey.isNotNullNorEmpty()
fun isApiLoginValid() = /*apiCertificateExpiryTime-30 > currentTimeUnix()
&&*/ apiCertificateKey.isNotNullNorEmpty()
&& apiCertificatePrivate.isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.api.v2.vulcan.data.api
import androidx.core.util.set
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.api.v2.Regexes
import pl.szczodrzynski.edziennik.api.v2.VULCAN_API_ENDPOINT_TIMETABLE
@ -14,6 +15,7 @@ import pl.szczodrzynski.edziennik.api.v2.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.utils.Utils.crc16
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -24,163 +26,180 @@ class VulcanApiTimetable(override val data: DataVulcan, val onSuccess: () -> Uni
const val TAG = "VulcanApiTimetable"
}
init {
data.profile?.also { profile ->
val currentWeekStart = Date.getToday().let { it.stepForward(0, 0, -it.weekDay) }
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
init { data.profile?.also { profile ->
val currentWeekStart = Date.getToday().let { it.stepForward(0, 0, -it.weekDay) }
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
apiGet(TAG, VULCAN_API_ENDPOINT_TIMETABLE, parameters = mapOf(
"DataPoczatkowa" to weekStart.stringY_m_d,
"DataKoncowa" to weekEnd.stringY_m_d,
"IdUczen" to data.studentId,
"IdOddzial" to data.studentClassId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
)) { json, _ ->
val dates: MutableSet<Int> = mutableSetOf()
val lessons: MutableList<Lesson> = mutableListOf()
apiGet(TAG, VULCAN_API_ENDPOINT_TIMETABLE, parameters = mapOf(
"DataPoczatkowa" to weekStart.stringY_m_d,
"DataKoncowa" to weekEnd.stringY_m_d,
"IdUczen" to data.studentId,
"IdOddzial" to data.studentClassId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
)) { json, _ ->
val dates: MutableSet<Int> = mutableSetOf()
val lessons: MutableList<Lesson> = mutableListOf()
json.getJsonArray("Data")?.asJsonObjectList()?.forEach { lesson ->
val lessonDate = Date.fromY_m_d(lesson.getString("DzienTekst"))
val lessonNumber = lesson.getInt("NumerLekcji")
val lessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber }
val startTime = lessonRange?.startTime
val endTime = lessonRange?.endTime
val teacherId = lesson.getLong("IdPracownik")
val teamId = data.studentClassId.toLong()
val classroom = lesson.getString("Classroom")
json.getJsonArray("Data")?.asJsonObjectList()?.forEach { lesson ->
if (lesson.getBoolean("PlanUcznia") != true)
return@forEach
val lessonDate = Date.fromY_m_d(lesson.getString("DzienTekst"))
val lessonNumber = lesson.getInt("NumerLekcji")
val lessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber }
val startTime = lessonRange?.startTime
val endTime = lessonRange?.endTime
val teacherId = lesson.getLong("IdPracownik")
val classroom = lesson.getString("Sala")
val oldTeacherId = lesson.getLong("IdPracownikOld")
val oldTeacherId = lesson.getLong("IdPracownikOld")
val changeAnnotation = lesson.getString("AdnotacjaOZmianie") ?: ""
val type = when {
changeAnnotation.startsWith("(przeniesiona z") -> Lesson.TYPE_SHIFTED_TARGET
changeAnnotation.startsWith("(przeniesiona na") -> Lesson.TYPE_SHIFTED_SOURCE
changeAnnotation.startsWith("(zastępstwo") -> Lesson.TYPE_CHANGE
lesson.getBoolean("PrzekreslonaNazwa") == true -> Lesson.TYPE_CANCELLED
else -> Lesson.TYPE_NORMAL
val changeAnnotation = lesson.getString("AdnotacjaOZmianie") ?: ""
val type = when {
changeAnnotation.startsWith("(przeniesiona z") -> Lesson.TYPE_SHIFTED_TARGET
changeAnnotation.startsWith("(przeniesiona na") -> Lesson.TYPE_SHIFTED_SOURCE
changeAnnotation.startsWith("(zastępstwo") -> Lesson.TYPE_CHANGE
lesson.getBoolean("PrzekreslonaNazwa") == true -> Lesson.TYPE_CANCELLED
else -> Lesson.TYPE_NORMAL
}
val teamId = lesson.getString("PodzialSkrot")?.let { teamName ->
val name = "${data.teamClass?.name} $teamName"
val id = name.crc16().toLong()
var team = data.teamList.singleOrNull { it.name == name }
if (team == null) {
team = Team(
profileId,
id,
name,
Team.TYPE_VIRTUAL,
"${data.schoolName}:$name",
teacherId ?: oldTeacherId ?: -1
)
data.teamList[id] = team
}
team.id
} ?: data.studentClassId.toLong()
val subjectId = lesson.getLong("IdPrzedmiot")?.let {
when (it) {
0L -> {
val subjectName = lesson.getString("PrzedmiotNazwa") ?: ""
val subjectId = lesson.getLong("IdPrzedmiot")?.let {
when (it) {
0L -> {
val subjectName = lesson.getString("PrzedmiotNazwa") ?: ""
data.subjectList.singleOrNull { subject -> subject.longName == subjectName }?.id
?: {
/**
* CREATE A NEW SUBJECT IF IT DOESN'T EXIST
*/
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 subjectObject = Subject(
profileId,
-1 * crc16(subjectName.toByteArray()).toLong(),
subjectName,
subjectName
)
data.subjectList.put(subjectObject.id, subjectObject)
subjectObject.id
}.invoke()
}
else -> it
}
}
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
val lessonObject = Lesson(profileId, id).apply {
this.type = type
when (type) {
Lesson.TYPE_NORMAL, Lesson.TYPE_CHANGE, Lesson.TYPE_SHIFTED_TARGET -> {
this.date = lessonDate
this.lessonNumber = lessonNumber
this.startTime = startTime
this.endTime = endTime
this.subjectId = subjectId
this.teacherId = teacherId
this.teamId = teamId
this.classroom = classroom
this.oldTeacherId = oldTeacherId
}
Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> {
this.oldDate = lessonDate
this.oldLessonNumber = lessonNumber
this.oldStartTime = startTime
this.oldEndTime = endTime
this.oldSubjectId = subjectId
this.oldTeacherId = teacherId
this.oldTeamId = teamId
this.oldClassroom = classroom
}
}
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
if (type == Lesson.TYPE_SHIFTED_SOURCE || type == Lesson.TYPE_SHIFTED_TARGET) {
val shift = Regexes.VULCAN_SHITFT_ANNOTATION.find(changeAnnotation)
val oldLessonNumber = shift?.get(2)?.toInt()
val oldLessonDate = shift?.get(3)?.let { Date.fromd_m_Y(it) }
val lessonObject = Lesson(profileId, id).apply {
this.type = type
val oldLessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == oldLessonNumber }
val oldStartTime = oldLessonRange?.startTime
val oldEndTime = oldLessonRange?.endTime
when (type) {
Lesson.TYPE_NORMAL, Lesson.TYPE_CHANGE, Lesson.TYPE_SHIFTED_TARGET -> {
this.date = lessonDate
this.lessonNumber = lessonNumber
this.startTime = startTime
this.endTime = endTime
this.subjectId = subjectId
this.teacherId = teacherId
this.teamId = teamId
this.classroom = classroom
this.oldTeacherId = oldTeacherId
Lesson.TYPE_SHIFTED_SOURCE -> {
this.lessonNumber = oldLessonNumber
this.date = oldLessonDate
this.startTime = oldStartTime
this.endTime = oldEndTime
}
Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> {
this.oldDate = lessonDate
this.oldLessonNumber = lessonNumber
this.oldStartTime = startTime
this.oldEndTime = endTime
this.oldSubjectId = subjectId
this.oldTeacherId = teacherId
this.oldTeamId = teamId
this.oldClassroom = classroom
}
}
if (type == Lesson.TYPE_SHIFTED_SOURCE || type == Lesson.TYPE_SHIFTED_TARGET) {
val shift = Regexes.VULCAN_SHITFT_ANNOTATION.find(changeAnnotation)
val oldLessonNumber = shift?.get(2)?.toInt()
val oldLessonDate = shift?.get(3)?.let { Date.fromd_m_Y(it) }
val oldLessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == oldLessonNumber }
val oldStartTime = oldLessonRange?.startTime
val oldEndTime = oldLessonRange?.endTime
when (type) {
Lesson.TYPE_SHIFTED_SOURCE -> {
this.lessonNumber = oldLessonNumber
this.date = oldLessonDate
this.startTime = oldStartTime
this.endTime = oldEndTime
}
Lesson.TYPE_SHIFTED_TARGET -> {
this.oldLessonNumber = oldLessonNumber
this.oldDate = oldLessonDate
this.oldStartTime = oldStartTime
this.oldEndTime = oldEndTime
}
Lesson.TYPE_SHIFTED_TARGET -> {
this.oldLessonNumber = oldLessonNumber
this.oldDate = oldLessonDate
this.oldStartTime = oldStartTime
this.oldEndTime = oldEndTime
}
}
}
if (type != Lesson.TYPE_NORMAL) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
id,
profile.empty,
profile.empty,
System.currentTimeMillis()
))
}
dates.add(lessonDate.value)
lessons.add(lessonObject)
}
val date: Date = weekStart.clone()
while (date <= weekEnd) {
if (!dates.contains(date.value)) {
lessons.add(Lesson(profileId, date.value.toLong()).apply {
this.type = Lesson.TYPE_NO_LESSONS
this.date = date.clone()
})
}
date.stepForward(0, 0, 1)
if (type != Lesson.TYPE_NORMAL) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
id,
profile.empty,
profile.empty,
System.currentTimeMillis()
))
}
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.lessonNewList.addAll(lessons)
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_VULCAN_API_TIMETABLE, SYNC_ALWAYS)
onSuccess()
dates.add(lessonDate.value)
lessons.add(lessonObject)
}
val date: Date = weekStart.clone()
while (date <= weekEnd) {
if (!dates.contains(date.value)) {
lessons.add(Lesson(profileId, date.value.toLong()).apply {
this.type = Lesson.TYPE_NO_LESSONS
this.date = date.clone()
})
}
date.stepForward(0, 0, 1)
}
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.lessonNewList.addAll(lessons)
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_VULCAN_API_TIMETABLE, SYNC_ALWAYS)
onSuccess()
}
}
}}
}

View File

@ -1,10 +1,10 @@
package pl.szczodrzynski.edziennik.data.db.modules.grades;
import androidx.room.Entity;
import java.util.ArrayList;
import java.util.List;
import androidx.room.Entity;
@Entity(tableName = "gradeCategories",
primaryKeys = {"profileId", "categoryId"})
public class GradeCategory {
@ -25,6 +25,9 @@ public class GradeCategory {
*/
public int type = 0;
public static final int TYPE_NORMAL = 0;
public static final int TYPE_COMMENT = 1;
public GradeCategory(int profileId, long categoryId, float weight, int color, String text) {
this.profileId = profileId;
this.categoryId = categoryId;

View File

@ -38,16 +38,16 @@ interface TimetableDao {
@Query("DELETE FROM timetable WHERE profileId = :profileId")
fun clear(profileId: Int)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom)")
@Query("DELETE FROM timetable WHERE profileId = :profileId AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))")
fun clearFromDate(profileId: Int, dateFrom: Date)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo)")
@Query("DELETE FROM timetable WHERE profileId = :profileId AND ((type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo))")
fun clearToDate(profileId: Int, dateTo: Date)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo)")
@Query("DELETE FROM timetable WHERE profileId = :profileId AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))")
fun clearBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date)
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND (type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date)
WHERE timetable.profileId = :profileId AND ((type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date))
ORDER BY id, type
""")
fun getForDate(profileId: Int, date: Date) : LiveData<List<LessonFull>>
@ -58,7 +58,15 @@ interface TimetableDao {
ORDER BY id, type
LIMIT 1
""")
fun getNextWithSubject(profileId: Int, today: Date, subjectId: Long) : LiveData<LessonFull>
fun getNextWithSubject(profileId: Int, today: Date, subjectId: Long) : LiveData<LessonFull?>
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND ((type != 3 AND date > :today) OR ((type = 3 OR type = 1) AND oldDate > :today)) AND timetable.subjectId = :subjectId AND timetable.teamId = :teamId
ORDER BY id, type
LIMIT 1
""")
fun getNextWithSubjectAndTeam(profileId: Int, today: Date, subjectId: Long, teamId: Long): LiveData<LessonFull?>
@Query("""
$QUERY

View File

@ -125,19 +125,22 @@ public class BootReceiver extends BroadcastReceiver {
String updateUrl = result.get("update_url").getAsString();
String updateFilename = result.get("update_filename").getAsString();
boolean updateMandatory = result.get("update_mandatory").getAsBoolean();
boolean updateDirect = result.get("update_direct").getAsBoolean();
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals(updateVersion)) {
app.appConfig.updateVersion = updateVersion;
app.appConfig.updateUrl = updateUrl;
app.appConfig.updateFilename = updateFilename;
app.appConfig.updateMandatory = updateMandatory;
app.appConfig.updateDirect = updateDirect;
app.saveConfig();
}
if (!UPDATES_ON_PLAY_STORE || intent.getBooleanExtra("UserChecked", false)) {
app.notifier.notificationUpdatesShow(
updateVersion,
updateUrl,
updateFilename);
updateFilename,
updateDirect);
}
} else {
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals("")) {
@ -259,7 +262,7 @@ public class BootReceiver extends BroadcastReceiver {
@Override
protected void onHandleIntent(Intent intent) {
if (UPDATES_ON_PLAY_STORE) {
if (UPDATES_ON_PLAY_STORE && !intent.getBooleanExtra("update_direct", false)) {
Utils.openGooglePlay(this, "pl.szczodrzynski.edziennik");
return;
}
@ -270,7 +273,8 @@ public class BootReceiver extends BroadcastReceiver {
app.notifier.notificationUpdatesShow(
intent.getStringExtra("update_version"),
intent.getStringExtra("update_url"),
intent.getStringExtra("update_filename"));
intent.getStringExtra("update_filename"),
intent.getBooleanExtra("update_direct", false));
return;
}
}

View File

@ -154,19 +154,22 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
String updateUrl = remoteMessage.getData().get("update_url");
String updateFilename = remoteMessage.getData().get("update_filename");
boolean updateMandatory = Boolean.parseBoolean(remoteMessage.getData().get("update_mandatory"));
boolean updateDirect = Boolean.parseBoolean(remoteMessage.getData().get("update_direct"));
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals(updateVersion)) {
app.appConfig.updateVersion = updateVersion;
app.appConfig.updateUrl = updateUrl;
app.appConfig.updateFilename = updateFilename;
app.appConfig.updateMandatory = updateMandatory;
app.appConfig.updateDirect = updateDirect;
app.saveConfig("updateVersion", "updateUrl", "updateFilename", "updateMandatory");
}
if (!remoteMessage.getData().containsKey("update_silent")) {
app.notifier.notificationUpdatesShow(
updateVersion,
updateUrl,
updateFilename);
updateFilename,
updateDirect);
}
} else {
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals("")) {

View File

@ -4,19 +4,25 @@
package pl.szczodrzynski.edziennik.ui.dialogs.event
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.modules.events.Event
import pl.szczodrzynski.edziennik.data.db.modules.events.EventType
import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -46,9 +52,11 @@ class EventManualV2Dialog(
private val app by lazy { activity.application as App }
private lateinit var b: DialogEventManualV2Binding
private lateinit var dialog: AlertDialog
private lateinit var event: Event
private var defaultLoaded = false
private lateinit var event: Event
private var customColor: Int? = null
init { run {
if (activity.isFinishing)
return@run
@ -80,6 +88,17 @@ class EventManualV2Dialog(
}*/
}
b.showMore.onClick { // TODO iconics is broken
it.apply {
refreshDrawableState()
if (isChecked)
Anim.expand(b.moreLayout, 200, null)
else
Anim.collapse(b.moreLayout, 200, null)
}
}
loadLists()
}}
@ -114,18 +133,33 @@ class EventManualV2Dialog(
""
)
b.teacherDropdown += teachers.map { TextInputDropDown.Item(it.id, it.fullName, tag = it) }
// get the event type list
val eventTypes = app.db.eventTypeDao().getAllNow(profileId)
b.typeDropdown.clear()
b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) }
}
deferred.await()
b.teamDropdown.isEnabled = true
b.subjectDropdown.isEnabled = true
b.teacherDropdown.isEnabled = true
b.typeDropdown.isEnabled = true
b.typeDropdown.selected?.let { item ->
customColor = (item.tag as EventType).color
}
// copy IDs from event being edited
editingEvent?.let {
b.teamDropdown.select(it.teamId)
b.subjectDropdown.select(it.subjectId)
b.teacherDropdown.select(it.teacherId)
b.typeDropdown.select(it.type)?.let { item ->
customColor = (item.tag as EventType).color
}
if (it.color != -1)
customColor = it.color
}
// copy IDs from the LessonFull
@ -135,6 +169,30 @@ class EventManualV2Dialog(
b.teacherDropdown.select(it.displayTeacherId)
}
b.typeDropdown.setOnChangeListener {
b.typeColor.background.colorFilter = PorterDuffColorFilter((it.tag as EventType).color, PorterDuff.Mode.SRC_ATOP)
customColor = null
return@setOnChangeListener true
}
(customColor ?: Event.COLOR_DEFAULT).let {
b.typeColor.background.colorFilter = PorterDuffColorFilter(it, PorterDuff.Mode.SRC_ATOP)
}
b.typeColor.onClick {
val currentColor = (b.typeDropdown?.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT
val colorPickerDialog = ColorPickerDialog.newBuilder()
.setColor(currentColor)
.create()
colorPickerDialog.setColorPickerDialogListener(
object : ColorPickerDialogListener {
override fun onDialogDismissed(dialogId: Int) {}
override fun onColorSelected(dialogId: Int, color: Int) {
b.typeColor.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
customColor = color
}
})
colorPickerDialog.show(activity.fragmentManager, "color-picker-dialog")
}
loadDates()
}}
@ -205,12 +263,20 @@ class EventManualV2Dialog(
val dates = deferred.await()
b.dateDropdown.clear().append(dates)
editingEvent?.let {
b.dateDropdown.select(it.eventDate.value.toLong())
editingEvent?.eventDate?.let {
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
defaultLesson?.let {
b.dateDropdown.select(it.displayDate?.value?.toLong())
defaultLesson?.displayDate?.let {
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
if (b.dateDropdown.selected == null) {
@ -223,22 +289,24 @@ class EventManualV2Dialog(
when {
// next lesson with specified subject
item.id < -1 -> {
app.db.timetableDao().getNextWithSubject(profileId, Date.getToday(), -item.id).observeOnce(activity, Observer {
val teamId = defaultLesson?.teamId ?: -1
val selectedLessonDate = defaultLesson?.date ?: Date.getToday()
when (teamId) {
-1L -> app.db.timetableDao().getNextWithSubject(profileId, selectedLessonDate, -item.id)
else -> app.db.timetableDao().getNextWithSubjectAndTeam(profileId, selectedLessonDate, -item.id, teamId)
}.observeOnce(activity, Observer {
val lessonDate = it?.displayDate ?: return@Observer
b.dateDropdown.selected = TextInputDropDown.Item(
b.dateDropdown.select(TextInputDropDown.Item(
lessonDate.value.toLong(),
lessonDate.formattedString,
tag = lessonDate
)
// TODO load correct hour when selecting next lesson
b.dateDropdown.updateText()
it.let {
b.teamDropdown.select(it.displayTeamId)
b.subjectDropdown.select(it.displaySubjectId)
b.teacherDropdown.select(it.displayTeacherId)
}
))
b.teamDropdown.select(it.displayTeamId)
b.subjectDropdown.select(it.displaySubjectId)
b.teacherDropdown.select(it.displayTeacherId)
defaultLoaded = false
loadHours()
loadHours(it.displayStartTime)
})
return@setOnChangeListener false
}
@ -246,17 +314,17 @@ class EventManualV2Dialog(
item.id == -1L -> {
MaterialDatePicker.Builder
.datePicker()
.setSelection((b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) } ?: Date.getToday()).inMillis)
.setSelection((b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) }
?: Date.getToday()).inMillis)
.build()
.apply {
addOnPositiveButtonClickListener {
val dateSelected = Date.fromMillis(it)
b.dateDropdown.selected = TextInputDropDown.Item(
b.dateDropdown.select(TextInputDropDown.Item(
dateSelected.value.toLong(),
dateSelected.formattedString,
tag = dateSelected
)
b.dateDropdown.updateText()
))
loadHours()
}
show(this@EventManualV2Dialog.activity.supportFragmentManager, "MaterialDatePicker")
@ -276,7 +344,7 @@ class EventManualV2Dialog(
loadHours()
}}
private fun loadHours() {
private fun loadHours(defaultHour: Time? = null) {
b.timeDropdown.isEnabled = false
// get the selected date
val date = b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) } ?: return
@ -302,7 +370,8 @@ class EventManualV2Dialog(
lesson.displayStartTime?.stringHM ?: "",
lesson.displaySubjectName?.let {
when {
lesson.type == Lesson.TYPE_CANCELLED -> it.asStrikethroughSpannable()
lesson.type == Lesson.TYPE_CANCELLED
|| lesson.type == Lesson.TYPE_SHIFTED_SOURCE -> it.asStrikethroughSpannable()
lesson.type != Lesson.TYPE_NORMAL -> it.asItalicSpannable()
else -> it
}
@ -338,6 +407,10 @@ class EventManualV2Dialog(
defaultLesson?.let {
b.timeDropdown.select(it.displayStartTime?.value?.toLong())
}
defaultHour?.let {
b.timeDropdown.select(it.value.toLong())
}
}
defaultLoaded = true
b.timeDropdown.isEnabled = true
@ -345,16 +418,16 @@ class EventManualV2Dialog(
// attach a listener to time dropdown
b.timeDropdown.setOnChangeListener { item ->
when {
// custom start hour
item.id == -1L -> {
return@setOnChangeListener false
}
// no lessons this day
item.id == -2L -> {
b.timeDropdown.deselect()
return@setOnChangeListener false
}
// custom start hour
item.id == -1L -> {
return@setOnChangeListener false
}
// selected a specific lesson
else -> {
if (item.tag is LessonFull) {
@ -383,4 +456,4 @@ class EventManualV2Dialog(
private fun saveEvent() {
}
}
}

View File

@ -132,28 +132,28 @@ class LessonDetailsDialog(
if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldSubjectId != null && lesson.subjectId != lesson.oldSubjectId) {
b.oldSubjectName = lesson.oldSubjectName
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.subjectId != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displaySubjectId != null) {
b.subjectName = lesson.subjectName
}
if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeacherId != null && lesson.teacherId != lesson.oldTeacherId) {
b.oldTeacherName = lesson.oldTeacherName
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.teacherId != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeacherId != null) {
b.teacherName = lesson.teacherName
}
if (lesson.oldClassroom != null && lesson.classroom != lesson.oldClassroom) {
b.oldClassroom = lesson.oldClassroom
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.classroom != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayClassroom != null) {
b.classroom = lesson.classroom
}
if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeamId != null && lesson.teamId != lesson.oldTeamId) {
b.oldTeamName = lesson.oldTeamName
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.teamId != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeamId != null) {
b.teamName = lesson.teamName
}
}

View File

@ -12,8 +12,8 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.FragmentHomeworkBinding
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.databinding.FragmentHomeworkBinding
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.utils.Themes
@ -84,7 +84,11 @@ class HomeworkFragment : Fragment() {
b.viewPager.currentItem = pageSelection
b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrollStateChanged(state: Int) {
if (b.refreshLayout != null) {
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
pageSelection = position

View File

@ -19,7 +19,7 @@ import pl.szczodrzynski.edziennik.api.v2.models.ApiError;
import pl.szczodrzynski.edziennik.databinding.FragmentLoginLibrusBinding;
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar;
import static pl.szczodrzynski.edziennik.api.v2.ErrorsKt.ERROR_LOGIN_DATA_INVALID;
import static pl.szczodrzynski.edziennik.api.v2.ErrorsKt.ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN;
import static pl.szczodrzynski.edziennik.api.v2.ErrorsKt.ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED;
import static pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_LIBRUS;
@ -57,7 +57,7 @@ public class LoginLibrusFragment extends Fragment {
ApiError error = LoginActivity.error;
if (error != null) {
switch (error.getErrorCode()) {
case ERROR_LOGIN_DATA_INVALID:
case ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN:
b.loginPasswordLayout.setError(getString(R.string.login_error_incorrect_login_or_password));
break;
case ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED:

View File

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

View File

@ -11,6 +11,7 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
@ -18,16 +19,25 @@ import pl.szczodrzynski.edziennik.api.v2.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
class TimetableFragment : Fragment() {
class TimetableFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "TimetableFragment"
const val ACTION_SCROLL_TO_DATE = "pl.szczodrzynski.edziennik.timetable.SCROLL_TO_DATE"
const val DEFAULT_START_HOUR = 6
const val DEFAULT_END_HOUR = 19
var pageSelection: Date? = null
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2Binding
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private var fabShown = false
private val items = mutableListOf<Date>()
@ -36,11 +46,13 @@ class TimetableFragment : Fragment() {
if (context == null)
return null
app = activity.application as App
job = Job()
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 = FragmentTimetableV2Binding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
@ -62,48 +74,64 @@ class TimetableFragment : Fragment() {
activity.unregisterReceiver(broadcastReceiver)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { launch {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
return@launch
if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS && app.profile.getLoginData("timetableNotPublic", false)) {
b.timetableLayout.visibility = View.GONE
b.timetableNotPublicLayout.visibility = View.VISIBLE
return
return@launch
}
b.timetableLayout.visibility = View.VISIBLE
b.timetableNotPublicLayout.visibility = View.GONE
items.clear()
val monthDayCount = listOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val today = Date.getToday().value
val yearStart = app.profile.dateSemester1Start?.clone() ?: return
val yearEnd = app.profile.dateYearEnd ?: return
while (yearStart.value <= yearEnd.value) {
items += yearStart.clone()
var maxDays = monthDayCount[yearStart.month-1]
if (yearStart.month == 2 && yearStart.isLeap)
maxDays++
yearStart.day++
if (yearStart.day > maxDays) {
yearStart.day = 1
yearStart.month++
}
if (yearStart.month > 12) {
yearStart.month = 1
yearStart.year++
}
}
var startHour = DEFAULT_START_HOUR
var endHour = DEFAULT_END_HOUR
val deferred = async(Dispatchers.Default) {
items.clear()
val pagerAdapter = TimetablePagerAdapter(fragmentManager ?: return, items)
val monthDayCount = listOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val yearStart = app.profile.dateSemester1Start?.clone() ?: return@async
val yearEnd = app.profile.dateYearEnd ?: return@async
while (yearStart.value <= yearEnd.value) {
items += yearStart.clone()
var maxDays = monthDayCount[yearStart.month-1]
if (yearStart.month == 2 && yearStart.isLeap)
maxDays++
yearStart.day++
if (yearStart.day > maxDays) {
yearStart.day = 1
yearStart.month++
}
if (yearStart.month > 12) {
yearStart.month = 1
yearStart.year++
}
}
val lessonRanges = app.db.lessonRangeDao().getAllNow(App.profileId)
startHour = lessonRanges.map { it.startTime.hour }.min() ?: DEFAULT_START_HOUR
endHour = lessonRanges.map { it.endTime.hour }.max()?.plus(1) ?: DEFAULT_END_HOUR
}
deferred.await()
val pagerAdapter = TimetablePagerAdapter(
fragmentManager ?: return@launch,
items,
startHour,
endHour
)
b.viewPager.offscreenPageLimit = 2
b.viewPager.adapter = pagerAdapter
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
if (b.refreshLayout != null) {
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
@ -111,6 +139,7 @@ class TimetableFragment : Fragment() {
}
override fun onPageSelected(position: Int) {
pageSelection = items[position]
activity.navView.bottomBar.fabEnable = items[position].value != today
if (activity.navView.bottomBar.fabEnable && !fabShown) {
activity.gainAttentionFAB()
@ -123,10 +152,10 @@ class TimetableFragment : Fragment() {
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, false)
//activity.navView.bottomBar.fabEnable = true
activity.navView.bottomBar.fabExtendedText = getString(pl.szczodrzynski.edziennik.R.string.timetable_today)
activity.navView.bottomBar.fabExtendedText = getString(R.string.timetable_today)
activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon.cmd_calendar_today
activity.navView.setFabOnClickListener(View.OnClickListener {
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, true)
})
}
}}
}

View File

@ -8,7 +8,12 @@ import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.day.TimetableDayFragme
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
class TimetablePagerAdapter(val fragmentManager: FragmentManager, val items: List<Date>) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
class TimetablePagerAdapter(
fragmentManager: FragmentManager,
private val items: List<Date>,
private val startHour: Int,
private val endHour: Int
) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
companion object {
private const val TAG = "TimetablePagerAdapter"
}
@ -21,6 +26,8 @@ class TimetablePagerAdapter(val fragmentManager: FragmentManager, val items: Lis
return TimetableDayFragment().apply {
arguments = Bundle().apply {
putInt("date", items[position].value)
putInt("startHour", startHour)
putInt("endHour", endHour)
}
}
/*return TimetableDayFragment().apply {

View File

@ -7,42 +7,97 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.linkedin.android.tachyon.DayView
import com.linkedin.android.tachyon.DayViewConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.api.v2.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2DayBinding
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment.Companion.DEFAULT_END_HOUR
import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment.Companion.DEFAULT_START_HOUR
import pl.szczodrzynski.edziennik.utils.ListenerScrollView
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.getColorFromAttr
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
class TimetableDayFragment() : Fragment() {
class TimetableDayFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "TimetableDayFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2DayBinding
private lateinit var inflater: AsyncLayoutInflater
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var date: Date
private var startHour = DEFAULT_START_HOUR
private var endHour = DEFAULT_END_HOUR
private var firstEventMinute = 24*60
// find SwipeRefreshLayout in the hierarchy
private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) }
// the day ScrollView
private val dayScrollDelegate = lazy {
val dayScroll = ListenerScrollView(context!!)
dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
dayScroll.setOnRefreshLayoutEnabledListener { enabled ->
refreshLayout?.isEnabled = enabled
}
dayScroll
}
private val dayScroll by dayScrollDelegate
// the lesson DayView
private val dayView by lazy {
val dayView = DayView(context!!, DayViewConfig(
startHour = startHour,
endHour = endHour,
dividerHeight = 1.dp,
halfHourHeight = 60.dp,
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
hourLabelWidth = 40.dp,
hourLabelMarginEnd = 10.dp,
eventMargin = 2.dp
), true)
dayView.setPadding(10.dp)
dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dayScroll.addView(dayView)
dayView
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
if (context == null)
return null
app = activity.application as App
b = FragmentTimetableV2DayBinding.inflate(inflater)
job = Job()
this.inflater = AsyncLayoutInflater(context!!)
date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday()
return b.root
startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR
endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR
return FrameLayout(activity).apply {
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -52,45 +107,42 @@ class TimetableDayFragment() : Fragment() {
Log.d(TAG, "onViewCreated, date=$date")
// Inflate a label view for each hour the day view will display
val hourLabelViews = ArrayList<View>()
for (i in b.day.startHour..b.day.endHour) {
val hourLabelView = layoutInflater.inflate(R.layout.timetable_hour_label, b.day, false) as TextView
hourLabelView.text = "$i:00"
hourLabelViews.add(hourLabelView)
}
b.day.setHourLabelViews(hourLabelViews)
// observe lesson database
app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer<List<LessonFull>> { lessons ->
buildLessonViews(lessons)
processLessonList(lessons)
})
}
private fun buildLessonViews(lessons: List<LessonFull>) {
private fun processLessonList(lessons: List<LessonFull>) {
// no lessons - timetable not downloaded yet
if (lessons.isEmpty()) {
b.dayScroll.visibility = View.GONE
b.noTimetableLayout.visibility = View.VISIBLE
b.noLessonsLayout.visibility = View.GONE
val weekStart = date.clone().stepForward(0, 0, -date.weekDay).stringY_m_d
b.noTimetableSync.onClick {
it.isEnabled = false
EdziennikTask.syncProfile(
profileId = App.profileId,
viewIds = listOf(
DRAWER_ITEM_TIMETABLE to 0
),
arguments = JsonObject(
"weekStart" to weekStart
)
).enqueue(activity)
inflater.inflate(R.layout.timetable_no_timetable, view as FrameLayout) { view, _, parent ->
parent?.removeAllViews()
parent?.addView(view)
val b = TimetableNoTimetableBinding.bind(view)
val weekStart = date.clone().stepForward(0, 0, -date.weekDay).stringY_m_d
b.noTimetableSync.onClick {
it.isEnabled = false
EdziennikTask.syncProfile(
profileId = App.profileId,
viewIds = listOf(
DRAWER_ITEM_TIMETABLE to 0
),
arguments = JsonObject(
"weekStart" to weekStart
)
).enqueue(activity)
}
b.noTimetableWeek.setText(R.string.timetable_no_timetable_week, weekStart)
}
b.noTimetableWeek.setText(R.string.timetable_no_timetable_week, weekStart)
return
}
// one lesson indicating a day without lessons
if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) {
b.dayScroll.visibility = View.GONE
b.noTimetableLayout.visibility = View.GONE
b.noLessonsLayout.visibility = View.VISIBLE
inflater.inflate(R.layout.timetable_no_lessons, view as FrameLayout) { view, _, parent ->
parent?.removeAllViews()
parent?.addView(view)
}
return
}
@ -101,23 +153,37 @@ class TimetableDayFragment() : Fragment() {
return
}
b.dayScroll.visibility = View.VISIBLE
b.noTimetableLayout.visibility = View.GONE
b.noLessonsLayout.visibility = View.GONE
// clear the root view and add the ScrollView
(view as FrameLayout).removeAllViews()
(view as FrameLayout).addView(dayScroll)
var firstEventMinute = 24*60
// Inflate a label view for each hour the day view will display
val hourLabelViews = ArrayList<View>()
for (i in dayView.startHour..dayView.endHour) {
val hourLabelView = layoutInflater.inflate(R.layout.timetable_hour_label, dayView, false) as TextView
hourLabelView.text = "$i:00"
hourLabelViews.add(hourLabelView)
}
dayView.setHourLabelViews(hourLabelViews)
buildLessonViews(lessons)
}
private fun buildLessonViews(lessons: List<LessonFull>) {
if (!isAdded)
return
val eventViews = mutableListOf<View>()
val eventTimeRanges = mutableListOf<DayView.EventTimeRange>()
// Reclaim all of the existing event views so we can reuse them if needed, this process
// can be useful if your day view is hosted in a recycler view for example
val recycled = b.day.removeEventViews()
val recycled = dayView.removeEventViews()
var remaining = recycled?.size ?: 0
val arrowRight = ""
val bullet = ""
val colorSecondary = getColorFromAttr(activity, android.R.attr.textColorSecondary)
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
for (lesson in lessons) {
val startTime = lesson.displayStartTime ?: continue
@ -127,7 +193,7 @@ class TimetableDayFragment() : Fragment() {
// Try to recycle an existing event view if there are enough left, otherwise inflate
// a new one
val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, b.day, false))
val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, dayView, false))
?: continue
val lb = TimetableLessonBinding.bind(eventView)
eventViews += eventView
@ -274,9 +340,16 @@ class TimetableDayFragment() : Fragment() {
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
}
val minuteHeight = (b.day.getHourTop(1) - b.day.getHourTop(0)).toFloat() / 60f
val firstEventTop = (firstEventMinute - b.day.startHour * 60) * minuteHeight
b.day.setEventViews(eventViews, eventTimeRanges)
b.dayScroll.scrollTo(0, firstEventTop.toInt())
dayView.setEventViews(eventViews, eventTimeRanges)
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
dayScroll.scrollTo(0, firstEventTop.toInt())
}
override fun onResume() {
super.onResume()
if (dayScrollDelegate.isInitialized()) {
val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight
dayScroll.scrollTo(0, firstEventTop.toInt())
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-16.
*/
package pl.szczodrzynski.edziennik.utils
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ScrollView
class ListenerScrollView(
context: Context,
attrs: AttributeSet? = null
) : ScrollView(context, attrs) {
private var onScrollChangedListener: ((v: ListenerScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) -> Unit)? = null
private var onRefreshLayoutEnabledListener: ((enabled: Boolean) -> Unit)? = null
private var refreshLayoutEnabled = true
init {
setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
refreshLayoutEnabled = scrollY < 10
onRefreshLayoutEnabledListener?.invoke(refreshLayoutEnabled)
}
false
}
}
override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
onScrollChangedListener?.invoke(this, l, t, oldl, oldt)
if (t > 10 && refreshLayoutEnabled) {
refreshLayoutEnabled = false
onRefreshLayoutEnabledListener?.invoke(refreshLayoutEnabled)
}
}
fun setOnScrollChangedListener(l: ((v: ListenerScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) -> Unit)?) {
onScrollChangedListener = l
}
fun setOnRefreshLayoutEnabledListener(l: ((enabled: Boolean) -> Unit)?) {
onRefreshLayoutEnabledListener = l
}
}

View File

@ -75,21 +75,22 @@ class TextInputDropDown : TextInputEditText {
}
}
fun select(item: Item) {
fun select(item: Item): Item? {
selected = item
updateText()
return item
}
fun select(id: Long?) {
items.singleOrNull { it.id == id }?.let { select(it) }
fun select(id: Long?): Item? {
return items.singleOrNull { it.id == id }?.let { select(it) }
}
fun select(tag: Any?) {
items.singleOrNull { it.tag == tag }?.let { select(it) }
fun select(tag: Any?): Item? {
return items.singleOrNull { it.tag == tag }?.let { select(it) }
}
fun select(index: Int) {
items.getOrNull(index)?.let { select(it) }
fun select(index: Int): Item? {
return items.getOrNull(index)?.let { select(it) }
}
fun deselect(): TextInputDropDown {

View File

@ -95,6 +95,7 @@ public class AppConfig {
public String updateUrl = "";
public String updateFilename = "";
public boolean updateMandatory = false;
public boolean updateDirect = false;
public boolean webPushEnabled = false;

View File

@ -173,7 +173,8 @@ public class WidgetTimetableListProvider implements RemoteViewsService.RemoteVie
intent.putExtra("endTime", lesson.endTime.getStringValue());
views.setOnClickFillInIntent(R.id.widgetTimetableRoot, intent);
views.setTextViewText(R.id.widgetTimetableTime, lesson.startTime.getStringHM() + " - " + lesson.endTime.getStringHM());
if (lesson.startTime != null && lesson.endTime != null)
views.setTextViewText(R.id.widgetTimetableTime, lesson.startTime.getStringHM() + " - " + lesson.endTime.getStringHM());
views.setViewVisibility(R.id.widgetTimetableEvent1, View.GONE);
views.setViewVisibility(R.id.widgetTimetableEvent2, View.GONE);

View File

@ -4,11 +4,17 @@
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:padding="24dp">
@ -46,41 +52,63 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/dialog_event_manual_team">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/teamDropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
tools:text="2b3T"/>
tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/shareSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_event_manual_share_enabled" />
<TextView
android:id="@+id/shareDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dialog_event_manual_share_first_notice"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/dialog_event_manual_subject">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/subjectDropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
tools:text="2b3T"/>
</com.google.android.material.textfield.TextInputLayout>
android:baselineAligned="false"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/dialog_event_manual_teacher">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/teacherDropdown"
android:layout_width="match_parent"
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:enabled="false"
tools:text="2b3T"/>
</com.google.android.material.textfield.TextInputLayout>
android:layout_weight="1"
android:hint="@string/dialog_event_manual_type">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/typeDropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:id="@+id/typeColor"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="2dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="@drawable/bg_circle"
android:foreground="?selectableItemBackgroundBorderless" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
@ -98,5 +126,64 @@
tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout>
<com.mikepenz.iconics.view.IconicsCheckableTextView
android:id="@+id/showMore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:text="@string/dialog_event_manual_more_options"
android:background="?selectableItemBackground"
app:iiv_end_icon="cmd-chevron-down"
app:iiv_end_color="?android:textColorSecondary"
app:iiv_end_size="16dp"
app:iiv_end_checked_icon="cmd-chevron-up"
app:iiv_end_checked_color="?android:textColorSecondary"
app:iiv_end_checked_size="16dp"/>
<LinearLayout
android:id="@+id/moreLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/dialog_event_manual_subject">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/subjectDropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/dialog_event_manual_teacher">
<pl.szczodrzynski.edziennik.utils.TextInputDropDown
android:id="@+id/teacherDropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
tools:text="2b3T" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</layout>

View File

@ -21,217 +21,207 @@
<variable name="oldTeamName" type="String" />
<variable name="teamName" type="String" />
</data>
<LinearLayout
android:orientation="vertical"
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
android:padding="24dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{oldSubjectName}"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Medium"
android:textColor="?android:textColorTertiary"
android:visibility="@{oldSubjectName == null ? View.GONE : View.VISIBLE}"
app:strikeThrough="@{true}"
tools:text="pracownia urządzeń techniki komputerowej" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{subjectName}"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Title"
android:visibility="@{subjectName == null ? View.GONE : View.VISIBLE}"
tools:text="pracownia urządzeń techniki komputerowej" />
<TextView
android:id="@+id/lessonDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Subtitle"
tools:text="czwartek, 14 listopada 2019"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_number"
android:visibility="@{lesson.displayLessonNumber == null ? View.GONE : View.VISIBLE}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:text="@{lesson.displayLessonNumber.toString()}"
android:textSize="36sp"
android:visibility="@{lesson.displayLessonNumber == null ? View.GONE : View.VISIBLE}"
tools:text="4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{lesson.displayStartTime.stringHM + ` - ` + lesson.displayEndTime.stringHM}"
tools:text="14:55 - 15:40" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/shiftedLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/shiftedText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textStyle="italic"
tools:text="Lekcja przeniesiona na czwartek, 17 października" />
<Button
android:id="@+id/shiftedGoTo"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Przejdź" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_teacher"
android:visibility="@{teacherName != null || oldTeacherName != null ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@{oldTeacherName}"
android:textIsSelectable="true"
android:visibility="@{oldTeacherName != null ? View.VISIBLE : View.GONE}"
app:strikeThrough="@{true}"
tools:text="Janósz Kowalski" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{teacherName}"
android:textIsSelectable="true"
android:visibility="@{teacherName != null ? View.VISIBLE : View.GONE}"
tools:text="Janósz Kowalski" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_classroom"
android:visibility="@{classroom != null || oldClassroom != null ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@{oldClassroom}"
android:textIsSelectable="true"
android:visibility="@{oldClassroom != null ? View.VISIBLE : View.GONE}"
app:strikeThrough="@{true}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{classroom}"
android:textIsSelectable="true"
android:visibility="@{classroom != null ? View.VISIBLE : View.GONE}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_team"
android:visibility="@{teamName != null || oldTeamName != null ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@{oldTeamName}"
android:textIsSelectable="true"
android:visibility="@{oldTeamName != null ? View.VISIBLE : View.GONE}"
app:strikeThrough="@{true}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{teamName}"
android:textIsSelectable="true"
android:visibility="@{teamName != null ? View.VISIBLE : View.GONE}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:visibility="@{App.devMode ? View.VISIBLE : View.GONE}"
android:text="@string/dialog_lesson_details_id" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:text="@{Long.toString(lesson.id)}"
android:textIsSelectable="true"
android:visibility="@{App.devMode ? View.VISIBLE : View.GONE}"
tools:text="12345" />
<!--<androidx.core.widget.NestedScrollView
android:id="@+id/gradeHistoryNest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{historyVisible ? View.VISIBLE : View.GONE}">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gradeHistoryList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/row_grades_list_item" />
android:orientation="horizontal"
android:baselineAligned="false">
</androidx.core.widget.NestedScrollView>-->
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1">
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{oldSubjectName}"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Medium"
android:textColor="?android:textColorTertiary"
android:visibility="@{oldSubjectName == null ? View.GONE : View.VISIBLE}"
app:strikeThrough="@{true}"
tools:text="pracownia urządzeń techniki komputerowej" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{subjectName}"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Title"
android:visibility="@{subjectName == null ? View.GONE : View.VISIBLE}"
tools:text="pracownia urządzeń techniki komputerowej" />
<TextView
android:id="@+id/lessonDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textAppearance="@style/NavView.TextView.Subtitle"
tools:text="czwartek, 14 listopada 2019"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_number"
android:visibility="@{lesson.displayLessonNumber == null ? View.GONE : View.VISIBLE}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:text="@{lesson.displayLessonNumber.toString()}"
android:textSize="36sp"
android:visibility="@{lesson.displayLessonNumber == null ? View.GONE : View.VISIBLE}"
tools:text="4" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{lesson.displayStartTime.stringHM + ` - ` + lesson.displayEndTime.stringHM}"
tools:text="14:55 - 15:40" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/shiftedLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/shiftedText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textStyle="italic"
tools:text="Lekcja przeniesiona na czwartek, 17 października" />
<Button
android:id="@+id/shiftedGoTo"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Przejdź" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_teacher"
android:visibility="@{teacherName != null || oldTeacherName != null ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@{oldTeacherName}"
android:textIsSelectable="true"
android:visibility="@{oldTeacherName != null ? View.VISIBLE : View.GONE}"
app:strikeThrough="@{true}"
tools:text="Janósz Kowalski" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{teacherName}"
android:textIsSelectable="true"
android:visibility="@{teacherName != null ? View.VISIBLE : View.GONE}"
tools:text="Janósz Kowalski" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_classroom"
android:visibility="@{classroom != null || oldClassroom != null ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@{oldClassroom}"
android:textIsSelectable="true"
android:visibility="@{oldClassroom != null ? View.VISIBLE : View.GONE}"
app:strikeThrough="@{true}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{classroom}"
android:textIsSelectable="true"
android:visibility="@{classroom != null ? View.VISIBLE : View.GONE}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_lesson_details_team"
android:visibility="@{teamName != null || oldTeamName != null ? View.VISIBLE : View.GONE}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@{oldTeamName}"
android:textIsSelectable="true"
android:visibility="@{oldTeamName != null ? View.VISIBLE : View.GONE}"
app:strikeThrough="@{true}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{teamName}"
android:textIsSelectable="true"
android:visibility="@{teamName != null ? View.VISIBLE : View.GONE}"
tools:text="013 informatyczna" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:visibility="@{App.devMode ? View.VISIBLE : View.GONE}"
android:text="@string/dialog_lesson_details_id" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:text="@{Long.toString(lesson.id)}"
android:textIsSelectable="true"
android:visibility="@{App.devMode ? View.VISIBLE : View.GONE}"
tools:text="12345" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -2,89 +2,96 @@
<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">
<FrameLayout
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/timetableLayout"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="gone">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface"
style="@style/Widget.MaterialComponents.AppBarLayout.Surface">
<com.nshmura.recyclertablayout.RecyclerTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorSurface_6dp"
app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab"
app:rtl_tabIndicatorColor="?colorPrimary"
app:rtl_tabMinWidth="90dp"
app:rtl_tabMaxWidth="300dp"
app:rtl_tabSelectedTextColor="?colorPrimary"
app:rtl_tabPaddingStart="16dp"
app:rtl_tabPaddingEnd="16dp"
app:rtl_tabPaddingTop="12dp"
app:rtl_tabPaddingBottom="12dp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/timetableLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
tools:visibility="gone">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.appbar.AppBarLayout
style="@style/Widget.MaterialComponents.AppBarLayout.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface">
<LinearLayout
android:id="@+id/timetableNotPublicLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="visible">
<com.nshmura.recyclertablayout.RecyclerTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorSurface_6dp"
app:rtl_tabIndicatorColor="?colorPrimary"
app:rtl_tabMaxWidth="300dp"
app:rtl_tabMinWidth="90dp"
app:rtl_tabPaddingBottom="12dp"
app:rtl_tabPaddingEnd="16dp"
app:rtl_tabPaddingStart="16dp"
app:rtl_tabPaddingTop="12dp"
app:rtl_tabSelectedTextColor="?colorPrimary"
app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab" />
</com.google.android.material.appbar.AppBarLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_no_timetable" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_not_public_title"
android:textSize="24sp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
<LinearLayout
android:id="@+id/timetableNotPublicLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/timetable_not_public_text"
android:textSize="16sp" />
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_not_public_hint"
android:textSize="14sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_no_timetable" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_not_public_title"
android:textSize="24sp" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_not_public_text"
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_not_public_hint"
android:textSize="14sp" />
</LinearLayout>
</FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -28,126 +28,5 @@
tools:visibility="gone"/>
</ScrollView>
<LinearLayout
android:id="@+id/noLessonsLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_timetable" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_no_lessons_title"
android:textSize="24sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/freeDayLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_sunbed" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_free_day_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_free_day_text"
android:textSize="14sp" />
<TextView
android:id="@+id/freeDayText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
tools:text="Dzień wolny dla szkoły z puli dyrektorskiej z okazji obchodów Światowego Dnia Wtorku w mieście Poznań i na przedmieśiach"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/freeDayShowTimetable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_free_day_show" />
</LinearLayout>
<LinearLayout
android:id="@+id/noTimetableLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
android:gravity="center"
tools:visibility="visible">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_sync" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_no_timetable_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_no_timetable_text"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/noTimetableSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_no_timetable_sync" />
<TextView
android:id="@+id/noTimetableWeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/timetable_no_timetable_week"
android:textSize="12sp"
android:textStyle="italic"/>
</LinearLayout>
</FrameLayout>
</layout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-15.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/freeDayLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_sunbed"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_free_day_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_free_day_text"
android:textSize="14sp" />
<TextView
android:id="@+id/freeDayText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:textSize="16sp"
tools:text="Dzień wolny dla szkoły z puli dyrektorskiej z okazji obchodów Światowego Dnia Wtorku w mieście Poznań i na przedmieśiach" />
<com.google.android.material.button.MaterialButton
android:id="@+id/freeDayShowTimetable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_free_day_show" />
</LinearLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-15.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTop="@drawable/ic_timetable"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_no_lessons_title"
android:textSize="24sp" />

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-15.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/noTimetableLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableTop="@drawable/ic_sync"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/timetable_no_timetable_title"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/timetable_no_timetable_text"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/noTimetableSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/timetable_no_timetable_sync" />
<TextView
android:id="@+id/noTimetableWeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textStyle="italic"
tools:text="@string/timetable_no_timetable_week" />
</LinearLayout>
</layout>

View File

@ -72,6 +72,12 @@
<string name="error_173" translatable="false">ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED</string>
<string name="error_174" translatable="false">ERROR_LIBRUS_SYNERGIA_OTHER</string>
<string name="error_175" translatable="false">ERROR_LIBRUS_SYNERGIA_MAINTENANCE</string>
<string name="error_176" translatable="false">ERROR_LIBRUS_MESSAGES_MAINTENANCE</string>
<string name="error_177" translatable="false">ERROR_LIBRUS_MESSAGES_ERROR</string>
<string name="error_178" translatable="false">ERROR_LIBRUS_MESSAGES_OTHER</string>
<string name="error_179" translatable="false">ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN</string>
<string name="error_180" translatable="false">ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN</string>
<string name="error_181" translatable="false">ERROR_LIBRUS_API_MAINTENANCE</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>
@ -165,7 +171,7 @@
<string name="error_127_reason">Wymagana akceptacja regulaminu</string>
<string name="error_128_reason">Błąd zmiany hasła</string>
<string name="error_129_reason">Wymagana zmiana hasła</string>
<string name="error_130_reason">Nieprawidłowe dane logowania</string>
<string name="error_130_reason">Librus API: nieprawidłowe dane logowania</string>
<string name="error_131_reason">Inny błąd logowania do API</string>
<string name="error_132_reason">Brak tokenu CSRF</string>
<string name="error_133_reason">Konto LIBRUS nie zostało aktywowane</string>
@ -206,7 +212,13 @@
<string name="error_172_reason">ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_INVALID</string>
<string name="error_173_reason">ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED</string>
<string name="error_174_reason">ERROR_LIBRUS_SYNERGIA_OTHER</string>
<string name="error_175_reason">ERROR_LIBRUS_SYNERGIA_MAINTENANCE</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_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>
<string name="error_201_reason">ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN</string>
<string name="error_202_reason">ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD</string>

View File

@ -1026,4 +1026,6 @@
<string name="snackbar_error_text">Wystąpił błąd</string>
<string name="dialog_sync_view_list_title">Synchronizacja ręczna</string>
<string name="timetable_no_subject_name">(brak nazwy)</string>
<string name="dialog_event_manual_more_options">Więcej opcji</string>
<string name="edziennik_progress_endpoint_grade_comments">Pobieranie komentarzy ocen...</string>
</resources>

View File

@ -101,6 +101,8 @@
<item name="timetable_lesson_change_color">#ffb300</item>
<item name="timetable_lesson_shifted_source_color">#A1887F</item>
<item name="timetable_lesson_shifted_target_color">#4caf50</item>
<item name="hourDividerColor">#b0b0b0</item>
<item name="halfHourDividerColor">#e0e0e0</item>
</style>
<style name="AppTheme.Dark" parent="NavView.Dark">
<item name="colorPrimary">#64b5f6</item>
@ -131,6 +133,8 @@
<item name="timetable_lesson_change_color">#ffb300</item>
<item name="timetable_lesson_shifted_source_color">#A1887F</item>
<item name="timetable_lesson_shifted_target_color">#4caf50</item>
<item name="hourDividerColor">#7fffffff</item>
<item name="halfHourDividerColor">#40ffffff</item>
</style>

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.3.50'
release = [
versionName: "3.9.7-dev",
versionCode: 3090700
versionName: "3.9.9-dev",
versionCode: 3090900
]
setup = [