Refactor timetable module (#160)

This commit is contained in:
Mikołaj Pich 2018-10-06 10:53:34 +02:00 committed by Rafał Borcz
parent f2b7c0e781
commit 5cd8ed88c0
33 changed files with 945 additions and 593 deletions

View File

@ -1,226 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PER_PROJECT_SETTINGS">
<value>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="RIGHT_MARGIN" value="100" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:layout_.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:width</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:height</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</value>
</option>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</component>
</project>

View File

@ -72,10 +72,7 @@ ext.supportVersion = "28.0.0"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation('com.github.wulkanowy:api:07201a4') { implementation('com.github.wulkanowy:api:f54b673') { exclude module: "threetenbp" }
exclude module: "threetenbp"
}
implementation "com.android.support:support-v4:$supportVersion" implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:design:$supportVersion" implementation "com.android.support:design:$supportVersion"

View File

@ -0,0 +1,51 @@
package io.github.wulkanowy.data.repositories.local
import android.arch.persistence.room.Room
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
class TimetableLocalTest {
private lateinit var timetableDb: TimetableLocal
private lateinit var testDb: AppDatabase
@Before
fun createDb() {
testDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(), AppDatabase::class.java).build()
timetableDb = TimetableLocal(testDb.timetableDao())
}
@After
fun closeDb() {
testDb.close()
}
@Test
fun saveAndReadTest() {
timetableDb.saveLessons(listOf(
Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 10)),
Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 14)),
Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 17)) // in next week
))
val exams = timetableDb.getLessons(
Semester(studentId = "1", diaryId = "2", semesterId = "3", diaryName = "", semesterName = 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
).blockingGet()
assertEquals(2, exams.size)
assertEquals(exams[0].date, LocalDate.of(2018, 9, 10))
assertEquals(exams[1].date, LocalDate.of(2018, 9, 14))
}
}

View File

@ -63,4 +63,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideAttendanceDao(database: AppDatabase) = database.attendanceDao() fun provideAttendanceDao(database: AppDatabase) = database.attendanceDao()
@Singleton
@Provides
fun provideTimetableDao(database: AppDatabase) = database.timetableDao()
} }

View File

@ -15,6 +15,7 @@ import javax.inject.Singleton
Student::class, Student::class,
Semester::class, Semester::class,
Exam::class, Exam::class,
Timetable::class,
Attendance::class, Attendance::class,
Grade::class, Grade::class,
GradeSummary::class GradeSummary::class
@ -38,6 +39,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun examsDao(): ExamDao abstract fun examsDao(): ExamDao
abstract fun timetableDao(): TimetableDao
abstract fun attendanceDao(): AttendanceDao abstract fun attendanceDao(): AttendanceDao
abstract fun gradeDao(): GradeDao abstract fun gradeDao(): GradeDao

View File

@ -7,7 +7,7 @@ import java.util.*
class Converters { class Converters {
@TypeConverter @TypeConverter
fun fromTimestamp(value: Long?): LocalDate? = value?.run { fun timestampToDate(value: Long?): LocalDate? = value?.run {
DateTimeUtils.toInstant(Date(value)).atZone(ZoneOffset.UTC).toLocalDate() DateTimeUtils.toInstant(Date(value)).atZone(ZoneOffset.UTC).toLocalDate()
} }
@ -15,4 +15,14 @@ class Converters {
fun dateToTimestamp(date: LocalDate?): Long? { fun dateToTimestamp(date: LocalDate?): Long? {
return date?.atStartOfDay()?.toInstant(ZoneOffset.UTC)?.toEpochMilli() return date?.atStartOfDay()?.toInstant(ZoneOffset.UTC)?.toEpochMilli()
} }
@TypeConverter
fun timestampToTime(value: Long?): LocalDateTime? = value?.let {
LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.UTC)
}
@TypeConverter
fun timeToTimestamp(date: LocalDateTime?): Long? {
return date?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
}
} }

View File

@ -0,0 +1,22 @@
package io.github.wulkanowy.data.db.dao
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
import io.github.wulkanowy.data.db.entities.Timetable
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
@Dao
interface TimetableDao {
@Insert
fun insertAll(exams: List<Timetable>): List<Long>
@Delete
fun deleteAll(exams: List<Timetable>)
@Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun getTimetable(diaryId: String, studentId: String, from: LocalDate, end: LocalDate): Maybe<List<Timetable>>
}

View File

@ -0,0 +1,43 @@
package io.github.wulkanowy.data.db.entities
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import java.io.Serializable
@Entity(tableName = "Timetable")
data class Timetable(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id")
var studentId: String = "",
@ColumnInfo(name = "diary_id")
var diaryId: String = "",
val number: Int = 0,
val start: LocalDateTime = LocalDateTime.now(),
val end: LocalDateTime = LocalDateTime.now(),
val date: LocalDate,
val subject: String = "",
val group: String = "",
val room: String = "",
val teacher: String = "",
val info: String = "",
val changes: Boolean = false,
val canceled: Boolean = false
) : Serializable

View File

@ -13,7 +13,9 @@ import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.TemporalAdjusters import org.threeten.bp.temporal.TemporalAdjusters
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AttendanceRepository @Inject constructor( class AttendanceRepository @Inject constructor(
private val settings: InternetObservingSettings, private val settings: InternetObservingSettings,
private val local: AttendanceLocal, private val local: AttendanceLocal,

View File

@ -0,0 +1,45 @@
package io.github.wulkanowy.data.repositories
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.local.TimetableLocal
import io.github.wulkanowy.data.repositories.remote.TimetableRemote
import io.github.wulkanowy.utils.weekFirstDayAlwaysCurrent
import io.reactivex.Single
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.TemporalAdjusters
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TimetableRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: TimetableLocal,
private val remote: TimetableRemote
) {
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Timetable>> {
val start = startDate.weekFirstDayAlwaysCurrent
val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY))
return local.getLessons(semester, start, end).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getLessons(semester, start, end)
else Single.error(UnknownHostException())
}.flatMap { newLessons ->
local.getLessons(semester, start, end).toSingle(emptyList()).map { lessons ->
local.deleteLessons(lessons - newLessons)
local.saveLessons(newLessons - lessons)
newLessons
}
}).map { list ->
list.asSequence().filter {
it.date in startDate..endDate
}.toList()
}
}
}

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.data.repositories.local
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import javax.inject.Inject
class TimetableLocal @Inject constructor(private val timetableDb: TimetableDao) {
fun getLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Timetable>> {
return timetableDb.getTimetable(semester.diaryId, semester.studentId, startDate, endDate)
.filter { !it.isEmpty() }
}
fun saveLessons(lessons: List<Timetable>) {
timetableDb.insertAll(lessons)
}
fun deleteLessons(exams: List<Timetable>) {
timetableDb.deleteAll(exams)
}
}

View File

@ -0,0 +1,40 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.utils.toLocalDate
import io.github.wulkanowy.utils.toLocalDateTime
import io.reactivex.Single
import org.threeten.bp.LocalDate
import javax.inject.Inject
class TimetableRemote @Inject constructor(private val api: Api) {
fun getLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Single<List<Timetable>> {
return Single.just(api.run {
if (diaryId != semester.diaryId) {
diaryId = semester.diaryId
notifyDataChanged()
}
}).flatMap { api.getTimetable(startDate, endDate) }.map { lessons ->
lessons.map {
Timetable(
studentId = semester.studentId,
diaryId = semester.diaryId,
number = it.number,
start = it.start.toLocalDateTime(),
end = it.end.toLocalDateTime(),
date = it.date.toLocalDate(),
subject = it.subject,
group = it.group,
room = it.room,
teacher = it.teacher,
info = it.info,
changes = it.changes,
canceled = it.canceled
)
}
}
}
}

View File

@ -101,11 +101,6 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
presenter.loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
presenter.detachView() presenter.detachView()

View File

@ -40,7 +40,7 @@ class AttendancePresenter @Inject constructor(
disposable.add(sessionRepository.getSemesters() disposable.add(sessionRepository.getSemesters()
.map { selectSemester(it, -1) } .map { selectSemester(it, -1) }
.flatMap { attendanceRepository.getAttendance(it, currentDate, currentDate, forceRefresh) } .flatMap { attendanceRepository.getAttendance(it, currentDate, currentDate, forceRefresh) }
.map { createTimetableItems(it) } .map { createAttendanceItems(it) }
.subscribeOn(schedulers.backgroundThread()) .subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread()) .observeOn(schedulers.mainThread())
.doOnSubscribe { .doOnSubscribe {
@ -74,7 +74,7 @@ class AttendancePresenter @Inject constructor(
}) })
} }
private fun createTimetableItems(items: List<Attendance>): List<AttendanceItem> { private fun createAttendanceItems(items: List<Attendance>): List<AttendanceItem> {
return items.map { return items.map {
AttendanceItem().apply { attendance = it } AttendanceItem().apply { attendance = it }
} }

View File

@ -94,11 +94,6 @@ class ExamFragment : BaseFragment(), ExamView {
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
presenter.loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
presenter.detachView() presenter.detachView()

View File

@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.main.exam
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import org.threeten.bp.LocalDate
interface ExamView : BaseView { interface ExamView : BaseView {

View File

@ -14,7 +14,7 @@ class MoreFragment : BaseFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timetable, container, false) return inflater.inflate(R.layout.fragment_attendance, container, false)
} }
} }

View File

@ -0,0 +1,79 @@
package io.github.wulkanowy.ui.main.timetable
import android.annotation.SuppressLint
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_timetable.*
class TimetableDialog : DialogFragment() {
private lateinit var lesson: Timetable
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Timetable): TimetableDialog {
return TimetableDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme)
arguments?.run {
lesson = getSerializable(ARGUMENT_KEY) as Timetable
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(getString(R.string.all_details))
return inflater.inflate(R.layout.dialog_timetable, container, false)
}
@SuppressLint("SetTextI18n")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
timetableDialogSubject.text = lesson.subject
timetableDialogTime.text = "${lesson.start.toFormattedString("HH:mm")} - ${lesson.end.toFormattedString("HH:mm")}"
lesson.group.let {
if (it.isBlank()) {
timetableDialogGroupTitle.visibility = GONE
timetableDialogGroup.visibility = GONE
} else timetableDialogGroup.text = it
}
lesson.room.let {
if (it.isBlank()) {
timetableDialogRoomTitle.visibility = GONE
timetableDialogRoom.visibility = GONE
} else timetableDialogRoom.text = it
}
lesson.teacher.let {
if (it.isBlank()) {
timetableDialogTeacherTitle.visibility = GONE
timetableDialogTeacher.visibility = GONE
} else timetableDialogTeacher.text = it
}
lesson.info.let {
if (it.isBlank()) {
timetableDialogChangesTitle.visibility = GONE
timetableDialogChanges.visibility = GONE
} else timetableDialogChanges.text = it
}
timetableDialogClose.setOnClickListener { dismiss() }
}
}

View File

@ -4,17 +4,105 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_timetable.*
import javax.inject.Inject
class TimetableFragment : BaseFragment() { class TimetableFragment : BaseFragment(), TimetableView {
@Inject
lateinit var presenter: TimetablePresenter
@Inject
lateinit var timetableAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
companion object { companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = TimetableFragment() fun newInstance() = TimetableFragment()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timetable, container, false) return inflater.inflate(R.layout.fragment_timetable, container, false)
} }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this@TimetableFragment)
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
}
override fun initView() {
timetableAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnItemClickListener { presenter.onTimetableItemSelected(getItem(it))}
}
timetableRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = timetableAdapter
}
timetableSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) }
timetablePreviousButton.setOnClickListener { presenter.loadTimetableForPreviousDay() }
timetableNextButton.setOnClickListener { presenter.loadTimetableForNextDay() }
}
override fun updateData(data: List<TimetableItem>) {
timetableAdapter.updateDataSet(data, true)
}
override fun clearData() {
timetableAdapter.clear()
}
override fun updateNavigationDay(date: String) {
timetableNavDate.text = date
}
override fun isViewEmpty() = timetableAdapter.isEmpty
override fun showEmpty(show: Boolean) {
timetableEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showProgress(show: Boolean) {
timetableProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
timetableRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showRefresh(show: Boolean) {
timetableSwipe.isRefreshing = show
}
override fun showPreButton(show: Boolean) {
timetablePreviousButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
}
override fun showNextButton(show: Boolean) {
timetableNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
}
override fun showTimetableDialog(lesson: Timetable) {
TimetableDialog.newInstance(lesson).show(fragmentManager, lesson.toString())
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
}
override fun onDestroyView() {
super.onDestroyView()
presenter.detachView()
}
}

View File

@ -0,0 +1,65 @@
package io.github.wulkanowy.ui.main.timetable
import android.annotation.SuppressLint
import android.graphics.Paint
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_timetable.*
class TimetableItem : AbstractFlexibleItem<TimetableItem.ViewHolder>() {
lateinit var lesson: Timetable
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun getLayoutRes(): Int = R.layout.item_timetable
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TimetableItem
if (lesson != other.lesson) return false
return true
}
override fun hashCode(): Int {
return lesson.hashCode()
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.bind(lesson)
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View
get() = contentView
@SuppressLint("SetTextI18n")
fun bind(lesson: Timetable) {
timetableItemNumber.text = lesson.number.toString()
timetableItemSubject.text = lesson.subject
timetableItemRoom.text = if (lesson.room.isNotBlank()) "${view.context.getString(R.string.timetable_room)} ${lesson.room}" else ""
timetableItemTime.text = "${lesson.start.toFormattedString("HH:mm")} - ${lesson.end.toFormattedString("HH:mm")}"
timetableItemAlert.visibility = if (lesson.changes || lesson.canceled) VISIBLE else GONE
timetableItemSubject.paintFlags =
if (lesson.canceled) timetableItemSubject.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
else timetableItemSubject.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
}

View File

@ -0,0 +1,93 @@
package io.github.wulkanowy.ui.main.timetable
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.schedulers.SchedulersManager
import org.threeten.bp.LocalDate
import javax.inject.Inject
class TimetablePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersManager,
private val timetableRepository: TimetableRepository,
private val sessionRepository: SessionRepository
) : BasePresenter<TimetableView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().nearSchoolDayNextOnWeekEnd
private set
override fun attachView(view: TimetableView) {
super.attachView(view)
view.initView()
}
fun loadTimetableForPreviousDay() = loadData(currentDate.previousWorkDay.toEpochDay())
fun loadTimetableForNextDay() = loadData(currentDate.nextWorkDay.toEpochDay())
fun loadData(date: Long?, forceRefresh: Boolean = false) {
this.currentDate = LocalDate.ofEpochDay(date ?: currentDate.nearSchoolDayNextOnWeekEnd.toEpochDay())
if (currentDate.isHolidays) return
disposable.clear()
disposable.add(sessionRepository.getSemesters()
.map { selectSemester(it, -1) }
.flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) }
.map { createTimetableItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doOnSubscribe {
view?.run {
showRefresh(forceRefresh)
showProgress(!forceRefresh)
if (!forceRefresh) clearData()
showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE \n dd.MM.YYYY").capitalize())
}
}
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
}
}
.subscribe({
view?.run {
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
updateData(it)
}
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
})
}
private fun createTimetableItems(items: List<Timetable>): List<TimetableItem> {
return items.map {
TimetableItem().apply { lesson = it }
}
}
fun onTimetableItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is TimetableItem) view?.showTimetableDialog(item.lesson)
}
private fun selectSemester(semesters: List<Semester>, index: Int): Semester {
return semesters.single { it.current }.let { currentSemester ->
if (index == -1) currentSemester
else semesters.single { semester ->
semester.run {
semesterName - 1 == index && diaryId == currentSemester.diaryId
}
}
}
}
}

View File

@ -0,0 +1,30 @@
package io.github.wulkanowy.ui.main.timetable
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.ui.base.BaseView
interface TimetableView : BaseView {
fun initView()
fun updateData(data: List<TimetableItem>)
fun updateNavigationDay(date: String)
fun showEmpty(show: Boolean)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun showRefresh(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)
fun showTimetableDialog(lesson: Timetable)
fun isViewEmpty(): Boolean
fun clearData()
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.utils
import org.threeten.bp.DayOfWeek.* import org.threeten.bp.DayOfWeek.*
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.DateTimeFormatter.ofPattern import org.threeten.bp.format.DateTimeFormatter.ofPattern
import org.threeten.bp.temporal.TemporalAdjusters import org.threeten.bp.temporal.TemporalAdjusters
@ -15,13 +16,16 @@ fun Date.toLocalDate(): LocalDate {
return LocalDate.parse(SimpleDateFormat(DATE_PATTERN, Locale.getDefault()).format(this)) return LocalDate.parse(SimpleDateFormat(DATE_PATTERN, Locale.getDefault()).format(this))
} }
fun Date.toLocalDateTime(): LocalDateTime = LocalDateTime.parse(SimpleDateFormat("yyyy-MM-dd HH:mm:ss",
Locale.getDefault()).format(this), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate { fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate {
return LocalDate.parse(this, DateTimeFormatter.ofPattern(format)) return LocalDate.parse(this, DateTimeFormatter.ofPattern(format))
} }
fun LocalDate.toFormattedString(format: String): String = this.format(ofPattern(format)) fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = this.format(ofPattern(format))
fun LocalDate.toFormattedString(): String = this.toFormattedString(DATE_PATTERN) fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = this.format(DateTimeFormatter.ofPattern(format))
inline val LocalDate.nextWorkDay: LocalDate inline val LocalDate.nextWorkDay: LocalDate
get() { get() {

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -8,194 +7,122 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="300dp" android:minWidth="300dp"
android:orientation="vertical"> android:orientation="vertical"
android:padding="20dp">
<RelativeLayout <TextView
android:id="@+id/timetable_dialog_relative_layout" android:id="@+id/timetableDialogChangesTitle"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="20dp" android:text="@string/timetable_changes"
android:paddingEnd="20dp" android:textColor="@color/colorPrimary"
android:paddingLeft="20dp" android:textSize="17sp" />
android:paddingRight="20dp"
android:paddingStart="20dp"
android:paddingTop="10dp"
tools:ignore="UselessParent">
<TextView <TextView
android:id="@+id/timetable_dialog_details" android:id="@+id/timetableDialogChanges"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="3dp"
android:layout_alignParentStart="true" android:text="@string/all_no_data"
android:layout_alignParentTop="true" android:textColor="@color/colorPrimary"
android:layout_gravity="start" android:textIsSelectable="true"
android:gravity="center_vertical" android:textSize="12sp" />
android:maxLines="5"
android:minHeight="60dp"
android:minLines="2"
android:paddingTop="10dp"
android:text="@string/all_details"
android:textIsSelectable="true"
android:textSize="20sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_description" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_marginTop="10dp"
android:layout_alignParentLeft="true" android:text="@string/timetable_lesson"
android:layout_alignParentStart="true" android:textIsSelectable="true"
android:layout_below="@+id/timetable_dialog_details" android:textSize="17sp" />
android:layout_marginTop="10dp"
android:text="@string/timetable_changes"
android:textColor="@color/colorPrimary"
android:textSize="17sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_description_value" android:id="@+id/timetableDialogSubject"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="3dp"
android:layout_alignParentStart="true" android:text="@string/all_no_data"
android:layout_below="@+id/timetable_dialog_description" android:textIsSelectable="true"
android:layout_marginTop="3dp" android:textSize="12sp" />
android:text="@string/all_no_data"
android:textColor="@color/colorPrimary"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_lesson" android:id="@+id/timetableDialogTeacherTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="10dp"
android:layout_alignParentStart="true" android:text="@string/all_teacher"
android:layout_below="@+id/timetable_dialog_description_value" android:textSize="17sp" />
android:layout_marginTop="10dp"
android:text="@string/timetable_lesson"
android:textIsSelectable="true"
android:textSize="17sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_lesson_value" android:id="@+id/timetableDialogTeacher"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="3dp"
android:layout_alignParentStart="true" android:text="@string/all_no_data"
android:layout_below="@+id/timetable_dialog_lesson" android:textIsSelectable="true"
android:layout_marginTop="3dp" android:textSize="12sp" />
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_teacher" android:id="@+id/timetableDialogGroupTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="10dp"
android:layout_alignParentStart="true" android:text="@string/timetable_group"
android:layout_below="@+id/timetable_dialog_lesson_value" android:textSize="17sp" />
android:layout_marginTop="10dp"
android:text="@string/all_teacher"
android:textSize="17sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_teacher_value" android:id="@+id/timetableDialogGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="3dp"
android:layout_alignParentStart="true" android:text="@string/all_no_data"
android:layout_below="@+id/timetable_dialog_teacher" android:textIsSelectable="true"
android:layout_marginTop="3dp" android:textSize="12sp" />
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_group" android:id="@+id/timetableDialogRoomTitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="10dp"
android:layout_alignParentStart="true" android:text="@string/timetable_room"
android:layout_below="@+id/timetable_dialog_teacher_value" android:textSize="17sp" />
android:layout_marginTop="10dp"
android:text="@string/timetable_group"
android:textSize="17sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_group_value" android:id="@+id/timetableDialogRoom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="3dp"
android:layout_alignParentStart="true" android:text="@string/all_no_data"
android:layout_below="@+id/timetable_dialog_group" android:textIsSelectable="true"
android:layout_marginTop="3dp" android:textSize="12sp" />
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_room" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_marginTop="10dp"
android:layout_alignParentLeft="true" android:text="@string/timetable_time"
android:layout_alignParentStart="true" android:textSize="17sp" />
android:layout_below="@+id/timetable_dialog_group_value"
android:layout_marginTop="10dp"
android:text="@string/timetable_room"
android:textSize="17sp" />
<TextView <TextView
android:id="@+id/timetable_dialog_room_value" android:id="@+id/timetableDialogTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_marginTop="3dp"
android:layout_alignParentStart="true" android:text="@string/all_no_data"
android:layout_below="@id/timetable_dialog_room" android:textIsSelectable="true"
android:layout_marginTop="3dp" android:textSize="12sp" />
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView <Button
android:id="@+id/timetable_dialog_time" android:id="@+id/timetableDialogClose"
android:layout_width="wrap_content" style="@style/Widget.AppCompat.Button.Borderless"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:layout_alignParentLeft="true" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_gravity="end"
android:layout_below="@+id/timetable_dialog_room_value" android:layout_marginTop="15dp"
android:layout_marginTop="10dp" android:padding="0dp"
android:text="@string/timetable_time" android:text="@string/all_close"
android:textSize="17sp" /> android:textAllCaps="true"
android:textSize="15sp" />
<TextView
android:id="@+id/timetable_dialog_time_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/timetable_dialog_time"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<Button
android:id="@+id/timetable_dialog_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignTop="@+id/timetable_dialog_time_value"
android:layout_marginTop="25dp"
android:background="?attr/selectableItemBackground"
android:focusable="true"
android:text="@string/all_close"
android:textColor="?android:attr/android:textColorSecondary"
android:textAllCaps="true"
android:textSize="15sp" />
</RelativeLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@ -1,12 +1,99 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/timetable_fragment_container" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignParentBottom="true"> android:orientation="vertical">
<TextView <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:gravity="center" android:layout_weight="1"
android:text="Timetable" /> android:orientation="vertical">
</FrameLayout>
<ProgressBar
android:id="@+id/timetableProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/timetableSwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/timetableRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/timetableEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone">
<android.support.v7.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:minWidth="100dp"
app:srcCompat="@drawable/ic_menu_main_timetable_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/timetable_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="horizontal">
<Button
android:id="@+id/timetablePreviousButton"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="start|center"
android:text="@string/prev"
android:textAlignment="gravity" />
<TextView
android:id="@+id/timetableNavDate"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/app_name" />
<Button
android:id="@+id/timetableNextButton"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="end|center"
android:text="@string/next"
android:textAlignment="gravity" />
</LinearLayout>
</LinearLayout>

View File

@ -1,73 +0,0 @@
<android.support.design.widget.CoordinatorLayout 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"
android:id="@+id/timetable_tab_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.github.wulkanowy.ui.main.grades.GradesFragment">
<RelativeLayout
android:id="@+id/timetable_tab_fragment_progress_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/timetable_tab_fragment_no_item_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/timetable_tab_fragment_no_item_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/timetable_tab_fragment_no_item_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="40dp"
android:minHeight="100dp"
android:minWidth="100dp"
app:srcCompat="@drawable/ic_menu_main_timetable_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/timetable_tab_fragment_no_item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="46dp"
android:gravity="center"
android:text="@string/timetable_no_items"
android:textSize="20sp" />
<TextView
android:id="@+id/timetable_tab_fragment_no_item_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/timetable_tab_fragment_no_item_image"
android:layout_marginTop="15dp"
android:gravity="center_horizontal"
android:text="@string/app_name"
android:textSize="20sp" />
</RelativeLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/timetable_tab_fragment_swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/timetable_tab_fragment_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -1,94 +1,82 @@
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tool="http://schemas.android.com/tools" xmlns:tool="http://schemas.android.com/tools"
android:id="@+id/timetable_subItem_cardView" android:id="@+id/timetable_subitem_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="7dp"
android:layout_marginEnd="5dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginStart="5dp"
android:foreground="?attr/selectableItemBackgroundBorderless" android:foreground="?attr/selectableItemBackgroundBorderless"
card_view:cardElevation="0dp"> android:paddingBottom="7dp"
android:paddingEnd="12dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingStart="12dp"
android:paddingTop="7dp">
<RelativeLayout <TextView
android:layout_width="match_parent" android:id="@+id/timetableItemNumber"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:gravity="center"
android:maxLength="2"
android:text="0"
android:textSize="32sp"
tool:ignore="all" />
<TextView
android:id="@+id/timetableItemSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="7dp" android:layout_alignParentTop="true"
android:layout_marginEnd="7dp" android:layout_marginEnd="40dp"
android:layout_marginLeft="7dp" android:layout_marginLeft="10dp"
android:layout_marginRight="7dp" android:layout_marginRight="40dp"
android:layout_marginStart="7dp" android:layout_marginStart="10dp"
android:layout_marginTop="7dp"> android:layout_toEndOf="@+id/timetableItemNumber"
android:layout_toRightOf="@+id/timetableItemNumber"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/app_name"
android:textSize="17sp"
tool:ignore="RelativeOverlap" />
<TextView <TextView
android:id="@+id/timetable_subItem_number_of_lesson" android:id="@+id/timetableItemTime"
android:layout_width="40dp" android:layout_width="wrap_content"
android:layout_height="40dp" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_alignBottom="@+id/timetableItemNumber"
android:gravity="center" android:layout_alignLeft="@id/timetableItemSubject"
android:maxLength="2" android:layout_alignStart="@id/timetableItemSubject"
android:text="0" android:maxLines="1"
android:textSize="32sp" android:text="@string/app_name"
tool:ignore="all"/> android:textColor="?android:attr/android:textColorSecondary"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/timetable_subItem_lesson" android:id="@+id/timetableItemRoom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true" android:layout_alignBottom="@+id/timetableItemNumber"
android:layout_marginEnd="40dp" android:layout_marginEnd="40dp"
android:layout_marginLeft="10dp" android:layout_marginLeft="10dp"
android:layout_marginRight="40dp" android:layout_marginRight="40dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_toEndOf="@+id/timetable_subItem_number_of_lesson" android:layout_toEndOf="@+id/timetableItemTime"
android:layout_toRightOf="@+id/timetable_subItem_number_of_lesson" android:layout_toRightOf="@+id/timetableItemTime"
android:ellipsize="end" android:maxLines="1"
android:maxLines="1" android:text="@string/app_name"
android:text="@string/app_name" android:textColor="?android:attr/android:textColorSecondary"
android:textSize="17sp" android:textSize="12sp"
tool:ignore="all"/> tool:ignore="RelativeOverlap" />
<TextView <ImageView
android:id="@+id/timetable_subItem_time" android:id="@+id/timetableItemAlert"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBottom="@+id/timetable_subItem_number_of_lesson" android:layout_alignParentEnd="true"
android:layout_alignLeft="@id/timetable_subItem_lesson" android:layout_alignParentRight="true"
android:layout_alignStart="@id/timetable_subItem_lesson" android:layout_marginTop="10dp"
android:maxLines="1" app:srcCompat="@drawable/ic_timetable_swap_30dp"
android:text="@string/app_name" tool:ignore="contentDescription" />
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="12sp" />
<TextView </RelativeLayout>
android:id="@+id/timetable_subItem_room"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/timetable_subItem_number_of_lesson"
android:layout_marginEnd="40dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="40dp"
android:layout_marginStart="10dp"
android:layout_toEndOf="@+id/timetable_subItem_time"
android:layout_toRightOf="@+id/timetable_subItem_time"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="12sp"
tool:ignore="all"/>
<ImageView
android:id="@+id/timetable_subItem_alert_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginTop="10dp"
app:srcCompat="@drawable/ic_timetable_swap_30dp"
tool:ignore="contentDescription"/>
</RelativeLayout>
</android.support.v7.widget.CardView>

View File

@ -63,11 +63,11 @@
<!--Timetable--> <!--Timetable-->
<string name="timetable_lesson">Lekcja</string> <string name="timetable_lesson">Lekcja</string>
<string name="timetable_room">Sala %s</string> <string name="timetable_room">Sala</string>
<string name="timetable_group">Grupa</string> <string name="timetable_group">Grupa</string>
<string name="timetable_time">Godziny</string> <string name="timetable_time">Godziny</string>
<string name="timetable_changes">Zmiany</string> <string name="timetable_changes">Zmiany</string>
<string name="timetable_no_items">Brak lekcji w tym tygodniu</string> <string name="timetable_no_items">Brak lekcji w tym dniu</string>
<!--Attendance--> <!--Attendance-->

View File

@ -63,7 +63,7 @@
<string name="timetable_group">Group</string> <string name="timetable_group">Group</string>
<string name="timetable_time">Hours</string> <string name="timetable_time">Hours</string>
<string name="timetable_changes">Changes</string> <string name="timetable_changes">Changes</string>
<string name="timetable_no_items">No lesson in this week</string> <string name="timetable_no_items">No lesson in this day</string>
<!--Attendance--> <!--Attendance-->

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.api.timetable.Timetable
import io.github.wulkanowy.data.db.entities.Semester
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.reactivex.Single
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import java.sql.Date
class TimetableRemoteTest {
@MockK
private lateinit var mockApi: Api
@MockK
private lateinit var semesterMock: Semester
@Before
fun initApi() {
MockKAnnotations.init(this)
}
@Test
fun getExamsTest() {
every { mockApi.getTimetable(
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 15)
) } returns Single.just(listOf(
getTimetable("2018-09-10"),
getTimetable("2018-09-17")
))
every { mockApi.diaryId } returns "1"
every { semesterMock.studentId } returns "1"
every { semesterMock.diaryId } returns "1"
val timetable = TimetableRemote(mockApi).getLessons(semesterMock,
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 15)
).blockingGet()
assertEquals(2, timetable.size)
}
private fun getTimetable(dateString: String): Timetable {
return Timetable(date = Date.valueOf(dateString))
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.utils
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import java.util.* import java.util.*
class TimeExtensionTest { class TimeExtensionTest {
@ -18,6 +19,12 @@ class TimeExtensionTest {
assertEquals("2018-10.01", LocalDate.of(2018, 10, 1).toFormattedString("yyyy-MM.dd")) assertEquals("2018-10.01", LocalDate.of(2018, 10, 1).toFormattedString("yyyy-MM.dd"))
} }
@Test
fun toFormat_LocalDateTime() {
assertEquals("2018-10-01", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString())
assertEquals("2018-10-01 10:00:00", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString("uuuu-MM-dd HH:mm:ss"))
}
@Test @Test
fun weekFirstDayAlwaysCurrentTest() { fun weekFirstDayAlwaysCurrentTest() {
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 2).weekFirstDayAlwaysCurrent) assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 2).weekFirstDayAlwaysCurrent)

View File

@ -10,7 +10,7 @@ buildscript {
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:3.2.0' classpath 'com.android.tools.build:gradle:3.2.0'
classpath "io.fabric.tools:gradle:1.25.4" classpath "io.fabric.tools:gradle:1.26.0"
classpath "com.google.gms:oss-licenses:0.9.2" classpath "com.google.gms:oss-licenses:0.9.2"
classpath "com.github.triplet.gradle:play-publisher:1.2.2" classpath "com.github.triplet.gradle:play-publisher:1.2.2"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2"

View File

@ -1,4 +1,3 @@
#Mon Sep 24 22:45:58 CEST 2018
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME