Commit 45c2148f authored by Filip Wiesner's avatar Filip Wiesner

SuperGlue map, Wobbly setInitialScroll, EventDetail as seperate flow

parent 8f53eac2
......@@ -19,4 +19,5 @@ interface ApplicationComponent {
fun inject(model: TimetableModel)
fun inject(model: SearchModel)
fun inject(model: ProfileModel)
fun inject(model: EventDetailModel)
}
\ No newline at end of file
......@@ -2,11 +2,16 @@ package com.cvut.blackbird.flows
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import com.cvut.blackbird.R
import com.cvut.blackbird.flows.detail.EventDetailViewModel
import com.cvut.blackbird.flows.profile.ProfileViewModel
import com.cvut.blackbird.flows.search.SearchViewModel
import com.cvut.blackbird.flows.tasks.TasksViewModel
import com.cvut.blackbird.flows.timetable.TimetableViewModel
import com.cvut.blackbird.model.services.UserInfo
import kotlinx.android.synthetic.main.black_bird_ac_activity.*
......@@ -18,17 +23,25 @@ class BlackBirdMain : AppCompatActivity() {
setContentView(R.layout.black_bird_ac_activity)
supportActionBar?.hide()
navController = NavHostFragment.findNavController(navHost)
// disableAuthPop()
// setupViewModels()
if (UserInfo.username.isBlank())
navController.navigate(R.id.toAuth)
}
// private fun setupViewModels() {
// ViewModelProviders.of(this).also { provider ->
// provider.get(TasksViewModel::class.java)
// provider.get(TimetableViewModel::class.java)
// provider.get(SearchViewModel::class.java)
// provider.get(ProfileViewModel::class.java)
//
// provider.get(EventDetailViewModel::class.java)
//
// }
// }
override fun onSupportNavigateUp() = findNavController(navHost.id).navigateUp()
private fun disableAuthPop() {
navController.addOnNavigatedListener { _, destination ->
if(destination.id == R.id.authFragment || destination.id == R.id.navigationFragment) supportFragmentManager.popBackStack()
}
}
}
\ No newline at end of file
package com.cvut.blackbird.flows.timetable
package com.cvut.blackbird.flows.detail
import android.content.res.ColorStateList
......@@ -12,24 +12,23 @@ import androidx.lifecycle.ViewModelProviders
import com.cvut.blackbird.R
import com.cvut.blackbird.extensions.courseAbbr
import com.cvut.blackbird.extensions.getColor
import com.cvut.blackbird.extensions.lastIndexOfNumber
import com.cvut.blackbird.model.Success
import com.cvut.blackbird.model.entities.Course
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.EventType
import com.cvut.blackbird.support.glue.bind
import com.cvut.blackbird.support.glue.clickTo
import com.cvut.blackbird.support.glue.observe
import com.cvut.blackbird.model.entities.Teacher
import com.cvut.blackbird.support.glue.*
import kotlinx.android.synthetic.main.fragment_timetable_detail.*
class TimetableDetailFragment : Fragment() {
class EventDetailFragment : Fragment() {
lateinit var event: Event
private lateinit var viewModel: TimetableViewModel
private lateinit var viewModel: EventDetailViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(TimetableViewModel::class.java)
viewModel = ViewModelProviders.of(activity!!).get(EventDetailViewModel::class.java)
setupBinding()
}
......@@ -37,36 +36,20 @@ class TimetableDetailFragment : Fragment() {
= inflater.inflate(R.layout.fragment_timetable_detail, container, false)
private fun setupBinding() {
observe(viewModel.eventDetail) {
event = it
viewModel.requestCourse(event.linked?.course ?: "")
if (event.linked?.teachers != null)
viewModel.requestTeachers(event.linked!!.teachers!!)
setupEvent(it)
}
observe(viewModel.course) {
if (it is Success && it.value != null)
detail_fullName.text = it.value.name
}
observe(viewModel.teacher) {
if (it.isNotEmpty()) {
detail_teachersContent.text = ""
for (teacher in it)
detail_teachersContent.text =
detail_teachersContent.text.toString() +
"${teacher.firstName} ${teacher.lastName}\n"
detail_teachersContent.text = detail_teachersContent.text.dropLast(1)
}
}
bind(viewModel.eventDetail) toPassValueTo ::setupEvent
bind(viewModel.eventDetail) toPassValueTo { viewModel
.requestCourse(event.linked?.course ?: "") }
bind(viewModel.eventDetail) toPassValueTo {if (event.linked?.teachers != null) viewModel
.requestTeachers(event.linked!!.teachers!!)}
bind(detail_pin) clickTo {
event.isPinned = !event.isPinned
refreshPinTint()
}
bind(viewModel.course) toPassValueTo ::setupCourse
bind(viewModel.teacher) toPassValueTo ::setupTeachers
bind(detail_pin) clickToInvoke ::onPinnedStateChange
}
private fun setupEvent(event: Event) {
this.event = event
detail_abbr.background = when(event.eventType) {
EventType.LECTURE -> context?.resources?.getDrawable(R.drawable.ic_timetable_lecture, context?.theme)
EventType.TUTORIAL -> context?.resources?.getDrawable(R.drawable.ic_timetable_tutorial, context?.theme)
......@@ -84,11 +67,33 @@ class TimetableDetailFragment : Fragment() {
detail_roomContent.text = event.linked?.room ?: "None"
detail_noteContent.text = event.note?.cs ?: "None"
if (event.isPinned) detail_pin.imageTintList = ColorStateList.valueOf(getColor(R.color.colorTextLight))
refreshPinTint()
}
private fun setupCourse(course: Course?) {
if (course != null)
detail_fullName.text = course.name
}
private fun setupTeachers(teachers: List<Teacher>) {
if (teachers.isEmpty()) return
detail_teachersContent.text = ""
val teachersText = buildString {
for (teacher in teachers)
append("${teacher.firstName} ${teacher.lastName}\n")
}
detail_teachersContent.text = teachersText.dropLast(1)
}
private fun onPinnedStateChange() = event.apply {
isPinned = !isPinned
refreshPinTint()
}
private fun refreshPinTint() {
detail_pin.imageTintList =
private fun refreshPinTint() = detail_pin.apply {
imageTintList =
if (event.isPinned) ColorStateList.valueOf(getColor(R.color.colorTextLight))
else ColorStateList.valueOf(getColor(R.color.colorTextDark))
}
......
package com.cvut.blackbird.flows.detail
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.cvut.blackbird.extensions.withDefault
import com.cvut.blackbird.model.entities.Course
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.Teacher
import com.cvut.blackbird.model.flows.EventDetailModel
import kotlinx.coroutines.experimental.launch
class EventDetailViewModel: ViewModel() {
private val model = EventDetailModel()
/**
* Observables
*/
private val _course = MutableLiveData<Course?>()
private val _teachers = MutableLiveData<List<Teacher>>() withDefault listOf()
val course: LiveData<Course?> get() = _course
val teacher: LiveData<List<Teacher>> get() = _teachers
/**
* Transition data
*/
val eventDetail = MutableLiveData<Event>() withDefault Event.empty
/**
* INPUT
*/
fun requestCourse(code: String) { launch {
_course.postValue(model.getCourse(code))
} }
fun requestTeachers(usernames: List<String>) { launch {
_teachers.postValue(model.getTeachers(usernames))
} }
}
\ No newline at end of file
......@@ -25,7 +25,7 @@ class ProfileFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(ProfileViewModel::class.java)
viewModel = ViewModelProviders.of(activity!!).get(ProfileViewModel::class.java)
setupUi()
setupBinding()
......
......@@ -29,7 +29,7 @@ class SearchFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(SearchViewModel::class.java)
viewModel = ViewModelProviders.of(activity!!).get(SearchViewModel::class.java)
}
}
package com.cvut.blackbird.flows.tasks
import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
......@@ -11,6 +12,8 @@ import androidx.recyclerview.widget.RecyclerView
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.support.kolor.Kolor
......@@ -48,7 +51,7 @@ class TasksListAdapter(private val clickListener: (Task) -> Unit): ListAdapter<T
EXAM -> ExamViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.exam_list_row, parent, false))
PINNED -> PinnedViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.tasks_pinned, parent, false))
.inflate(R.layout.tasks_pinned, parent, false), parent.context)
else -> throw Exception("This view type is not implemented")
}
}
......@@ -93,13 +96,24 @@ class ExamViewHolder(view: View) : TaskViewHolder(view) {
}
}
class PinnedViewHolder(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")
view.pin_eventAbbr.background = when {
pinnedEvent.eventType == EventType.LECTURE -> context.resources.getDrawable(R.drawable.ic_timetable_lecture, context.theme)
pinnedEvent.eventType == EventType.TUTORIAL -> context.resources.getDrawable(R.drawable.ic_timetable_tutorial, context.theme)
else -> context.resources.getDrawable(R.drawable.ic_timetable_info, context.theme)
}
view.pin_eventAbbr.text = pinnedEvent.linked?.course?.courseAbbr ?: "-"
view.pin_timeLeftTxt.text = "Due in ${Days.daysBetween(DateTime.now(),pinnedEvent.startsAt).days} days"
if (pinnedEvent.note?.cs != null) {
view.pin_description.setVisible()
view.pin_description.text = pinnedEvent.note.cs
}
view.pin_unpinBtn.setOnClickListener { pinnedEvent.isPinned = !pinnedEvent.isPinned }
view.setOnClickListener{clickListener(task)}
......
......@@ -12,6 +12,7 @@ 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
......@@ -29,7 +30,7 @@ class TasksFragment : Fragment() {
}
private lateinit var viewModel: TasksViewModel
private lateinit var eventViewModel: TimetableViewModel
private lateinit var eventViewModel: EventDetailViewModel
private lateinit var taskAdapter: TasksListAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
......@@ -40,11 +41,10 @@ class TasksFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(TasksViewModel::class.java)
eventViewModel = ViewModelProviders.of(activity!!).get(TimetableViewModel::class.java)
eventViewModel = ViewModelProviders.of(activity!!).get(EventDetailViewModel::class.java)
setupUi()
setupBinding()
viewModel.refresh()
}
private fun setupUi() {
......@@ -68,12 +68,6 @@ class TasksFragment : Fragment() {
private fun goToEventDetail(event: Event) {
eventViewModel.eventDetail.postValue(event)
val options = NavOptions.Builder()
.setEnterAnim(R.anim.nav_default_enter_anim)
// .setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
.build()
findMainNav()?.navigate(R.id.timetableDetailFragment,null, options)
findMainNav()?.navigate(R.id.toEventDetail)
}
}
......@@ -28,7 +28,10 @@ class TasksViewModel : ViewModel() {
val exams: LiveData<List<Event>> get() = _exams
val tasks: LiveData<List<Task>> get() = _tasks
init { configTasks() }
init {
refresh()
configTasks()
}
private var refreshJob: Job? = null
fun refresh() {
......
......@@ -6,24 +6,23 @@ import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import com.cvut.blackbird.R
import com.cvut.blackbird.extensions.*
import com.cvut.blackbird.flows.NavigationFragmentDirections
import com.cvut.blackbird.extensions.dpToPx
import com.cvut.blackbird.flows.detail.EventDetailViewModel
import com.cvut.blackbird.flows.findMainNav
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.support.glue.bind
import com.cvut.blackbird.support.glue.clickTo
import com.cvut.blackbird.support.glue.toPassValueTo
import com.cvut.blackbird.support.glue.withMap
import com.cvut.blackbird.support.wobbly.WobblyAdapter
import com.cvut.blackbird.support.wobbly.WobblyElement
import kotlinx.android.synthetic.main.timetable_dots_fragment.*
......@@ -38,6 +37,8 @@ class TimetableFragment : Fragment() {
private var first: Boolean = true
private lateinit var viewModel: TimetableViewModel
private lateinit var eventViewModel: EventDetailViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
......@@ -47,21 +48,31 @@ class TimetableFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(activity!!).get(TimetableViewModel::class.java)
eventViewModel = ViewModelProviders.of(activity!!).get(EventDetailViewModel::class.java)
first = true
wobblyTimetable.adapter = getAdapter()
setupBinding()
}
override fun onPause() {
super.onPause()
viewModel.scrollState.value = Pair(
wobblyTimetable.wobblyScrollX,
wobblyTimetable.wobblyScrollY
)
}
private fun setupBinding() {
this bind dotsLeftArrowBtn clickTo viewModel::previousWeek
this bind dotsRightArrowBtn clickTo viewModel::nextWeek
bind(dotsLeftArrowBtn) clickTo viewModel::previousWeek
bind(dotsRightArrowBtn) clickTo viewModel::nextWeek
this bind viewModel.timetable toPassValueTo ::onWeekChange
this bind viewModel.displayedWeek toPassValueTo { date ->
dotsWeekDateTxt.text = date.monthOfYear().asText + " ${date.withDayOfWeek(1).dayOfMonth} - ${date.withDayOfWeek(5).dayOfMonth}"
}
bind(viewModel.timetable) toPassValueTo ::onWeekChange
bind(viewModel.scrollState) toPassValueTo wobblyTimetable::setScrollInitialState
bind(viewModel.displayedWeek) withMap {it.monthOfYear().asText +
" ${it.withDayOfWeek(1).dayOfMonth} - ${it.withDayOfWeek(5).dayOfMonth}"
} toPassValueTo dotsWeekDateTxt::setText
}
private fun onWeekChange(events: Map<Int,List<Event>>) {
......@@ -96,7 +107,7 @@ class TimetableFragment : Fragment() {
private fun onLectureClick(element: View) {
if (element is TimetableBall) {
viewModel.eventDetail.value = element.payload
eventViewModel.eventDetail.value = element.payload
findMainNav()!!.navigate(R.id.toEventDetail)
}
}
......
......@@ -25,21 +25,15 @@ class TimetableViewModel : ViewModel() {
private val _timetableUpdateRequestResult = MutableLiveData<Result<List<Event>>>() withDefault NotYet()
private val _timetable = MutableLiveData<Map<Int,List<Event>>>() withDefault mapOf()
private val _displayedWeek = MutableLiveData<DateTime>() withDefault DateTime.now()
private val _course = MutableLiveData<Result<Course?>>() withDefault NotYet()
private val _teachers = MutableLiveData<List<Teacher>>() withDefault listOf()
val loadingStatus: LiveData<Boolean> get() = _loadingStatus
val timetableUpdateResult: LiveData<Result<List<Event>>> get() = _timetableUpdateRequestResult
val timetable: LiveData<Map<Int,List<Event>>> get() = _timetable
val displayedWeek: LiveData<DateTime> get() = _displayedWeek
val course: LiveData<Result<Course?>> get() = _course
val teacher: LiveData<List<Teacher>> get() = _teachers
val eventDetail = MutableLiveData<Event>() withDefault Event.empty
val scrollState = MutableLiveData<Pair<Int, Int>>() withDefault Pair(0, 0)
init {
refresh()
if (_displayedWeek.value!!.dayOfWeek > 5) _displayedWeek.apply { value = value?.plusWeeks(1) }
_displayedWeek.observeForever { date ->
requestTimetable(
......@@ -71,14 +65,6 @@ class TimetableViewModel : ViewModel() {
}
}
fun requestCourse(code: String) { launch {
_course.postValue(Success(model.getCourse(code)))
} }
fun requestTeachers(usernames: List<String>) { launch {
_teachers.postValue(model.getTeachers(usernames))
} }
private var refreshJob: Job? = null
fun refresh() {
refreshJob?.cancel()
......
package com.cvut.blackbird.model.flows
import com.cvut.blackbird.BlackBirdAC
import com.cvut.blackbird.model.BlackBirdModel
import com.cvut.blackbird.model.database.CourseDao
import com.cvut.blackbird.model.database.TeacherDao
import com.cvut.blackbird.model.entities.Teacher
import javax.inject.Inject
class EventDetailModel: BlackBirdModel() {
@Inject lateinit var courseDao: CourseDao
@Inject lateinit var teacherDao: TeacherDao
init { BlackBirdAC.graph.inject(this) }
suspend fun getCourse(code: String) = courseDao.getCourse(code)
suspend fun getTeachers(usernames: List<String>) = ArrayList<Teacher>().apply {
for (username in usernames) {
val teacher = teacherDao.getTeacher(username)
if (teacher != null)
add(teacher)
}
}
}
\ No newline at end of file
......@@ -14,22 +14,10 @@ typealias DateSpan = Pair<Long, Long>
class TimetableModel: BlackBirdModel() {
@Inject lateinit var siriusService: SiriusService
@Inject lateinit var eventDao: EventDao
@Inject lateinit var courseDao: CourseDao
@Inject lateinit var teacherDao: TeacherDao
init { BlackBirdAC.graph.inject(this) }
suspend fun getTimetable(span: DateSpan) = eventDao.timeSpan(span.first, span.second)
suspend fun refreshEvents() = refreshEvents(siriusService, eventDao)
suspend fun getCourse(code: String) = courseDao.getCourse(code)
suspend fun getTeachers(usernames: List<String>) = ArrayList<Teacher>().apply {
for (username in usernames) {
val teacher = teacherDao.getTeacher(username)
if (teacher != null)
add(teacher)
}
}
}
\ No newline at end of file
......@@ -5,13 +5,35 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
/**
* Glue structure
*/
class Glue(val life: LifecycleOwner, val actor: View)
/**
* Initializers
*/
infix fun LifecycleOwner.bind(view: View) = Glue(this, view)
/**
* Helper functions
*/
inline fun<T> LifecycleOwner.observe(data: LiveData<T>, crossinline stalker: (p: T) -> Unit) =
data.observe(this, Observer { stalker(it) })
/**
* Glue functionality
*/
//STATE
infix fun Glue.visibilityTo(source: LiveData<Boolean>) {
source.observe(life, Observer { actor.visibility =
if(it) View.VISIBLE
......@@ -22,9 +44,23 @@ infix fun Glue.enabledTo(source: LiveData<Boolean>) {
source.observe(life, Observer { actor.isEnabled = it })
}
//ACTION
infix fun Glue.clickTo(receiver: () -> Unit) = this.apply {
actor.setOnClickListener { receiver.invoke() }
}
inline fun<T> Glue.clickTo(payload: T, crossinline receiver: (T) -> Unit) = this.apply {