Commit 99668687 authored by Filip Wiesner's avatar Filip Wiesner

FEL/FIT RSS in 'Tasks' tab, LiveData combine

parent 73a20fe6
......@@ -42,57 +42,106 @@ inline infix fun<T> MutableLiveData<Result<T>>.asProgressStatus(job: () -> Resul
postValue(job.invoke())
}
inline infix fun<T> MutableLiveData<Result<T>>.fetchUsing(crossinline job: suspend () -> Result<T>) =
launch {
postValue(Loading())
postValue(job.invoke())
}
launch {
postValue(Loading())
postValue(job.invoke())
}
infix fun <T> LiveData<T>.passTo(data: MutableLiveData<T>) = observeForever { data.value = it }
fun<T, K> LiveData<T>.combineWith(data: LiveData<K>, observe: (data1: T?, data2: K?) -> Unit) {
CombinedLiveData(this, data, observe)
}
fun<T, K> LiveData<T>.combineWith(data: LiveData<K>, destination: MutableLiveData<Pair<T?,K?>>) =
combineWith(data) { first: T?, second: K? ->
destination.postValue(Pair(first, second))
}
/**
* LiveData combining
*/
fun <T> LiveData<T>.observeOnBackground(onChange: (T?) -> Unit) {
BackgroundLiveData(this, onChange)
}
fun <A, B, R> LiveData<A>.combineWith(data: LiveData<B>, combine: (data1: A?, data2: B?) -> R) =
combineLiveData(this, data, combine)
fun <A, B, R> combineLiveData(
source1: LiveData<A>,
source2: LiveData<B>,
combine: (data1: A?, data2: B?) -> R)
: LiveData<R> {
class BackgroundLiveData<T, R>(source: LiveData<T>, stalker:(T?) -> R) : MediatorLiveData<R>() {
private var data: T? = source.value
return MediatorLiveData<R>().apply {
var data1: A? = null
var data2: B? = null
init {
super.addSource(source) {
data = it
value = stalker(data)
addSource(source1) {
data1 = it
value = combine(data1, data2)
}
addSource(source2) {
data2 = it
value = combine(data1, data2)
}
}
}
class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() {
fun <A, B, C, R> combineLiveData(
source1: LiveData<A>,
source2: LiveData<B>,
source3: LiveData<C>,
combine: (data1: A?, data2: B?, data3: C?) -> R)
: LiveData<R> {
private var data1: T? = source1.value
private var data2: K? = source2.value
return MediatorLiveData<R>().apply {
var data1: A? = null
var data2: B? = null
var data3: C? = null
init {
super.addSource(source1) {
addSource(source1) {
data1 = it
value = combine(data1, data2)
value = combine(data1, data2, data3)
}
super.addSource(source2) {
addSource(source2) {
data2 = it
value = combine(data1, data2)
value = combine(data1, data2, data3)
}
addSource(source3) {
data3 = it
value = combine(data1, data2, data3)
}
}
}
fun <A, B, C, D, R> combineLiveData(
source1: LiveData<A>,
source2: LiveData<B>,
source3: LiveData<C>,
source4: LiveData<D>,
combine: (data1: A?, data2: B?, data3: C?, data4: D?) -> R)
: LiveData<R> {
return MediatorLiveData<R>().apply {
var data1: A? = null
var data2: B? = null
var data3: C? = null
var data4: D? = null
addSource(source1) {
data1 = it
value = combine(data1, data2, data3, data4)
}
override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) =
throw UnsupportedOperationException()
addSource(source2) {
data2 = it
value = combine(data1, data2, data3, data4)
}
override fun <S : Any?> removeSource(toRemote: LiveData<S>) =
throw UnsupportedOperationException()
addSource(source3) {
data3 = it
value = combine(data1, data2, data3, data4)
}
addSource(source4) {
data4 = it
value = combine(data1, data2, data3, data4)
}
}
}
/**
......
package com.cvut.blackbird.flows.authentication
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import android.view.Gravity
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintSet
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import androidx.transition.TransitionManager
import com.cvut.blackbird.R
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.extensions.appendParameters
import com.cvut.blackbird.extensions.buildUri
import com.cvut.blackbird.model.Failure
import com.cvut.blackbird.model.Success
import com.cvut.blackbird.model.entities.Student
import com.cvut.blackbird.model.services.AuthService
import com.cvut.blackbird.model.services.UserInfo
import com.cvut.blackbird.model.support.AuthResult
import com.cvut.blackbird.support.glue.*
import com.github.florent37.kotlin.pleaseanimate.please
import com.cvut.blackbird.support.glue.bind
import com.cvut.blackbird.support.glue.observe
import com.cvut.blackbird.support.glue.toPassValueTo
import com.cvut.blackbird.support.glue.visibilityTo
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.auth_fragment.*
......@@ -77,13 +72,10 @@ class AuthFragment : Fragment() {
if (!result.critical) authorize()
else snack!!.show()
}
}
observe(viewModel.authLoadingStatus) { if (it) snack?.dismiss() }
bind(authProgress) visibilityTo viewModel.authLoadingStatus
bind(viewModel.userInitState) toPassValueTo infoMessage::setText
}
......
......@@ -3,7 +3,6 @@ package com.cvut.blackbird.flows.authentication
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.model.flows.AuthModel
import com.cvut.blackbird.extensions.asBoolProgressStatus
import com.cvut.blackbird.extensions.fetchUsing
import com.cvut.blackbird.extensions.indicateProgressBy
......@@ -12,7 +11,7 @@ import com.cvut.blackbird.model.Failure
import com.cvut.blackbird.model.NotYet
import com.cvut.blackbird.model.Result
import com.cvut.blackbird.model.Success
import com.cvut.blackbird.model.entities.Student
import com.cvut.blackbird.model.flows.AuthModel
import com.cvut.blackbird.model.support.AuthResult
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.launch
......
package com.cvut.blackbird.flows.tasks
import android.annotation.TargetApi
import android.content.Context
import android.content.res.ColorStateList
import android.os.Build
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.cvut.blackbird.R
import com.cvut.blackbird.extensions.courseAbbr
import com.cvut.blackbird.extensions.lastIndexOfNumber
import com.cvut.blackbird.extensions.setGone
import com.cvut.blackbird.extensions.setVisible
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.EventType
import com.cvut.blackbird.model.entities.News
import com.cvut.blackbird.support.kolor.Kolor
import kotlinx.android.synthetic.main.exam_list_row.view.*
import kotlinx.android.synthetic.main.lecture_list_row.view.*
import kotlinx.android.synthetic.main.tasks_news.view.*
import kotlinx.android.synthetic.main.tasks_pinned.view.*
import org.joda.time.DateTime
import org.joda.time.Days
import org.joda.time.Minutes
sealed class Task(val id: Int)
data class Exam(val event: Event): Task(event.id)
data class Pinned(val event: Event): Task(event.id)
data class ExamTask(val event: Event): Task(event.id)
data class PinnedTask(val event: Event): Task(event.id)
data class NewsTask(val news: News): Task(news.title.hashCode())
class TasksListAdapter(private val clickListener: (Task) -> Unit): ListAdapter<Task, TaskViewHolder>(
object : DiffUtil.ItemCallback<Task>() {
......@@ -37,12 +41,14 @@ class TasksListAdapter(private val clickListener: (Task) -> Unit): ListAdapter<T
companion object {
const val EXAM = 1
const val PINNED = 2
const val NEWS = 3
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Exam -> EXAM
is Pinned -> PINNED
is ExamTask -> EXAM
is PinnedTask -> PINNED
is NewsTask -> NEWS
}
}
......@@ -52,6 +58,8 @@ class TasksListAdapter(private val clickListener: (Task) -> Unit): ListAdapter<T
.inflate(R.layout.exam_list_row, parent, false))
PINNED -> PinnedViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.tasks_pinned, parent, false), parent.context)
NEWS -> NewsViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.tasks_news, parent, false))
else -> throw Exception("This view type is not implemented")
}
}
......@@ -66,7 +74,7 @@ sealed class TaskViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
class ExamViewHolder(view: View) : TaskViewHolder(view) {
override fun bind(task: Task, clickListener: (Task) -> Unit) {
val event = (task as? Exam)?.event ?: throw TypeCastException("You have to pass Exam in ExamViewHolder dumbass")
val event = (task as? ExamTask)?.event ?: throw TypeCastException("You have to pass ExamTask to ExamViewHolder dumbass")
view.examLengthTxt.text = "${Minutes.minutesBetween(event.startsAt, event.endsAt).minutes} minutes"
view.examOccupiedTxt.text = event.occupied.toString()
......@@ -98,7 +106,7 @@ class ExamViewHolder(view: View) : TaskViewHolder(view) {
class PinnedViewHolder(view: View, val context: Context) : TaskViewHolder(view) {
override fun bind(task: Task, clickListener: (Task) -> Unit) {
val pinnedEvent = (task as? Pinned)?.event ?: throw TypeCastException("You have to pass pinned in PinnedViewHolder dumbass")
val pinnedEvent = (task as? PinnedTask)?.event ?: throw TypeCastException("You have to pass pinnedTask to PinnedViewHolder dumbass")
view.pin_eventAbbr.background = when {
pinnedEvent.eventType == EventType.LECTURE -> context.resources.getDrawable(R.drawable.ic_timetable_lecture, context.theme)
......@@ -120,3 +128,18 @@ class PinnedViewHolder(view: View, val context: Context) : TaskViewHolder(view)
}
}
class NewsViewHolder(view: View) : TaskViewHolder(view) {
override fun bind(task: Task, clickListener: (Task) -> Unit) {
val news = (task as? NewsTask)?.news ?: throw TypeCastException("You have to pass NewsTask to NewsViewHolder dumbass")
view.news_title.text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(news.title, Html.FROM_HTML_MODE_COMPACT)
} else Html.fromHtml(news.title)
// (itemView.layoutParams as StaggeredGridLayoutManager.LayoutParams)
// .isFullSpan = true
view.setOnClickListener { clickListener(task) }
}
}
package com.cvut.blackbird.flows.tasks
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import android.util.Log
......@@ -7,21 +9,19 @@ import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.NavOptions
import androidx.recyclerview.widget.*
import com.cvut.blackbird.BlackBirdAC
import com.cvut.blackbird.R
import com.cvut.blackbird.flows.detail.EventDetailViewModel
import com.cvut.blackbird.flows.findMainNav
import com.cvut.blackbird.flows.timetable.TimetableViewModel
import com.cvut.blackbird.model.Failure
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.News
import com.cvut.blackbird.support.glue.bind
import com.cvut.blackbird.support.glue.observe
import com.cvut.blackbird.support.glue.toPassValueTo
import kotlinx.android.synthetic.main.tasks_fragment.*
import org.jetbrains.anko.toast
class TasksFragment : Fragment() {
......@@ -50,14 +50,22 @@ class TasksFragment : Fragment() {
private fun setupUi() {
taskAdapter = TasksListAdapter {
when (it) {
is Pinned -> goToEventDetail(it.event)
is Exam -> goToEventDetail(it.event)
is PinnedTask -> goToEventDetail(it.event)
is ExamTask -> goToEventDetail(it.event)
is NewsTask -> goToNews(it.news)
}
}
examsRecyclerView.adapter = taskAdapter
examsRecyclerView.layoutManager = StaggeredGridLayoutManager(2, RecyclerView.VERTICAL)
}
private fun goToNews(news: News) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(news.link))
if (activity != null && intent.resolveActivity(activity!!.packageManager) != null) {
startActivity(intent)
}
}
private fun setupBinding() {
bind(viewModel.tasks) toPassValueTo taskAdapter::submitList
......
package com.cvut.blackbird.flows.tasks
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.BlackBirdAC
import androidx.lifecycle.*
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.model.*
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.News
import com.cvut.blackbird.model.flows.TasksModel
import com.github.magneticflux.livedata.zipTo
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
import kotlin.concurrent.thread
class TasksViewModel : ViewModel() {
private val model: TasksModel = TasksModel()
......@@ -22,6 +18,7 @@ class TasksViewModel : ViewModel() {
private val _refreshResult = MutableLiveData<Result<Unit>>() withDefault NotYet()
private val _exams = MutableLiveData<List<Event>>() withDefault listOf()
private val _tasks = MutableLiveData<List<Task>>() withDefault listOf()
private val _news = MutableLiveData<List<News>>() withDefault listOf()
val loadingStatus: LiveData<Boolean> get() = _loadingStatus
val refreshResult: LiveData<Result<Unit>> get() = _refreshResult
......@@ -37,19 +34,41 @@ class TasksViewModel : ViewModel() {
fun refresh() {
refreshJob?.cancel()
refreshJob = _refreshResult
.fetchUsing { model.refreshEvents() }
.fetchUsing {
val resultNews = model.getNews()
if (resultNews is Success) {
_news.postValue(resultNews.value)
model.refreshEvents()
} else Failure((resultNews as? Failure)
?.message ?: "Unexpected error")
}
.indicateProgressBy( _loadingStatus )
}
private fun configTasks() { launch {
val result = model.getExams() zipTo model.getPinnedEvents()
withContext(UI) {
result.observeForever { (exams, pinned) ->
val resultTasks = ArrayList<Task>()
for (item in pinned) resultTasks.add(Pinned(item))
for (item in exams) resultTasks.add(Exam(item))
_tasks.postValue(resultTasks)
}
// val result = model.getExams() zipTo model.getPinnedEvents()
val combined = combineLiveData(model.getPinnedEvents(), model.getExams(), _news) {
pinned: List<Event>?, exams: List<Event>?, news: List<News>? ->
val resultTasks = ArrayList<Task>()
if (pinned != null) for (item in pinned) resultTasks.add(PinnedTask(item))
if (exams != null) for (item in exams) resultTasks.add(ExamTask(item))
if (news != null) for (item in news) resultTasks.add(NewsTask(item))
resultTasks as List<Task>
}
withContext(UI) { combined passTo _tasks}
// withContext(UI) {
// result.observeForever { (exams, pinned) ->
// val resultTasks = ArrayList<Task>()
// for (item in pinned) resultTasks.add(PinnedTask(item))
// for (item in exams) resultTasks.add(ExamTask(item))
// _tasks.postValue(resultTasks)
// }
// }
} }
}
\ No newline at end of file
package com.cvut.blackbird.model
object Constants {
const val FACULTY_FEL = "fel"
const val FACULTY_FIT = "fit"
}
\ No newline at end of file
......@@ -82,7 +82,7 @@ data class Note(
)
enum class EventType(cs: String, en: String) {
EXAM("Zkouška","Exam"),
EXAM("Zkouška","ExamTask"),
LECTURE("Přednáška","Lecture"),
TUTORIAL("Cvičení","Tutorial"),
UNDEFINED("Nedefinovaný","Undefined");
......
package com.cvut.blackbird.model.entities
import com.cvut.blackbird.model.Constants
import com.cvut.blackbird.model.services.UserInfo
import com.tickaroo.tikxml.annotation.Element
import com.tickaroo.tikxml.annotation.Path
import com.tickaroo.tikxml.annotation.PropertyElement
import com.tickaroo.tikxml.annotation.Xml
import java.security.InvalidKeyException
@Xml
class NewsRoot(
@Path("channel")
@Element(name = "item")
val news: List<News>
)
) { companion object {
fun getNewsLink(): String =
if (false) {
when (UserInfo.facultyAbbr) {
Constants.FACULTY_FEL -> "http://www.fel.cvut.cz/aktuality/rss.xml"
Constants.FACULTY_FIT -> "https://fit.cvut.cz/rss-novinky.xml"
else -> throw InvalidKeyException("Faculty ${UserInfo.facultyAbbr} is not supported!")
}
} else {
when (UserInfo.facultyAbbr) {
Constants.FACULTY_FEL -> "http://www.fel.cvut.cz/en/aktuality/rss-en.xml"
Constants.FACULTY_FIT -> "https://fit.cvut.cz/rss-news.xml"
else -> throw InvalidKeyException("Faculty ${UserInfo.facultyAbbr} is not supported!")
}
}
} }
@Xml
class News(
@PropertyElement
val title: String,
@PropertyElement(name = "datum")
val date: String,
@PropertyElement
val description: String,
......
......@@ -73,6 +73,8 @@ class AuthModel: BlackBirdModel() {
suspend fun loadUser(): Result<Unit> {
val userInfo = fetch(authService.getUserInfo())
val user = if (userInfo is Success) {
UserInfo.facultyAbbr = userInfo.value.email
.split('.','@')[1]
fetch(kosService.getStudent(userInfo.value.username))
} else Failure((userInfo as? Failure)?.message ?: "Unexpected Error")
......
......@@ -6,6 +6,7 @@ import com.cvut.blackbird.model.database.CourseDao
import com.cvut.blackbird.model.database.EventDao
import com.cvut.blackbird.model.database.TeacherDao
import com.cvut.blackbird.model.services.AuthInfo
import com.cvut.blackbird.model.services.EventsMeta
import com.cvut.blackbird.model.services.UserInfo
import javax.inject.Inject
......@@ -27,5 +28,6 @@ class ProfileModel: BlackBirdModel() {
suspend fun deleteSharedPrefs() {
UserInfo.clear()
AuthInfo.clear()
EventsMeta.clear()
}
}
\ No newline at end of file
......@@ -8,9 +8,15 @@ import com.cvut.blackbird.BlackBirdAC
import com.cvut.blackbird.model.*
import com.cvut.blackbird.model.database.EventDao
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.News
import com.cvut.blackbird.model.entities.NewsRoot
import com.cvut.blackbird.model.services.*
import kotlinx.coroutines.experimental.async
import com.tickaroo.tikxml.TikXml
import kotlinx.coroutines.experimental.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import java.lang.Exception
import java.security.InvalidKeyException
import javax.inject.Inject
class TasksModel: BlackBirdModel() {
......@@ -24,7 +30,7 @@ class TasksModel: BlackBirdModel() {
BlackBirdAC.graph.inject(this)
pinnedPref = EventsMeta.asLiveData(EventsMeta::pinned)
pinnedPref.observeForever { onPinnedChange(it)
Log.d(BlackBirdAC.LOG_TAG, "Pinned changed in size: ${it.size}")}
Log.d(BlackBirdAC.LOG_TAG, "PinnedTask changed in size: ${it.size}")}
}
suspend fun getExams() = eventDao.futureExams()
......@@ -32,11 +38,28 @@ class TasksModel: BlackBirdModel() {
private fun onPinnedChange(set: Set<String>) { launch {
pinnedEvents.postValue(
eventDao.getEvents( set
.map { it.toInt() }
)
)
eventDao.getEvents( set.map { it.toInt() }))
} }
suspend fun refreshEvents() = refreshEvents(siriusService, eventDao)