Commit 8f53eac2 authored by Filip Wiesner's avatar Filip Wiesner

Pinned events in Tasks, (Pinned layout is WIP)

parent 0e07d401
......@@ -25,21 +25,24 @@ android {
kapt {
generateStubs = true
mapDiagnosticLocations = true
arguments {
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
dependencies {
def anko_version = "0.10.5"
def room_version = "2.0.0-rc01"
def dagger_version = "2.16"
def nav_version = "1.0.0-alpha05"
def nav_version = "1.0.0-alpha06"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.0-rc02'
implementation 'com.google.android.material:material:1.0.0-rc02'
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0-rc01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0-rc02'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:4.12'
// Testing
......@@ -48,8 +51,10 @@ dependencies {
//Tools
implementation "org.jetbrains.anko:anko-commons:$anko_version"
implementation 'androidx.core:core-ktx:1.0.0-rc02'
implementation 'androidx.core:core-ktx:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.25.3'
implementation "org.jetbrains.kotlin:kotlin-reflect:1.2.70"
implementation 'com.github.magneticflux:kotlin-livedata-utils:0.3.3'
//Navigation
implementation "android.arch.navigation:navigation-fragment-ktx:$nav_version"
......@@ -68,7 +73,8 @@ dependencies {
implementation 'com.tickaroo.tikxml:retrofit-converter:0.8.13'
// Shared Prefs
implementation 'com.chibatching.kotpref:kotpref:2.5.0'
implementation 'com.chibatching.kotpref:kotpref:2.6.0'
implementation 'com.chibatching.kotpref:livedata-support:2.6.0'
// Parsing
implementation 'com.google.code.gson:gson:2.8.4'
......@@ -80,7 +86,7 @@ dependencies {
// Animation/Trasnition
implementation 'bg.devlabs.transitioner:transitioner:1.3'
implementation 'com.github.florent37:kotlinpleaseanimate:1.0.4'
implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0-rc02'
implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
// Database
implementation "androidx.room:room-runtime:$room_version"
......
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "5281afc96c4c67e7c26079ad972be723",
"entities": [
{
"tableName": "Event",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `sequenceNum` INTEGER NOT NULL, `startsAt` INTEGER NOT NULL, `endsAt` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `capacity` INTEGER NOT NULL, `occupied` INTEGER NOT NULL, `eventTypeRaw` TEXT NOT NULL, `parallel` TEXT NOT NULL, `cs` TEXT, `en` TEXT, `course` TEXT, `room` TEXT, `teachers` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequenceNum",
"columnName": "sequenceNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startsAt",
"columnName": "startsAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endsAt",
"columnName": "endsAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deleted",
"columnName": "deleted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "capacity",
"columnName": "capacity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "occupied",
"columnName": "occupied",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "eventTypeRaw",
"columnName": "eventTypeRaw",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parallel",
"columnName": "parallel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "note.cs",
"columnName": "cs",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "note.en",
"columnName": "en",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "linked.course",
"columnName": "course",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "linked.room",
"columnName": "room",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "linked.teachers",
"columnName": "teachers",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_Event_eventTypeRaw",
"unique": false,
"columnNames": [
"eventTypeRaw"
],
"createSql": "CREATE INDEX `index_Event_eventTypeRaw` ON `${TABLE_NAME}` (`eventTypeRaw`)"
}
],
"foreignKeys": []
},
{
"tableName": "Course",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`code` TEXT NOT NULL, `completion` TEXT NOT NULL, `credits` INTEGER NOT NULL, `department` TEXT NOT NULL, `homepage` TEXT, `name` TEXT NOT NULL, `range` TEXT NOT NULL, `season` TEXT NOT NULL, `state` TEXT NOT NULL, `studyForm` TEXT, PRIMARY KEY(`code`))",
"fields": [
{
"fieldPath": "code",
"columnName": "code",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "completion",
"columnName": "completion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "credits",
"columnName": "credits",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "department",
"columnName": "department",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "homepage",
"columnName": "homepage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "range",
"columnName": "range",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "season",
"columnName": "season",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "studyForm",
"columnName": "studyForm",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"code"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Teacher",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `personalNumber` INTEGER NOT NULL, `titlesPost` TEXT, `titlesPre` TEXT, `username` TEXT NOT NULL, `division` TEXT NOT NULL, `email` TEXT NOT NULL, `extern` INTEGER NOT NULL, `phone` TEXT, PRIMARY KEY(`username`))",
"fields": [
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "personalNumber",
"columnName": "personalNumber",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "titlesPost",
"columnName": "titlesPost",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "titlesPre",
"columnName": "titlesPre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "division",
"columnName": "division",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extern",
"columnName": "extern",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "phone",
"columnName": "phone",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"username"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"5281afc96c4c67e7c26079ad972be723\")"
]
}
}
\ No newline at end of file
package com.cvut.blackbird.extensions
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.MutableLiveData
import com.cvut.blackbird.model.Loading
......@@ -46,6 +47,54 @@ inline infix fun<T> MutableLiveData<Result<T>>.fetchUsing(crossinline job: suspe
postValue(job.invoke())
}
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))
}
fun <T> LiveData<T>.observeOnBackground(onChange: (T?) -> Unit) {
BackgroundLiveData(this, onChange)
}
class BackgroundLiveData<T, R>(source: LiveData<T>, stalker:(T?) -> R) : MediatorLiveData<R>() {
private var data: T? = source.value
init {
super.addSource(source) {
data = it
value = stalker(data)
}
}
}
class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() {
private var data1: T? = source1.value
private var data2: K? = source2.value
init {
super.addSource(source1) {
data1 = it
value = combine(data1, data2)
}
super.addSource(source2) {
data2 = it
value = combine(data1, data2)
}
}
override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) =
throw UnsupportedOperationException()
override fun <S : Any?> removeSource(toRemote: LiveData<S>) =
throw UnsupportedOperationException()
}
/**
* JOBS
*/
......@@ -59,4 +108,4 @@ inline fun doWith(job: Job?, strategy: JobStrategy = JobStrategy.CANCEL, crossin
}
}
return todo()
}
}
\ No newline at end of file
......@@ -3,4 +3,11 @@ package com.cvut.blackbird.extensions
import android.util.Base64
fun String.base64Encoded(): String = Base64.encodeToString(toByteArray(), Base64.NO_WRAP)
fun String.lastIndexOfNumber(): Int = indexOfLast {it.isDigit()} + 1
\ No newline at end of file
fun String.lastIndexOfNumber(): Int = indexOfLast {it.isDigit()} + 1
val String.courseAbbr get() = this.substring(
this.lastIndexOfNumber()
.coerceAtMost(this.length - 3)
.coerceAtLeast(0),
this.length
)
\ No newline at end of file
package com.cvut.blackbird.flows.zupport
package com.cvut.blackbird.flows.tasks
import android.content.res.ColorStateList
import android.view.LayoutInflater
......@@ -83,7 +83,7 @@ class EventListAdapter(private val clickListener: (Event) -> Unit): ListAdapter<
.inflate(layout, parent, false))
}
override fun onBindViewHolder(holder:EventViewHolder, position: Int) {
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
holder.bind(getItem(position), clickListener)
}
}
\ No newline at end of file
package com.cvut.blackbird.flows.tasks
import android.content.res.ColorStateList
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 com.cvut.blackbird.R
import com.cvut.blackbird.extensions.courseAbbr
import com.cvut.blackbird.extensions.lastIndexOfNumber
import com.cvut.blackbird.model.entities.Event
import com.cvut.blackbird.model.entities.EventType
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_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)
class TasksListAdapter(private val clickListener: (Task) -> Unit): ListAdapter<Task, TaskViewHolder>(
object : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Task, newItem: Task) = oldItem == newItem
}
) {
companion object {
const val EXAM = 1
const val PINNED = 2
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is Exam -> EXAM
is Pinned -> PINNED
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
return when (viewType) {
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))
else -> throw Exception("This view type is not implemented")
}
}
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) =
holder.bind(getItem(position), clickListener)
}
sealed class TaskViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
abstract fun bind(task: Task, clickListener: (Task) -> Unit)
}
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")
view.examLengthTxt.text = "${Minutes.minutesBetween(event.startsAt, event.endsAt).minutes} minutes"
view.examOccupiedTxt.text = event.occupied.toString()
view.examCapacityTxt.text = event.capacity.toString()
view.examDueTimeTxt.text = "in ${Days.daysBetween(DateTime.now(),event.startsAt).days} days"
view.examRoomTxt.text = event.linked?.room
if (event.note != null)
view.examInfoTxt.text = buildString {
append(event.note.cs)
if (event.note.en != null) append(event.note.en)
view.examInfoTxt.visibility = View.VISIBLE
}
if(event.linked?.course != null && event.linked.course.length >= 3) {
val course = event.linked.course
view.examCourseAbbr.text = course.substring(course.lastIndexOfNumber(), course.length)
view.examCourseAbbr.backgroundTintList = ColorStateList.valueOf(Kolor
.fromSeed(course.hashCode())
.withBrightness(0.75f)
.withSaturation(0.65f)
.colorInt)
}
view.setOnClickListener{clickListener(task)}
}
}
class PinnedViewHolder(view: View) : 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.text = pinnedEvent.linked?.course?.courseAbbr ?: "-"
view.pin_timeLeftTxt.text = "Due in ${Days.daysBetween(DateTime.now(),pinnedEvent.startsAt).days} days"
view.pin_unpinBtn.setOnClickListener { pinnedEvent.isPinned = !pinnedEvent.isPinned }
view.setOnClickListener{clickListener(task)}
}
}
......@@ -7,12 +7,15 @@ 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.zupport.EventListAdapter
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.support.glue.bind
import com.cvut.blackbird.support.glue.observe
import com.cvut.blackbird.support.glue.toPassValueTo
......@@ -26,7 +29,8 @@ class TasksFragment : Fragment() {
}
private lateinit var viewModel: TasksViewModel
private lateinit var listAdapter: EventListAdapter
private lateinit var eventViewModel: TimetableViewModel
private lateinit var taskAdapter: TasksListAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
......@@ -35,25 +39,41 @@ class TasksFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(TasksViewModel::class.java)
viewModel = ViewModelProviders.of(activity!!).get(TasksViewModel::class.java)
eventViewModel = ViewModelProviders.of(activity!!).get(TimetableViewModel::class.java)
setupUi()
setupBinding()
viewModel.refresh()
}
private fun setupUi() {
listAdapter = EventListAdapter {
activity?.toast("Clicked on ${it.linked?.course}")
taskAdapter = TasksListAdapter {
when (it) {