Compare commits

...

13 Commits

53 changed files with 1428 additions and 125 deletions

View File

@ -29,9 +29,13 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.navlib.R
import pl.szczodrzynski.navlib.getColorFromRes
import java.text.SimpleDateFormat
@ -418,15 +422,15 @@ fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) {
text = context.getString(resid, *formatArgs)
}
fun JsonObject(vararg properties: Pair<String, Any>): JsonObject {
fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
return JsonObject().apply {
for (property in properties) {
when (property.second) {
is JsonElement -> add(property.first, property.second as JsonElement)
is String -> addProperty(property.first, property.second as String)
is Char -> addProperty(property.first, property.second as Char)
is Number -> addProperty(property.first, property.second as Number)
is Boolean -> addProperty(property.first, property.second as Boolean)
is JsonElement -> add(property.first, property.second as JsonElement?)
is String -> addProperty(property.first, property.second as String?)
is Char -> addProperty(property.first, property.second as Char?)
is Number -> addProperty(property.first, property.second as Number?)
is Boolean -> addProperty(property.first, property.second as Boolean?)
}
}
}
@ -502,4 +506,30 @@ fun View.findParentById(targetId: Int): View? {
return viewParent.findParentById(targetId)
}
return null
}
}
fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: () -> Unit) = launch {
delay(delayMillis)
if (repeatMillis > 0) {
while (true) {
action()
delay(repeatMillis)
}
} else {
action()
}
}
operator fun Time?.compareTo(other: Time?): Int {
if (this == null && other == null)
return 0
if (this == null)
return -1
if (other == null)
return 1
return this.compareTo(other)
}
operator fun StringBuilder.plusAssign(str: String?) {
this.append(str)
}

View File

@ -56,7 +56,7 @@ import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2
import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment
@ -122,7 +122,7 @@ class MainActivity : AppCompatActivity() {
val list: MutableList<NavTarget> = mutableListOf()
// home item
list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragment::class)
list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragmentV2::class)
.withTitle(R.string.app_name)
.withIcon(CommunityMaterial.Icon2.cmd_home_outline)
.isInDrawer(true)
@ -538,9 +538,7 @@ class MainActivity : AppCompatActivity() {
else -> 0
}
val arguments = when (navTargetId) {
DRAWER_ITEM_TIMETABLE -> JsonObject().apply {
addProperty("weekStart", TimetableFragment.pageSelection?.weekStart?.stringY_m_d)
}
DRAWER_ITEM_TIMETABLE -> JsonObject("weekStart" to TimetableFragment.pageSelection?.weekStart?.stringY_m_d)
else -> null
}
EdziennikTask.syncProfile(
@ -704,7 +702,10 @@ class MainActivity : AppCompatActivity() {
loadProfile(intentProfileId, intentTargetId, extras)
}
intentProfileId != -1 -> {
loadProfile(intentProfileId, intentTargetId, extras)
if (app.profile.id != intentProfileId)
loadProfile(intentProfileId, intentTargetId, extras)
else
loadTarget(intentTargetId, extras)
}
intentTargetId != -1 -> {
drawer.currentProfile = app.profile.id

View File

@ -45,8 +45,8 @@ class DataNotifications(val data: Data) {
return@run
}
for (change in app.db.lessonChangeDao().getNotNotifiedNow(profileId)) {
val text = app.getString(R.string.notification_lesson_change_format, change.changeTypeStr(app), if (change.lessonDate == null) "" else change.lessonDate!!.formattedString, change.subjectLongName)
for (lesson in app.db.timetableDao().getNotNotifiedNow(profileId)) {
val text = app.getString(R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, lesson.changeSubjectName)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_TIMETABLE_LESSON_CHANGE),
text = text,
@ -54,8 +54,8 @@ class DataNotifications(val data: Data) {
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_TIMETABLE,
addedDate = change.addedDate
).addExtra("timetableDate", change.lessonDate?.value?.toLong())
addedDate = lesson.addedDate
).addExtra("timetableDate", lesson.displayDate?.stringY_m_d ?: "")
}
for (event in app.db.eventDao().getNotNotifiedNow(profileId)) {
@ -186,10 +186,10 @@ class DataNotifications(val data: Data) {
val luckyNumbers = app.db.luckyNumberDao().getNotNotifiedNow(profileId)
luckyNumbers?.removeAll { it.date < today }
luckyNumbers?.forEach { luckyNumber ->
val text = when {
luckyNumber.date.value == todayValue -> // LN for today
val text = when (luckyNumber.date.value) {
todayValue -> // LN for today
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_format else R.string.notification_lucky_number_format, luckyNumber.number)
luckyNumber.date.value == todayValue + 1 -> // LN for tomorrow
todayValue + 1 -> // LN for tomorrow
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_tomorrow_format else R.string.notification_lucky_number_tomorrow_format, luckyNumber.number)
else -> // LN for later
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_later_format else R.string.notification_lucky_number_later_format, luckyNumber.date.formattedString, luckyNumber.number)
@ -207,4 +207,4 @@ class DataNotifications(val data: Data) {
data.db.metadataDao().setAllNotified(profileId, true)
}}
}
}

View File

@ -48,6 +48,7 @@ const val ERROR_PROFILE_MISSING = 105
const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val ERROR_NOT_IMPLEMENTED = 112
const val ERROR_FILE_DOWNLOAD = 113
const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115

View File

@ -76,4 +76,10 @@ object Regexes {
val VULCAN_SHITFT_ANNOTATION by lazy {
"""\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex()
}
val LIBRUS_ATTACHMENT_KEY by lazy {
"""singleUseKey=([0-9A-f_]+)""".toRegex()
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-24
*/
package pl.szczodrzynski.edziennik.api.v2.events
data class AttachmentGetEvent(val profileId: Int, val messageId: Long, val attachmentId: Long,
var eventType: Int = TYPE_PROGRESS, val fileName: String? = null,
val bytesWritten: Long = 0) {
companion object {
const val TYPE_PROGRESS = 0
const val TYPE_FINISHED = 1
}
}

View File

@ -24,6 +24,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
fun syncProfileList(profileList: List<Int>) = EdziennikTask(-1, SyncProfileListRequest(profileList))
fun messageGet(profileId: Int, message: MessageFull) = EdziennikTask(profileId, MessageGetRequest(message))
fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest())
fun attachmentGet(profileId: Int, messageId: Long, attachmentId: Long, attachmentName: String) = EdziennikTask(profileId, AttachmentGetRequest(messageId, attachmentId, attachmentName))
}
private lateinit var loginStore: LoginStore
@ -35,8 +36,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
loginStore = request.loginStore
// save the profile ID and name as the current task's
taskName = app.getString(R.string.edziennik_notification_api_first_login_title)
}
else {
} else {
// get the requested profile and login store
val profile = app.db.profileDao().getByIdNow(profileId)
this.profile = profile
@ -67,12 +67,14 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
when (request) {
is SyncProfileRequest -> edziennikInterface?.sync(
featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) } ?: Features.getAllIds(),
featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) }
?: Features.getAllIds(),
viewId = request.viewIds?.get(0)?.first,
arguments = request.arguments)
is MessageGetRequest -> edziennikInterface?.getMessage(request.message)
is FirstLoginRequest -> edziennikInterface?.firstLogin()
is AnnouncementsReadRequest -> edziennikInterface?.markAllAnnouncementsAsRead()
is AttachmentGetRequest -> edziennikInterface?.getAttachment(request.messageId, request.attachmentId, request.attachmentName)
}
}
@ -90,4 +92,5 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
data class SyncProfileListRequest(val profileList: List<Int>)
data class MessageGetRequest(val message: MessageFull)
class AnnouncementsReadRequest
data class AttachmentGetRequest(val messageId: Long, val attachmentId: Long, val attachmentName: String)
}

View File

@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.Notifier.ID_NOTIFICATIONS
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.db.modules.notification.getNotificationTitle
import pl.szczodrzynski.edziennik.utils.models.Notification
import kotlin.math.min
@ -33,9 +34,9 @@ class NotifyTask : IApiTask(-1) {
val pendingIntent = PendingIntent.getActivity(app, notification.id, intent, 0)
val notificationBuilder = NotificationCompat.Builder(app, app.notifier.notificationGroup)
// title, text, type, date
.setContentTitle(notification.title)
.setContentTitle(notification.profileName)
.setContentText(notification.text)
.setSubText(Notification.stringType(app, notification.type))
.setSubText(app.getNotificationTitle(notification.type))
.setWhen(notification.addedDate)
.setTicker(app.getString(R.string.notification_ticker_format, Notification.stringType(app, notification.type)))
// icon, color, lights, priority

View File

@ -70,6 +70,10 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
IdziennikFirstLogin(data) {
completed()

View File

@ -28,7 +28,7 @@ class IdziennikWebTimetable(override val data: DataIdziennik,
private const val TAG = "IdziennikWebTimetable"
}
init {
init { data.profile?.also { profile ->
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4) {
@ -91,10 +91,9 @@ class IdziennikWebTimetable(override val data: DataIdziennik,
val lessonDate = weekStart.clone().stepForward(0, 0, weekDay)
val classroom = lesson.getString("NazwaSali")
val id = lessonDate.combineWith(lessonRange.startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
val type = lesson.getInt("TypZastepstwa") ?: -1
val lessonObject = Lesson(profileId, id)
val lessonObject = Lesson(profileId, -1)
when (type) {
1, 2, 3, 4, 5 -> {
@ -150,16 +149,20 @@ class IdziennikWebTimetable(override val data: DataIdziennik,
}
}
lessonObject.id = lessonObject.buildId()
dates.add(lessonDate.value)
lessons.add(lessonObject)
val seen = profile.empty || lessonDate < Date.getToday()
if (lessonObject.type != Lesson.TYPE_NORMAL && lessonDate >= Date.getToday()) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonObject.id,
profile?.empty ?: false,
profile?.empty ?: false,
seen,
seen,
System.currentTimeMillis()
))
}
@ -185,5 +188,5 @@ class IdziennikWebTimetable(override val data: DataIdziennik,
data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS)
onSuccess()
}
}
}}
}

View File

@ -11,6 +11,7 @@ interface EdziennikInterface {
fun sync(featureIds: List<Int>, viewId: Int? = null, arguments: JsonObject? = null)
fun getMessage(message: MessageFull)
fun markAllAnnouncementsAsRead()
fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String)
fun firstLogin()
fun cancel()
}

View File

@ -10,13 +10,11 @@ import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusData
import pl.szczodrzynski.edziennik.api.v2.librus.data.messages.LibrusMessagesGetAttachment
import pl.szczodrzynski.edziennik.api.v2.librus.data.messages.LibrusMessagesGetMessage
import pl.szczodrzynski.edziennik.api.v2.librus.data.synergia.LibrusSynergiaMarkAllAnnouncementsAsRead
import pl.szczodrzynski.edziennik.api.v2.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginApi
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginMessages
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginSynergia
import pl.szczodrzynski.edziennik.api.v2.librus.login.*
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
@ -82,11 +80,13 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getMessage(message: MessageFull) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, message) {
completed()
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, message) {
completed()
}
}
}
}
@ -94,10 +94,26 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun markAllAnnouncementsAsRead() {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusSynergiaMarkAllAnnouncementsAsRead(data) {
completed()
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusSynergiaMarkAllAnnouncementsAsRead(data) {
completed()
}
}
}
}
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetAttachment(data, messageId, attachmentId, attachmentName) {
completed()
}
}
}
}
}

View File

@ -4,9 +4,12 @@
package pl.szczodrzynski.edziennik.api.v2.librus.data
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.FileCallbackHandler
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import org.jsoup.Jsoup
@ -16,6 +19,7 @@ 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.utils.Utils.d
import java.io.File
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
@ -131,4 +135,95 @@ open class LibrusMessages(open val data: DataLibrus) {
.build()
.enqueue()
}
fun sandboxGet(tag: String, action: String, parameters: Map<String, Any>? = null,
onSuccess: (json: JsonObject) -> Unit) {
d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action")
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
try {
onSuccess(json)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(json))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("$LIBRUS_SANDBOX_URL$action")
.userAgent(SYNERGIA_USER_AGENT)
.apply {
parameters?.forEach { (k, v) ->
addParameter(k, v)
}
}
.post()
.callback(callback)
.build()
.enqueue()
}
fun sandboxGetFile(tag: String, action: String, targetFile: File, onSuccess: (file: File) -> Unit,
onProgress: (written: Long, total: Long) -> Unit) {
d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action")
val callback = object : FileCallbackHandler(targetFile) {
override fun onSuccess(file: File?, response: Response?) {
if (file == null) {
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withResponse(response))
return
}
try {
onSuccess(file)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withResponse(response)
.withThrowable(e))
}
}
override fun onProgress(bytesWritten: Long, bytesTotal: Long) {
try {
onProgress(bytesWritten, bytesTotal)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withThrowable(e))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("$LIBRUS_SANDBOX_URL$action")
.userAgent(SYNERGIA_USER_AGENT)
.post()
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -75,7 +75,7 @@ class LibrusApiTimetables(override val data: DataLibrus,
}
}
private fun parseLesson(lessonDate: Date, lesson: JsonObject) {
private fun parseLesson(lessonDate: Date, lesson: JsonObject) { data.profile?.also { profile ->
val isSubstitution = lesson.getBoolean("IsSubstitutionClass") ?: false
val isCancelled = lesson.getBoolean("IsCanceled") ?: false
@ -88,8 +88,7 @@ class LibrusApiTimetables(override val data: DataLibrus,
val virtualClassId = lesson.getJsonObject("VirtualClass")?.getLong("Id")
val teamId = lesson.getJsonObject("Class")?.getLong("Id") ?: virtualClassId
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
val lessonObject = Lesson(profileId, id)
val lessonObject = Lesson(profileId, -1)
if (isSubstitution && isCancelled) {
// shifted lesson - source
@ -184,17 +183,21 @@ class LibrusApiTimetables(override val data: DataLibrus,
}
}
if (lessonObject.type != Lesson.TYPE_NORMAL && lessonDate >= Date.getToday()) {
lessonObject.id = lessonObject.buildId()
val seen = profile.empty || lessonDate < Date.getToday()
if (lessonObject.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonObject.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
seen,
seen,
System.currentTimeMillis()
))
}
data.lessonNewList.add(lessonObject)
}
}}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-24
*/
package pl.szczodrzynski.edziennik.api.v2.librus.data.messages
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.api.v2.ERROR_FILE_DOWNLOAD
import pl.szczodrzynski.edziennik.api.v2.EXCEPTION_LIBRUS_MESSAGES_REQUEST
import pl.szczodrzynski.edziennik.api.v2.Regexes
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent.Companion.TYPE_FINISHED
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent.Companion.TYPE_PROGRESS
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusMessages
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
import java.io.File
import kotlin.coroutines.CoroutineContext
class LibrusMessagesGetAttachment(override val data: DataLibrus, val messageId: Long, val attachmentId: Long,
val attachmentName: String, val onSuccess: () -> Unit) : LibrusMessages(data), CoroutineScope {
companion object {
const val TAG = "LibrusMessagesGetAttachment"
}
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
private var getAttachmentCheckKeyTries = 0
init {
messagesGet(TAG, "GetFileDownloadLink", parameters = mapOf(
"fileId" to attachmentId,
"msgId" to messageId,
"archive" to 0
)) { doc ->
val downloadLink = doc.select("response GetFileDownloadLink downloadLink").text()
val keyMatcher = Regexes.LIBRUS_ATTACHMENT_KEY.find(downloadLink)
if (keyMatcher != null) {
getAttachmentCheckKeyTries = 0
val attachmentKey = keyMatcher[1]
getAttachmentCheckKey(attachmentKey) {
downloadAttachment(attachmentKey)
}
} else {
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withApiResponse(doc.toString()))
}
onSuccess()
}
}
private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) {
sandboxGet(TAG, "CSCheckKey",
parameters = mapOf("singleUseKey" to attachmentKey)) { json ->
when (json.getString("status")) {
"not_downloaded_yet" -> {
if (getAttachmentCheckKeyTries++ > 5) {
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withApiResponse(json))
return@sandboxGet
}
launch {
delay(2000)
getAttachmentCheckKey(attachmentKey, callback)
}
}
"ready" -> {
launch { callback() }
}
else -> {
data.error(ApiError(TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withApiResponse(json))
}
}
}
}
private fun downloadAttachment(attachmentKey: String) {
val targetFile = File(Utils.getStorageDir(), attachmentName)
sandboxGetFile(TAG, "CSDownload&singleUseKey=$attachmentKey",
targetFile, { file ->
val event = AttachmentGetEvent(
profileId,
messageId,
attachmentId,
TYPE_FINISHED,
file.absolutePath
)
val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}")
Utils.writeStringToFile(attachmentDataFile, event.fileName)
EventBus.getDefault().post(event)
}) { written, _ ->
val event = AttachmentGetEvent(
profileId,
messageId,
attachmentId,
TYPE_PROGRESS,
bytesWritten = written
)
EventBus.getDefault().post(event)
}
}
}

View File

@ -76,6 +76,10 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
MobidziennikFirstLogin(data) {
completed()

View File

@ -14,7 +14,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
init {
init { data.profile?.also { profile ->
val lessons = rows.filterNot { it.isEmpty() }.map { it.split("|") }
val dataStart = Date.getToday()
@ -32,7 +32,6 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
val date = Date.fromYmd(lesson[2])
val startTime = Time.fromYmdHm(lesson[3])
val endTime = Time.fromYmdHm(lesson[4])
val id = date.combineWith(startTime) / 6L * 10L + (lesson.joinToString("|").hashCode() and 0xFFFF)
dataDays.remove(date.value)
@ -41,7 +40,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
val teamId = data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.id ?: -1
val classroom = lesson[11]
Lesson(data.profileId, id).also {
Lesson(data.profileId, -1).also {
when (lesson[1]) {
"plan_lekcji", "lekcja" -> {
it.type = Lesson.TYPE_NORMAL
@ -75,14 +74,18 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
}
}
if (it.type != Lesson.TYPE_NORMAL && date >= Date.getToday()) {
it.id = it.buildId()
val seen = profile.empty || date < Date.getToday()
if (it.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LESSON_CHANGE,
it.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
seen,
seen,
System.currentTimeMillis()
))
}
@ -194,5 +197,5 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
}
}
}*/
}
}}
}

View File

@ -375,7 +375,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
}
fun shouldSyncLuckyNumber(): Boolean {
return (db.luckyNumberDao().getNearestFutureNow(profileId, Date.getToday()) ?: -1) == -1
return (db.luckyNumberDao().getNearestFutureNow(profileId, Date.getToday().value) ?: -1) == -1
}
fun error(tag: String, errorCode: Int, response: Response? = null, throwable: Throwable? = null, apiResponse: JsonObject? = null) {

View File

@ -70,6 +70,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
TemplateFirstLogin(data) {
completed()

View File

@ -76,6 +76,10 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
VulcanFirstLogin(data) {
completed()

View File

@ -29,14 +29,14 @@ class VulcanApiTimetable(override val data: DataVulcan, val onSuccess: () -> Uni
init { data.profile?.also { profile ->
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4) {
currentWeekStart.stepForward(0, 0, 7)
}
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
val weekStart = Date.fromY_m_d(getDate)
if (Date.getToday().weekDay > 4 && weekStart == currentWeekStart) {
weekStart.stepForward(0, 0, 7)
}
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
apiGet(TAG, VULCAN_API_ENDPOINT_TIMETABLE, parameters = mapOf(
@ -114,9 +114,7 @@ class VulcanApiTimetable(override val data: DataVulcan, val onSuccess: () -> Uni
}
}
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
val lessonObject = Lesson(profileId, id).apply {
val lessonObject = Lesson(profileId, -1).apply {
this.type = type
when (type) {
@ -170,15 +168,19 @@ class VulcanApiTimetable(override val data: DataVulcan, val onSuccess: () -> Uni
}
}
}
this.id = buildId()
}
if (type != Lesson.TYPE_NORMAL && lessonDate >= Date.getToday()) {
val seen = profile.empty || lessonDate < Date.getToday()
if (type != Lesson.TYPE_NORMAL) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
id,
profile.empty,
profile.empty,
lessonObject.id,
seen,
seen,
System.currentTimeMillis()
))
}

View File

@ -37,7 +37,10 @@ public abstract class LuckyNumberDao {
@Nullable
@Query("SELECT * FROM luckyNumbers WHERE profileId = :profileId AND luckyNumberDate >= :date ORDER BY luckyNumberDate DESC LIMIT 1")
public abstract LuckyNumber getNearestFutureNow(int profileId, Date date);
public abstract LuckyNumber getNearestFutureNow(int profileId, int date);
@Query("SELECT * FROM luckyNumbers WHERE profileId = :profileId AND luckyNumberDate >= :date ORDER BY luckyNumberDate DESC LIMIT 1")
public abstract LiveData<LuckyNumber> getNearestFuture(int profileId, int date);
@RawQuery(observedEntities = {LuckyNumber.class})
abstract LiveData<List<LuckyNumberFull>> getAll(SupportSQLiteQuery query);

View File

@ -15,7 +15,7 @@ import pl.szczodrzynski.edziennik.utils.models.Time
Index(value = ["profileId", "type", "date"]),
Index(value = ["profileId", "type", "oldDate"])
])
open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
open class Lesson(val profileId: Int, @PrimaryKey var id: Long) {
companion object {
const val TYPE_NO_LESSONS = -1
const val TYPE_NORMAL = 0
@ -45,6 +45,22 @@ open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
var oldTeamId: Long? = null
var oldClassroom: String? = null
val displayDate: Date?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldDate
return date ?: oldDate
}
val displayStartTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldStartTime
return startTime ?: oldStartTime
}
fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF)
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +
@ -66,6 +82,55 @@ open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
"oldTeamId=$oldTeamId, " +
"oldClassroom=$oldClassroom)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Lesson) return false
if (profileId != other.profileId) return false
if (id != other.id) return false
if (type != other.type) return false
if (date != other.date) return false
if (lessonNumber != other.lessonNumber) return false
if (startTime != other.startTime) return false
if (endTime != other.endTime) return false
if (subjectId != other.subjectId) return false
if (teacherId != other.teacherId) return false
if (teamId != other.teamId) return false
if (classroom != other.classroom) return false
if (oldDate != other.oldDate) return false
if (oldLessonNumber != other.oldLessonNumber) return false
if (oldStartTime != other.oldStartTime) return false
if (oldEndTime != other.oldEndTime) return false
if (oldSubjectId != other.oldSubjectId) return false
if (oldTeacherId != other.oldTeacherId) return false
if (oldTeamId != other.oldTeamId) return false
if (oldClassroom != other.oldClassroom) return false
return true
}
override fun hashCode(): Int {
var result = profileId
result = 31 * result + type
result = 31 * result + (date?.hashCode() ?: 0)
result = 31 * result + (lessonNumber ?: 0)
result = 31 * result + (startTime?.hashCode() ?: 0)
result = 31 * result + (endTime?.hashCode() ?: 0)
result = 31 * result + (subjectId?.hashCode() ?: 0)
result = 31 * result + (teacherId?.hashCode() ?: 0)
result = 31 * result + (teamId?.hashCode() ?: 0)
result = 31 * result + (classroom?.hashCode() ?: 0)
result = 31 * result + (oldDate?.hashCode() ?: 0)
result = 31 * result + (oldLessonNumber ?: 0)
result = 31 * result + (oldStartTime?.hashCode() ?: 0)
result = 31 * result + (oldEndTime?.hashCode() ?: 0)
result = 31 * result + (oldSubjectId?.hashCode() ?: 0)
result = 31 * result + (oldTeacherId?.hashCode() ?: 0)
result = 31 * result + (oldTeamId?.hashCode() ?: 0)
result = 31 * result + (oldClassroom?.hashCode() ?: 0)
return result
}
}
/*
DROP TABLE lessons;
@ -94,4 +159,4 @@ CREATE TABLE lessons (
PRIMARY KEY(profileId)
);
*/
*/

View File

@ -1,6 +1,7 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import pl.szczodrzynski.edziennik.utils.models.Date
import android.content.Context
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.models.Time
class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
@ -11,24 +12,13 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
var oldTeacherName: String? = null
var oldTeamName: String? = null
val displayDate: Date?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldDate
return date ?: oldDate
}
val displayLessonNumber: Int?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldLessonNumber
return lessonNumber ?: oldLessonNumber
}
val displayStartTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldStartTime
return startTime ?: oldStartTime
}
val displayEndTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
@ -42,12 +32,14 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
return oldSubjectName
return subjectName ?: oldSubjectName
}
val displayTeacherName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldTeacherName
return teacherName ?: oldTeacherName
}
val displayTeamName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
@ -68,12 +60,14 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
return oldTeamId
return teamId ?: oldTeamId
}
val displaySubjectId: Long?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldSubjectId
return subjectId ?: oldSubjectId
}
val displayTeacherId: Long?
get() {
if (type == TYPE_SHIFTED_SOURCE)
@ -81,8 +75,35 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
return teacherId ?: oldTeacherId
}
fun getDisplayChangeType(context: Context): String {
return context.getString(when (type) {
TYPE_CHANGE -> R.string.lesson_change
TYPE_CANCELLED -> R.string.lesson_cancelled
TYPE_SHIFTED_TARGET, TYPE_SHIFTED_SOURCE -> R.string.lesson_shifted
else -> R.string.lesson_timetable_change
})
}
val changeSubjectName: String
get() {
val first = when (type) {
TYPE_CHANGE, TYPE_CANCELLED, TYPE_SHIFTED_SOURCE -> oldSubjectName
else -> subjectName
}
val second = when (type) {
TYPE_CHANGE -> subjectName
else -> null
}
return when (second) {
null -> first ?: ""
else -> "$first -> $second"
}
}
// metadata
var seen: Boolean = false
var notified: Boolean = false
var addedDate: Long = 0
}
}

View File

@ -75,10 +75,23 @@ interface TimetableDao {
""")
fun getBetweenDatesNow(dateFrom: Date, dateTo: Date) : List<LessonFull>
@Query("""
$QUERY
WHERE (type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo)
ORDER BY profileId, id, type
""")
fun getBetweenDates(dateFrom: Date, dateTo: Date) : LiveData<List<LessonFull>>
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND timetable.id = :lessonId
ORDER BY id, type
""")
fun getByIdNow(profileId: Int, lessonId: Long) : LessonFull?
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND timetable.type NOT IN (${Lesson.TYPE_NORMAL}, ${Lesson.TYPE_NO_LESSONS}, ${Lesson.TYPE_SHIFTED_SOURCE}) AND metadata.notified = 0
""")
fun getNotNotifiedNow(profileId: Int): List<LessonFull>
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-24.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.home
import android.text.InputType
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.materialdialogs.MaterialDialog
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
class StudentNumberDialog(
val activity: AppCompatActivity,
val profile: Profile,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) {
companion object {
private const val TAG = "StudentNumberDialog"
}
private lateinit var dialog: AlertDialog
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
MaterialDialog.Builder(activity)
.title(R.string.card_lucky_number_set_title)
.content(R.string.card_lucky_number_set_text)
.inputType(InputType.TYPE_CLASS_NUMBER)
.input(null, if (profile.studentNumber == -1) "" else profile.studentNumber.toString()) { _: MaterialDialog?, input: CharSequence ->
try {
profile.studentNumber = input.toString().toInt()
} catch (e: Exception) {
Toast.makeText(activity, R.string.incorrect_format, Toast.LENGTH_SHORT).show()
}
}
.dismissListener {
onDismissListener?.invoke(TAG)
}.show()
}}
}

View File

@ -94,15 +94,23 @@ class SyncViewListDialog(
listOfNotNull(*it.toTypedArray())
}
if (selectedViewIds.isNotEmpty()) {
activity.swipeRefreshLayout.isRefreshing = true
EdziennikTask.syncProfile(
App.profileId,
selectedViewIds
).enqueue(activity)
}
}
.setNeutralButton(R.string.sync_feature_all) { _, _ ->
dialog.dismiss()
activity.swipeRefreshLayout.isRefreshing = true
EdziennikTask.syncProfile(
App.profileId,
selectedViewIds
).enqueue(activity)
EdziennikTask.syncProfile(App.profileId).enqueue(activity)
}
.setNegativeButton(R.string.cancel) { _, _ ->
dialog.dismiss()
}
.show()
}}
}
}

View File

@ -120,7 +120,7 @@ class LessonDetailsDialog(
dialog.dismiss()
val dateStr = otherLessonDate?.stringY_m_d ?: return@setOnClickListener
val intent = Intent(TimetableFragment.ACTION_SCROLL_TO_DATE).apply {
putExtra("date", dateStr)
putExtra("timetableDate", dateStr)
}
activity.sendBroadcast(intent)
}
@ -157,4 +157,4 @@ class LessonDetailsDialog(
b.teamName = lesson.teamName
}
}
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2.Companion.swapCards
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, private val refreshLayout: SwipeRefreshLayoutNoIndicator?) : ItemTouchHelper.Callback() {
companion object {
private const val TAG = "CardItemTouchHelperCallback"
private const val DRAG_FLAGS = ItemTouchHelper.UP or ItemTouchHelper.DOWN
private const val SWIPE_FLAGS = 0
}
private var dragCardView: MaterialCardView? = null
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(DRAG_FLAGS, SWIPE_FLAGS)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
swapCards(fromPosition, toPosition, cardAdapter)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
dragCardView = viewHolder.itemView as MaterialCardView
dragCardView?.isDragged = true
refreshLayout?.isEnabled = false
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && dragCardView != null) {
refreshLayout?.isEnabled = true
dragCardView?.isDragged = false
dragCardView = null
}
}
}

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
interface HomeCard {
fun bind(position: Int, holder: HomeCardAdapter.ViewHolder)
fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder)
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import pl.szczodrzynski.edziennik.R
class HomeCardAdapter(var items: MutableList<HomeCard>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val TAG = "HomeCardAdapter"
}
var itemTouchHelper: ItemTouchHelper? = null
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(itemTouchHelper)
items[position].bind(position, holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
items.getOrNull(holder.adapterPosition)?.unbind(holder.adapterPosition, holder as ViewHolder)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.card_home, parent, false) as MaterialCardView
)
}
override fun getItemCount(): Int = items.size
class ViewHolder(val root: MaterialCardView) : RecyclerView.ViewHolder(root) {
@SuppressLint("ClickableViewAccessibility")
fun bind(itemTouchHelper: ItemTouchHelper?) {
/*root.setOnTouchListener { _: View?, event: MotionEvent ->
if (event.action == MotionEvent.ACTION_DOWN) {
itemTouchHelper?.startDrag(this)
return@setOnTouchListener true
}
false
}*/
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import android.widget.TextView
import androidx.core.view.plusAssign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.startCoroutineTimer
import kotlin.coroutines.CoroutineContext
class HomeDummyCard(val id: Int) : HomeCard, CoroutineScope {
companion object {
private const val TAG = "HomeDummyCard"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var timer: Job? = null
var time = 0
override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) { launch {
holder.root.removeAllViews()
//holder.setIsRecyclable(false)
val text = TextView(holder.root.context).apply {
text = "This is a card #$id"
}
holder.root += text
timer = startCoroutineTimer(repeatMillis = 1000) {
time++
text.text = "Coroutine timer at #$id! $time seconds"
}
/*val button = MaterialButton(holder.root.context).apply {
setText("Cancel")
onClick {
timer.cancel()
}
}
holder.root += button*/
}}
override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) {
timer?.cancel()
timer = null
}
}

View File

@ -0,0 +1,141 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.FragmentHomeV2Binding
import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeLuckyNumberCard
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeTimetableCard
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext
class HomeFragmentV2 : Fragment(), CoroutineScope {
companion object {
private const val TAG = "HomeFragment"
fun swapCards(fromPosition: Int, toPosition: Int, cardAdapter: HomeCardAdapter) {
val fromCard = cardAdapter.items[fromPosition]
cardAdapter.items[fromPosition] = cardAdapter.items[toPosition]
cardAdapter.items[toPosition] = fromCard
cardAdapter.notifyItemMoved(fromPosition, toPosition)
}
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentHomeV2Binding
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
b = FragmentHomeV2Binding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
job = Job()
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_set_student_number)
.withIcon(SzkolnyFont.Icon.szf_clipboard_list_outline)
.withOnClickListener(OnClickListener {
activity.bottomSheet.close()
StudentNumberDialog(activity, app.profile) {
app.profileSaveAsync()
}
}),
BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_everything_as_read)
.withIcon(Icon.cmd_eye_check_outline)
.withOnClickListener(OnClickListener {
activity.bottomSheet.close()
AsyncTask.execute { app.db.metadataDao().setAllSeen(App.profileId, true) }
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
})
)
val items = mutableListOf<HomeCard>(
HomeLuckyNumberCard(0, app, activity, this, app.profile),
HomeTimetableCard(1, app, activity, this, app.profile)
)
val adapter = HomeCardAdapter(items)
val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))
adapter.itemTouchHelper = itemTouchHelper
b.list.layoutManager = LinearLayoutManager(activity)
b.list.adapter = adapter
b.list.setAccessibilityDelegateCompat(object : RecyclerViewAccessibilityDelegate(b.list) {
override fun getItemDelegate(): AccessibilityDelegateCompat {
return object : ItemDelegate(this) {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val position: Int = b.list.getChildLayoutPosition(host)
if (position != 0) {
info.addAction(AccessibilityActionCompat(
R.id.move_card_up_action,
host.resources.getString(R.string.card_action_move_up)
))
}
if (position != adapter.itemCount - 1) {
info.addAction(AccessibilityActionCompat(
R.id.move_card_down_action,
host.resources.getString(R.string.card_action_move_down)
))
}
}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle): Boolean {
val fromPosition: Int = b.list.getChildLayoutPosition(host)
if (action == R.id.move_card_down_action) {
swapCards(fromPosition, fromPosition + 1, adapter)
return true
} else if (action == R.id.move_card_up_action) {
swapCards(fromPosition, fromPosition - 1, adapter)
return true
}
return super.performAccessibilityAction(host, action, args)
}
}
}
})
itemTouchHelper.attachToRecyclerView(b.list)
}
}

View File

@ -33,17 +33,17 @@ class HomeTimetableCard(
private val layoutInflater: LayoutInflater,
private val insertPoint: ViewGroup
) {
companion object {
private const val TAG = "HomeTimetableCard"
const val TIME_TILL = 0
const val TIME_LEFT = 1
}
private lateinit var timetableTimer: Timer
private lateinit var b: CardTimetableBinding
private var bellSyncTime: Time? = null
private companion object {
const val TIME_TILL = 0
const val TIME_LEFT = 1
}
private var counterType = TIME_TILL
private val counterTarget = Time(0, 0, 0)

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-24.
*/
package pl.szczodrzynski.edziennik.ui.modules.home.cards
import android.view.LayoutInflater
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import androidx.core.view.plusAssign
import androidx.core.view.setMargins
import androidx.lifecycle.Observer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeLuckyNumberBinding
import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
class HomeLuckyNumberCard(
val id: Int,
val app: App,
val activity: MainActivity,
val fragment: HomeFragmentV2,
val profile: Profile
) : HomeCard, CoroutineScope {
companion object {
private const val TAG = "HomeLuckyNumberCard"
}
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) { launch {
holder.root.removeAllViews()
val b = CardHomeLuckyNumberBinding.inflate(LayoutInflater.from(holder.root.context))
b.root.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
setMargins(8.dp)
}
holder.root += b.root
val today = Date.getToday()
val todayValue = today.value
val subTextRes = if (profile.studentNumber == -1)
R.string.home_lucky_number_details_click_to_set
else
R.string.home_lucky_number_details
b.subText.setText(subTextRes, profile.name ?: "", profile.studentNumber)
app.db.luckyNumberDao().getNearestFuture(App.profileId, todayValue).observe(fragment, Observer { luckyNumber ->
val isYours = luckyNumber?.number == profile.studentNumber
val titleRes = when {
luckyNumber == null -> R.string.home_lucky_number_no_info
luckyNumber.number == -1 -> R.string.home_lucky_number_no_number
else -> when (isYours) {
true -> when (luckyNumber.date.value) {
todayValue -> R.string.home_lucky_number_yours_today
todayValue + 1 -> R.string.home_lucky_number_yours_tomorrow
else -> R.string.home_lucky_number_yours_later
}
false -> when (luckyNumber.date.value) {
todayValue -> R.string.home_lucky_number_today
todayValue + 1 -> R.string.home_lucky_number_tomorrow
else -> R.string.home_lucky_number_later
}
}
}
b.title.setText(
titleRes,
luckyNumber?.number ?: 0,
luckyNumber?.date?.formattedString ?: ""
)
val drawableRes = when {
luckyNumber == null || luckyNumber.number == -1 -> R.drawable.emoji_sad
isYours -> R.drawable.emoji_glasses
!isYours -> R.drawable.emoji_smiling
else -> R.drawable.emoji_no_face
}
b.image.setImageResource(drawableRes)
})
holder.root.onClick {
StudentNumberDialog(activity, profile, onDismissListener = {
app.profileSaveAsync(profile)
val newSubTextRes = if (profile.studentNumber == -1)
R.string.home_lucky_number_details_click_to_set
else
R.string.home_lucky_number_details
b.subText.setText(newSubTextRes, profile.name ?: "", profile.studentNumber)
})
}
}}
override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-24.
*/
package pl.szczodrzynski.edziennik.ui.modules.home.cards
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.plusAssign
import androidx.core.view.setMargins
import androidx.lifecycle.Observer
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.modules.events.Event
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.CardHomeTimetableBinding
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard
import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.coroutines.CoroutineContext
class HomeTimetableCard(
val id: Int,
val app: App,
val activity: MainActivity,
val fragment: HomeFragmentV2,
val profile: Profile
) : HomeCard, CoroutineScope {
companion object {
private const val TAG = "HomeTimetableCard"
}
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private lateinit var b: CardHomeTimetableBinding
private val today = Date.getToday()
private val searchEnd = today.clone().stepForward(0, 0, 7)
private var allLessons = listOf<LessonFull>()
private var lessons = listOf<LessonFull>()
private var events = listOf<Event>()
override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) {
holder.root.removeAllViews()
b = CardHomeTimetableBinding.inflate(LayoutInflater.from(holder.root.context))
b.root.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
setMargins(8.dp)
}
holder.root += b.root
// get all lessons within the search bounds
app.db.timetableDao().getBetweenDates(today, searchEnd).observe(fragment, Observer {
allLessons = it
update()
})
}
private fun update() { launch {
val deferred = async(Dispatchers.Default) {
// get current bell-sync params
var bellSyncDiffMillis: Long = 0
if (app.appConfig.bellSyncDiff != null) {
bellSyncDiffMillis = (app.appConfig.bellSyncDiff.hour * 60 * 60 * 1000 + app.appConfig.bellSyncDiff.minute * 60 * 1000 + app.appConfig.bellSyncDiff.second * 1000).toLong()
bellSyncDiffMillis *= app.appConfig.bellSyncMultiplier.toLong()
bellSyncDiffMillis *= -1
}
// get the current bell-synced time
val now = Time.fromMillis(Time.getNow().inMillis + bellSyncDiffMillis)
// search for lessons to display
val timetableDate = Date.getToday()
var checkedDays = 0
lessons = allLessons.filter { it.profileId == profile.id && it.displayDate == timetableDate && it.displayEndTime > now && it.type != Lesson.TYPE_NO_LESSONS }
while ((lessons.isEmpty() || lessons.none {
it.displayDate != today || (it.displayDate == today && it.displayEndTime != null && it.displayEndTime!! >= now)
}) && checkedDays < 7) {
timetableDate.stepForward(0, 0, 1)
lessons = allLessons.filter { it.profileId == profile.id && it.displayDate == timetableDate && it.type != Lesson.TYPE_NO_LESSONS }
checkedDays++
}
}
deferred.await()
val text = StringBuilder()
for (lesson in lessons) {
text += lesson.displayStartTime?.stringHM+" "+lesson.displaySubjectName+"\n"
}
b.text.text = text.toString()
}}
override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit
}

View File

@ -31,6 +31,9 @@ import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent.Companion.TYPE_FINISHED
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent.Companion.TYPE_PROGRESS
import pl.szczodrzynski.edziennik.api.v2.events.MessageGetEvent
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
@ -225,18 +228,17 @@ class MessageFragment : Fragment(), CoroutineScope {
attachmentChip.ellipsize = TextUtils.TruncateAt.MIDDLE
// create an icon for the attachment
var icon: IIcon = CommunityMaterial.Icon.cmd_file_outline
when (Utils.getExtensionFromFileName(name)) {
"txt" -> icon = CommunityMaterial.Icon.cmd_file_document_outline
"doc", "docx", "odt", "rtf" -> icon = SzkolnyFont.Icon.szf_file_word_outline
"xls", "xlsx", "ods" -> icon = SzkolnyFont.Icon.szf_file_excel_outline
"ppt", "pptx", "odp" -> icon = SzkolnyFont.Icon.szf_file_powerpoint_outline
"pdf" -> icon = SzkolnyFont.Icon.szf_file_pdf_outline
"mp3", "wav", "aac" -> icon = SzkolnyFont.Icon.szf_file_music_outline
"mp4", "avi", "3gp", "mkv", "flv" -> icon = SzkolnyFont.Icon.szf_file_video_outline
"jpg", "jpeg", "png", "bmp", "gif" -> icon = SzkolnyFont.Icon.szf_file_image_outline
"zip", "rar", "tar", "7z" -> icon = SzkolnyFont.Icon.szf_zip_box_outline
"html", "cpp", "c", "h", "css", "java", "py" -> icon = SzkolnyFont.Icon.szf_file_code_outline
val icon: IIcon = when (Utils.getExtensionFromFileName(name)) {
"doc", "docx", "odt", "rtf" -> SzkolnyFont.Icon.szf_file_word_outline
"xls", "xlsx", "ods" -> SzkolnyFont.Icon.szf_file_excel_outline
"ppt", "pptx", "odp" -> SzkolnyFont.Icon.szf_file_powerpoint_outline
"pdf" -> SzkolnyFont.Icon.szf_file_pdf_outline
"mp3", "wav", "aac" -> SzkolnyFont.Icon.szf_file_music_outline
"mp4", "avi", "3gp", "mkv", "flv" -> SzkolnyFont.Icon.szf_file_video_outline
"jpg", "jpeg", "png", "bmp", "gif" -> SzkolnyFont.Icon.szf_file_image_outline
"zip", "rar", "tar", "7z" -> SzkolnyFont.Icon.szf_zip_box_outline
"html", "cpp", "c", "h", "css", "java", "py" -> SzkolnyFont.Icon.szf_file_code_outline
else -> CommunityMaterial.Icon.cmd_file_document_outline
}
attachmentChip.chipIcon = IconicsDrawable(activity).color(IconicsColor.colorRes(R.color.colorPrimary)).icon(icon).size(IconicsSize.dp(26))
attachmentChip.closeIcon = IconicsDrawable(activity).icon(CommunityMaterial.Icon.cmd_check).size(IconicsSize.dp(18)).color(IconicsColor.colorInt(Utils.getAttr(activity, android.R.attr.textColorPrimary)))
@ -245,7 +247,7 @@ class MessageFragment : Fragment(), CoroutineScope {
attachmentChip.tag = index
attachmentChip.setOnClickListener { v ->
if (v.tag is Int) {
// TODO downloadAttachment(v.tag as Int)
downloadAttachment(v.tag as Int)
}
}
attachmentLayout.addView(attachmentChip)
@ -269,6 +271,60 @@ class MessageFragment : Fragment(), CoroutineScope {
}
}
private fun downloadAttachment(index: Int) {
val attachment = attachmentList[index]
if (attachment.downloaded != null) {
Utils.openFile(activity, File(attachment.downloaded))
return
}
attachment.chip.isEnabled = false
attachment.chip.setTextColor(Themes.getSecondaryTextColor(activity))
attachment.progressBar.visibility = View.VISIBLE
EdziennikTask.attachmentGet(
App.profileId,
attachment.messageId,
attachment.attachmentId,
attachment.attachmentName
).enqueue(activity)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onAttachmentGetEvent(event: AttachmentGetEvent) {
attachmentList.firstOrNull { it.profileId == event.profileId
&& it.messageId == event.messageId
&& it.attachmentId == event.attachmentId }?.let { attachment ->
when (event.eventType) {
TYPE_FINISHED -> {
// save the downloaded file name
attachment.downloaded = event.fileName
// set the correct name (and size)
if (attachment.attachmentSize == -1L)
attachment.chip.text = getString(R.string.messages_attachment_no_size_format, attachment.attachmentName)
else
attachment.chip.text = getString(R.string.messages_attachment_format, attachment.attachmentName, readableFileSize(attachment.attachmentSize))
// hide the progress bar and show a tick icon
attachment.progressBar.visibility = View.GONE
attachment.chip.isEnabled = true
attachment.chip.setTextColor(Themes.getPrimaryTextColor(activity))
attachment.chip.isCloseIconVisible = true
// open the file
Utils.openFile(activity, File(attachment.downloaded))
}
TYPE_PROGRESS -> {
attachment.chip.text = getString(R.string.messages_attachment_downloading_format, attachment.attachmentName, event.bytesWritten.toFloat() / 1000000)
}
}
}
}
private fun checkAttachment(attachment: Attachment) {
val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu")
storageDir.mkdirs()
@ -286,7 +342,6 @@ class MessageFragment : Fragment(), CoroutineScope {
e.printStackTrace()
//app.apiEdziennik.guiReportException(activity, 355, e)
}
}
}

View File

@ -67,7 +67,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
override fun onReceive(context: Context, i: Intent) {
if (!isAdded)
return
val dateStr = i.extras?.getString("date", null) ?: return
val dateStr = i.extras?.getString("timetableDate", null) ?: return
val date = Date.fromY_m_d(dateStr)
b.viewPager.setCurrentItem(items.indexOf(date), true)
}
@ -155,8 +155,10 @@ class TimetableFragment : Fragment(), CoroutineScope {
}
})
val selectedDate = arguments?.getString("timetableDate", "")?.let { if (it.isBlank()) null else Date.fromY_m_d(it) }
b.tabLayout.setUpWithViewPager(b.viewPager)
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, false)
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == selectedDate?.value ?: today }, false)
activity.navView.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)

View File

@ -272,4 +272,12 @@ public class Date implements Comparable<Date> {
", day=" + day +
'}';
}
@Override
public int hashCode() {
int result = year;
result = 31 * result + month;
result = 31 * result + day;
return result;
}
}

View File

@ -121,7 +121,7 @@ public class Time implements Comparable<Time> {
public String getStringValue()
{
return (hour < 10 ? "0" : "")+Integer.toString(hour)+(minute < 10 ? "0" : "")+Integer.toString(minute)+(second < 10 ? "0" : "")+Integer.toString(second);
return (hour < 10 ? "0" : "")+ hour +(minute < 10 ? "0" : "")+ minute +(second < 10 ? "0" : "")+ second;
}
public String getStringHM()
@ -129,18 +129,18 @@ public class Time implements Comparable<Time> {
if (hour < 0) {
return "";
}
return Integer.toString(hour)+":"+(minute < 10 ? "0" : "")+Integer.toString(minute);
return hour +":"+(minute < 10 ? "0" : "")+ minute;
}
public String getStringH_M()
{
if (hour < 0) {
return "";
}
return Integer.toString(hour)+"-"+(minute < 10 ? "0" : "")+Integer.toString(minute);
return hour +"-"+(minute < 10 ? "0" : "")+ minute;
}
public String getStringHMS()
{
return Integer.toString(hour)+":"+(minute < 10 ? "0" : "")+Integer.toString(minute)+":"+(second < 10 ? "0" : "")+Integer.toString(second);
return hour +":"+(minute < 10 ? "0" : "")+ minute +":"+(second < 10 ? "0" : "")+ second;
}
public static Time getNow()
@ -194,4 +194,12 @@ public class Time implements Comparable<Time> {
", second=" + second +
'}';
}
@Override
public int hashCode() {
int result = hour;
result = 31 * result + minute;
result = 31 * result + second;
return result;
}
}

View File

@ -0,0 +1,28 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M63.79,8.64C1.48,8.64 0,78.5 0,92.33c0,13.83 28.56,25.03 63.79,25.03c35.24,0 63.79,-11.21 63.79,-25.03C127.58,78.5 126.11,8.64 63.79,8.64z"
android:fillColor="#FCC21B"/>
<path
android:pathData="M63.91,104.82c-3.43,0 -6.87,-0.43 -10.25,-1.31c-1.6,-0.42 -2.56,-2.06 -2.15,-3.66c0.42,-1.6 2.06,-2.56 3.66,-2.14c11.65,3.04 24.21,-0.21 32.78,-8.48c1.19,-1.15 3.09,-1.12 4.24,0.08c1.15,1.19 1.12,3.09 -0.08,4.24C84.54,100.85 74.32,104.82 63.91,104.82z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M55.53,67.26c-0.01,0.01 -0.02,0.02 -0.02,0.02C55.51,67.27 55.52,67.26 55.53,67.26z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M98.21,41.34c-13.36,0 -15.15,2.03 -21.4,3.36C70.56,46.02 64,46.02 64,46.02s-6.56,0 -12.81,-1.33c-6.25,-1.33 -8.05,-3.36 -21.4,-3.36c-13.36,0 -29.37,2.89 -29.37,2.89v8.51c0,0 3.59,0.47 3.91,3.75c0.16,1.33 -3.12,28.35 23.51,28.35c18.9,0 26.87,-11.33 29.45,-20.54c1.17,-4.37 2.19,-9.37 6.72,-9.37c4.53,0 5.55,5 6.72,9.37c2.58,9.22 10.54,20.54 29.45,20.54c26.63,0 23.35,-27.03 23.51,-28.35c0.31,-3.28 3.91,-3.75 3.91,-3.75v-8.51C127.58,44.23 111.57,41.34 98.21,41.34z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M95.94,45.05c-6.62,0.23 -11.65,1.31 -11.65,1.31c-9.84,2.06 -10.55,8.14 -9.93,12.97c0.8,6.07 3.29,13.75 10.04,18.49c0.53,0.38 1.76,0.79 2.35,-0.77c0,0 -0.02,0.11 0,0c2.22,-10.48 5.52,-20.14 10.78,-29.89l0,0C98.14,45.37 96.71,45.02 95.94,45.05z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M31.06,45.02c-4.27,-0.09 -9.11,0.19 -13.65,1.34c-5.1,1.28 -7.07,3.85 -7.6,9.39c-0.53,5.43 -1.13,19.27 8.73,24.46c0.57,0.3 1.83,0.5 2.44,-0.91l0,0C24,66.21 25.61,60.13 32.54,47.22l0,0C33.11,45.49 31.83,45.03 31.06,45.02z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -0,0 +1,22 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M64,9.56c-62.41,0 -63.88,69.96 -63.88,83.8c0,13.86 28.59,25.08 63.88,25.08c35.28,0 63.88,-11.22 63.88,-25.08C127.88,79.52 126.4,9.56 64,9.56z"
android:fillColor="#FCC21B"/>
<path
android:pathData="M42.21,62.3c-4.49,0.04 -8.17,-4.27 -8.22,-9.62c-0.05,-5.37 3.55,-9.75 8.04,-9.79c4.48,-0.04 8.17,4.27 8.22,9.64C50.3,57.88 46.7,62.25 42.21,62.3z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M86.32,62.3c4.48,-0.01 8.11,-4.36 8.1,-9.71c-0.01,-5.37 -3.66,-9.7 -8.14,-9.69c-4.49,0.01 -8.13,4.36 -8.12,9.73C78.18,57.98 81.83,62.31 86.32,62.3z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M89.69,84.75H38.31c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3h51.39c1.66,0 3,1.34 3,3S91.35,84.75 89.69,84.75z"
android:fillColor="#2F2F2F"/>
</vector>

View File

@ -0,0 +1,19 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M64,9.56c-62.41,0 -63.88,69.96 -63.88,83.8c0,13.86 28.59,25.08 63.88,25.08c35.28,0 63.88,-11.22 63.88,-25.08C127.88,79.52 126.4,9.56 64,9.56z"
android:fillColor="#FCC21B"/>
<path
android:pathData="M42.21,65.3c-4.49,0.04 -8.17,-4.27 -8.22,-9.62c-0.05,-5.37 3.55,-9.75 8.04,-9.79c4.48,-0.04 8.17,4.27 8.22,9.64C50.3,60.88 46.7,65.25 42.21,65.3z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M86.32,65.3c4.48,-0.01 8.11,-4.36 8.1,-9.71c-0.01,-5.37 -3.66,-9.7 -8.14,-9.69c-4.49,0.01 -8.13,4.36 -8.12,9.73C78.18,60.98 81.83,65.31 86.32,65.3z"
android:fillColor="#2F2F2F"/>
</vector>

View File

@ -0,0 +1,22 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M64,9.62c-62.41,0 -63.88,69.96 -63.88,83.8c0,13.86 28.59,25.08 63.88,25.08c35.28,0 63.88,-11.22 63.88,-25.08C127.88,79.58 126.4,9.62 64,9.62z"
android:fillColor="#FCC21B"/>
<path
android:pathData="M41.99,65.5c-4.49,0.04 -8.17,-4.27 -8.22,-9.62c-0.05,-5.37 3.55,-9.75 8.04,-9.79c4.48,-0.04 8.17,4.27 8.22,9.64C50.08,61.09 46.47,65.46 41.99,65.5z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M86.1,65.5c4.48,-0.01 8.11,-4.36 8.1,-9.71c-0.01,-5.37 -3.66,-9.7 -8.14,-9.69c-4.49,0.01 -8.13,4.36 -8.12,9.73C77.95,61.18 81.61,65.51 86.1,65.5z"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M43.08,97.67c1.99,1.34 4.5,0.46 6.71,0c6.18,-1.28 11.6,-1.33 14.2,-1.33s8.03,0.05 14.2,1.33c2.21,0.46 4.72,1.34 6.71,0c2.52,-1.71 0.66,-7.83 -3.31,-11.97c-2.4,-2.5 -8.13,-7.35 -17.61,-7.35c-9.48,0 -15.2,4.85 -17.61,7.35C42.42,89.85 40.56,95.97 43.08,97.67z"
android:fillColor="#ED6C30"/>
</vector>

View File

@ -0,0 +1,22 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M127.94,93.75c0,14.02 -28.61,25.39 -63.93,25.39S0.06,107.77 0.06,93.75c0,-14.03 1.48,-84.89 63.95,-84.89C126.47,8.86 127.94,79.72 127.94,93.75"
android:fillColor="#FCC21B"/>
<path
android:pathData="M48.14,57.33c0,5.47 -3.66,9.9 -8.19,9.9c-4.53,0 -8.21,-4.43 -8.21,-9.9c0,-5.48 3.68,-9.91 8.21,-9.91C44.48,47.42 48.14,51.85 48.14,57.33"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M80.14,57.33c0,5.47 3.66,9.9 8.21,9.9c4.53,0 8.21,-4.43 8.21,-9.9c0,-5.48 -3.68,-9.91 -8.21,-9.91C83.8,47.42 80.14,51.85 80.14,57.33"
android:fillColor="#2F2F2F"/>
<path
android:pathData="M66.8,93.74c-0.72,0 -1.48,-0.03 -2.25,-0.07C46.53,92.59 39.69,82.82 39.41,82.4c-1,-1.48 -0.62,-3.48 0.85,-4.48c1.46,-1 3.45,-0.62 4.46,0.83c0.25,0.37 5.61,7.61 20.22,8.47c14.57,0.84 20.91,-8.67 20.99,-8.77c0.95,-1.49 2.97,-1.92 4.45,-0.96c1.49,0.97 1.93,2.96 0.95,4.45C91.01,82.46 83.52,93.74 66.8,93.74z"
android:fillColor="#2F2F2F"/>
</vector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-23.
-->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true" />

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_margin="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/image"
android:layout_width="40dp"
android:layout_height="40dp"
app:srcCompat="@drawable/emoji_sad" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text="@string/home_lucky_number_no_info"
android:textAppearance="@style/NavView.TextView.Title" />
</LinearLayout>
<TextView
android:id="@+id/subText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Helper"
tools:text="Oranż Metylowy • Numer w dzienniku to 23" />
</LinearLayout>
</layout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_margin="8dp">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-23.
-->
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/card_home" />
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -295,6 +295,7 @@
<string name="lesson_break">Break</string>
<string name="lesson_cancelled">Lesson cancelled</string>
<string name="lesson_change">Lesson change</string>
<string name="lesson_shifted">Shifted lesson</string>
<string name="lesson_timetable_change">Timetable change</string>
<string name="loading">Loading…</string>
<string name="login_allow_registration">Allow registration</string>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-24.
-->
<resources>
<item name="move_card_down_action" type="id"/>
<item name="move_card_up_action" type="id"/>
</resources>

View File

@ -318,6 +318,7 @@
<string name="lesson_break">Przerwa</string>
<string name="lesson_cancelled">Lekcja odwołana</string>
<string name="lesson_change">Zastępstwo</string>
<string name="lesson_shifted">Lekcja przeniesiona</string>
<string name="lesson_timetable_change">Zmiana planu</string>
<string name="loading">Ładowanie…</string>
<string name="login_allow_registration">Zezwól na rejestrację</string>
@ -448,7 +449,7 @@
<string name="menu_timetable">Plan lekcji</string>
<string name="messages_attachment_cannot_download">Nie można pobrać załącznika</string>
<string name="messages_attachment_cannot_download_text">Wystąpił błąd wewnętrzny pobierania załącznika. Może to być spowodowane słabym połączeniem internetowym.</string>
<string name="messages_attachment_downloading_format" translatable="false">%s (%d%%)</string>
<string name="messages_attachment_downloading_format" translatable="false">%s (%.2fMB)</string>
<string name="messages_attachment_format" translatable="false">%s (%s)</string>
<string name="messages_attachment_no_size_format" translatable="false">%s</string>
<string name="messages_compose_menu_attachment">Dodaj załącznik</string>
@ -1031,4 +1032,16 @@
<string name="menu_remove_notifications">Usuń wszystkie</string>
<string name="menu_remove_notifications_success">Wyczyszczono powiadomienia</string>
<string name="timetable_select_day">Wybierz dzień</string>
<string name="card_action_move_up">Przesuń w górę</string>
<string name="card_action_move_down">Przesuń w dół</string>
<string name="home_lucky_number_details_click_to_set">%s • Kliknij, aby ustawić swój numerek.</string>
<string name="home_lucky_number_details">%s • Numer w dzienniku to %d</string>
<string name="home_lucky_number_no_info">Brak informacji o szczęśliwym numerku.</string>
<string name="home_lucky_number_yours_today">Dzisiaj to Ty masz szczęśliwy numerek!</string>
<string name="home_lucky_number_yours_tomorrow">Jutro to Ty masz szczęśliwy numerek!</string>
<string name="home_lucky_number_yours_later">Dnia %s Twój numerek jest szczęśliwy.</string>
<string name="home_lucky_number_later">Dnia %s szczęśliwy numerek to %d.</string>
<string name="home_lucky_number_today">%d to dzisiejszy szczęśliwy numerek.</string>
<string name="home_lucky_number_tomorrow">%d to szczęśliwy numerek na jutro.</string>
<string name="home_lucky_number_no_number">Nie ma dzisiaj szczęśliwego numerka.</string>
</resources>

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.3.50'
release = [
versionName: "3.9.10-dev",
versionCode: 3091000
versionName: "3.9.11-dev",
versionCode: 3091100
]
setup = [