1
0

Modules improvements (#164)

This commit is contained in:
Rafał Borcz
2018-10-14 22:16:58 +02:00
committed by Mikołaj Pich
parent bb69d1b643
commit 27f6fc7e04
90 changed files with 881 additions and 1069 deletions

View File

@ -1,6 +1,7 @@
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.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query
@ -13,6 +14,9 @@ interface GradeSummaryDao {
@Insert(onConflict = REPLACE)
fun insertAll(gradesSummary: List<GradeSummary>)
@Delete
fun deleteAll(gradesSummary: List<GradeSummary>)
@Query("SELECT * FROM grades_summary WHERE student_id = :studentId AND semester_id = :semesterId")
fun getGradesSummary(semesterId: String, studentId: String): Maybe<List<GradeSummary>>
}

View File

@ -2,7 +2,6 @@ package io.github.wulkanowy.data.db.dao
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Single
@ -10,7 +9,7 @@ import io.reactivex.Single
@Dao
interface SemesterDao {
@Insert(onConflict = REPLACE)
@Insert
fun insertAll(semester: List<Semester>)
@Query("SELECT * FROM Semesters WHERE student_id = :studentId")

View File

@ -2,7 +2,6 @@ package io.github.wulkanowy.data.db.dao
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe
@ -10,7 +9,7 @@ import io.reactivex.Maybe
@Dao
interface StudentDao {
@Insert(onConflict = REPLACE)
@Insert
fun insert(student: Student): Long
@Query("SELECT * FROM Students WHERE id = :id")

View File

@ -9,9 +9,6 @@ import java.io.Serializable
@Entity(tableName = "Attendance")
data class Attendance(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id")
var studentId: String,
@ -37,4 +34,8 @@ data class Attendance(
var excused: Boolean = false,
var deleted: Boolean = false
) : Serializable
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -9,9 +9,6 @@ import java.io.Serializable
@Entity(tableName = "Exams")
data class Exam(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id")
var studentId: String,
@ -35,4 +32,8 @@ data class Exam(
@ColumnInfo(name = "teacher_symbol")
var teacherSymbol: String
) : Serializable
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -2,16 +2,11 @@ package io.github.wulkanowy.data.db.entities
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey
@Entity(tableName = "Grades_Summary",
indices = [Index(value = ["semester_id", "student_id", "subject"], unique = true)])
@Entity(tableName = "Grades_Summary")
data class GradeSummary(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "semester_id")
var semesterId: String,
@ -23,4 +18,8 @@ data class GradeSummary(
var predictedGrade: String,
var finalGrade: String
)
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -2,11 +2,9 @@ package io.github.wulkanowy.data.db.entities
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey
@Entity(tableName = "Semesters",
indices = [Index(value = ["semester_id", "diary_id", "student_id"], unique = true)])
@Entity(tableName = "Semesters")
data class Semester(
@PrimaryKey(autoGenerate = true)

View File

@ -2,11 +2,9 @@ package io.github.wulkanowy.data.db.entities
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey
@Entity(tableName = "Students",
indices = [Index(value = ["student_id", "student_name"], unique = true)])
@Entity(tableName = "Students")
data class Student(
@PrimaryKey(autoGenerate = true)

View File

@ -10,14 +10,11 @@ import java.io.Serializable
@Entity(tableName = "Timetable")
data class Timetable(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id")
var studentId: String = "",
var studentId: String,
@ColumnInfo(name = "diary_id")
var diaryId: String = "",
var diaryId: String,
val number: Int = 0,
@ -27,17 +24,21 @@ data class Timetable(
val date: LocalDate,
val subject: String = "",
val subject: String,
val group: String = "",
val group: String,
val room: String = "",
val room: String,
val teacher: String = "",
val teacher: String,
val info: String = "",
val info: String,
val changes: Boolean = false,
val canceled: Boolean = false
) : Serializable
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -6,11 +6,10 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.AttendanceLocal
import io.github.wulkanowy.data.repositories.remote.AttendanceRemote
import io.github.wulkanowy.utils.weekFirstDayAlwaysCurrent
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
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
@ -22,24 +21,25 @@ class AttendanceRepository @Inject constructor(
private val remote: AttendanceRemote
) {
fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Attendance>> {
val start = startDate.weekFirstDayAlwaysCurrent
val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY))
return local.getAttendance(semester, start, end).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(semester, start, end)
else Single.error(UnknownHostException())
}.flatMap { newLessons ->
local.getAttendance(semester, start, end).toSingle(emptyList()).map { grades ->
local.deleteAttendance(grades - newLessons)
local.saveAttendance(newLessons - grades)
newLessons
}
}).map { list ->
list.asSequence().filter {
it.date in startDate..endDate
}.toList()
fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean)
: Single<List<Attendance>> {
return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
local.getAttendance(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newAttendance ->
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldAttendance ->
local.deleteAttendance(oldAttendance - newAttendance)
local.saveAttendance(newAttendance - oldAttendance)
}
}.flatMap {
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
}

View File

@ -6,11 +6,10 @@ import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.ExamLocal
import io.github.wulkanowy.data.repositories.remote.ExamRemote
import io.github.wulkanowy.utils.weekFirstDayAlwaysCurrent
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
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
@ -23,23 +22,24 @@ class ExamRepository @Inject constructor(
) {
fun getExams(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Exam>> {
val start = startDate.weekFirstDayAlwaysCurrent
val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY))
return local.getExams(semester, start, end).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getExams(semester, start, end)
else Single.error(UnknownHostException())
}.flatMap { newExams ->
local.getExams(semester, start, end).toSingle(emptyList()).map { grades ->
local.deleteExams(grades - newExams)
local.saveExams(newExams - grades)
newExams
}
}).map { list ->
list.asSequence().filter {
it.date in startDate..endDate
}.toList()
return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
local.getExams(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getExams(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newExams ->
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldExams ->
local.deleteExams(oldExams - newExams)
local.saveExams(newExams - oldExams)
}
}.flatMap {
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
}

View File

@ -24,7 +24,12 @@ class GradeSummaryRepository @Inject constructor(
.flatMap {
if (it) remote.getGradeSummary(semester)
else Single.error(UnknownHostException())
}
).doOnSuccess { local.saveGradesSummary(it) }
}.flatMap { newGradesSummary ->
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { oldGradesSummary ->
local.deleteGradesSummary(oldGradesSummary - newGradesSummary)
local.saveGradesSummary(newGradesSummary - oldGradesSummary)
}
}.flatMap { local.getGradesSummary(semester).toSingle(emptyList()) })
}
}

View File

@ -6,11 +6,10 @@ 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.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
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
@ -22,24 +21,26 @@ class TimetableRepository @Inject constructor(
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()
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false)
: Single<List<Timetable>> {
return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
local.getTimetable(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getTimetable(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newTimetable ->
local.getTimetable(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldTimetable ->
local.deleteTimetable(oldTimetable - newTimetable)
local.saveTimetable(newTimetable - oldTimetable)
}
}.flatMap {
local.getTimetable(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
}

View File

@ -18,4 +18,8 @@ class GradeSummaryLocal @Inject constructor(private val gradeSummaryDb: GradeSum
fun saveGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.insertAll(gradesSummary)
}
fun deleteGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.deleteAll(gradesSummary)
}
}

View File

@ -6,8 +6,8 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.security.Scrambler.decrypt
import io.github.wulkanowy.utils.security.Scrambler.encrypt
import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single

View File

@ -9,16 +9,16 @@ import javax.inject.Inject
class TimetableLocal @Inject constructor(private val timetableDb: TimetableDao) {
fun getLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Timetable>> {
fun getTimetable(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 saveTimetable(timetables: List<Timetable>) {
timetableDb.insertAll(timetables)
}
fun deleteLessons(exams: List<Timetable>) {
timetableDb.deleteAll(exams)
fun deleteTimetable(timetables: List<Timetable>) {
timetableDb.deleteAll(timetables)
}
}

View File

@ -26,7 +26,7 @@ class GradeRemote @Inject constructor(private val api: Api) {
subject = it.subject,
entry = it.entry,
value = it.value,
modifier = it.modifier.toDouble(),
modifier = it.modifier,
comment = it.comment,
color = it.color,
gradeSymbol = it.symbol,

View File

@ -11,7 +11,7 @@ import javax.inject.Inject
class TimetableRemote @Inject constructor(private val api: Api) {
fun getLessons(semester: Semester, startDate: LocalDate, endDate: LocalDate): Single<List<Timetable>> {
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate): Single<List<Timetable>> {
return Single.just(api.run {
if (diaryId != semester.diaryId) {
diaryId = semester.diaryId

View File

@ -1,10 +1,16 @@
package io.github.wulkanowy.ui.base
import android.support.design.widget.Snackbar
import android.support.design.widget.Snackbar.LENGTH_LONG
import android.view.View
import dagger.android.support.DaggerFragment
abstract class BaseFragment : DaggerFragment(), BaseView {
protected var messageContainer: View? = null
override fun showMessage(text: String) {
(activity as BaseActivity?)?.showMessage(text)
if (messageContainer == null) (activity as? BaseActivity)?.showMessage(text)
else messageContainer?.also { Snackbar.make(it, text, LENGTH_LONG).show() }
}
}

View File

@ -9,15 +9,12 @@ open class BasePresenter<T : BaseView>(private val errorHandler: ErrorHandler) {
var view: T? = null
val isViewAttached: Boolean
get() = view != null
open fun attachView(view: T) {
open fun onAttachView(view: T) {
this.view = view
errorHandler.showErrorMessage = { view.showMessage(it) }
}
open fun detachView() {
open fun onDetachView() {
view = null
disposable.clear()
errorHandler.clear()

View File

@ -27,8 +27,9 @@ class LoginActivity : BaseActivity(), LoginView, LoginSwitchListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
presenter.attachView(this)
messageContainer = loginContainer
presenter.onAttachView(this)
}
override fun onBackPressed() {
@ -65,7 +66,7 @@ class LoginActivity : BaseActivity(), LoginView, LoginSwitchListener {
override fun currentViewPosition() = loginViewpager.currentItem
public override fun onDestroy() {
presenter.detachView()
presenter.onDetachView()
super.onDestroy()
}
}

View File

@ -7,8 +7,8 @@ import javax.inject.Inject
class LoginPresenter @Inject constructor(errorHandler: ErrorHandler)
: BasePresenter<LoginView>(errorHandler) {
override fun attachView(view: LoginView) {
super.attachView(view)
override fun onAttachView(view: LoginView) {
super.onAttachView(view)
view.run {
initAdapter()
hideActionBar()

View File

@ -31,7 +31,7 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.attachView(this)
presenter.onAttachView(this)
}
override fun initInputs() {
@ -139,6 +139,6 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
override fun onDestroyView() {
super.onDestroyView()
presenter.detachView()
presenter.onDetachView()
}
}

View File

@ -14,8 +14,8 @@ class LoginFormPresenter @Inject constructor(
private var wasEmpty = false
override fun attachView(view: LoginFormView) {
super.attachView(view)
override fun onAttachView(view: LoginFormView) {
super.onAttachView(view)
view.initInputs()
}

View File

@ -35,7 +35,7 @@ class LoginOptionsFragment : BaseFragment(), LoginOptionsView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.attachView(this)
presenter.onAttachView(this)
}
override fun initRecycler() {
@ -80,6 +80,6 @@ class LoginOptionsFragment : BaseFragment(), LoginOptionsView {
override fun onDestroyView() {
super.onDestroyView()
presenter.detachView()
presenter.onDetachView()
}
}

View File

@ -13,8 +13,8 @@ class LoginOptionsPresenter @Inject constructor(
private val schedulers: SchedulersManager)
: BasePresenter<LoginOptionsView>(errorHandler) {
override fun attachView(view: LoginOptionsView) {
super.attachView(view)
override fun onAttachView(view: LoginOptionsView) {
super.onAttachView(view)
view.initRecycler()
}

View File

@ -39,7 +39,7 @@ class MainActivity : BaseActivity(), MainView {
setSupportActionBar(mainToolbar)
messageContainer = mainFragmentContainer
presenter.attachView(this)
presenter.onAttachView(this)
navController.initialize(DEFAULT_TAB, savedInstanceState)
}
@ -90,10 +90,6 @@ class MainActivity : BaseActivity(), MainView {
supportActionBar?.title = title
}
override fun expandActionBar(show: Boolean) {
mainAppBarContainer.setExpanded(show, true)
}
override fun viewTitle(index: Int): String {
return getString(listOf(R.string.grade_title,
R.string.attendance_title,
@ -119,6 +115,6 @@ class MainActivity : BaseActivity(), MainView {
override fun onDestroy() {
super.onDestroy()
presenter.detachView()
presenter.onDetachView()
}
}

View File

@ -7,8 +7,8 @@ import javax.inject.Inject
class MainPresenter @Inject constructor(errorHandler: ErrorHandler)
: BasePresenter<MainView>(errorHandler) {
override fun attachView(view: MainView) {
super.attachView(view)
override fun onAttachView(view: MainView) {
super.onAttachView(view)
view.initView()
}
@ -22,7 +22,6 @@ class MainPresenter @Inject constructor(errorHandler: ErrorHandler)
fun onTabSelected(index: Int, wasSelected: Boolean): Boolean {
return view?.run {
expandActionBar(true)
if (wasSelected) {
notifyMenuViewReselected()
false

View File

@ -10,8 +10,6 @@ interface MainView : BaseView {
fun setViewTitle(title: String)
fun expandActionBar(show: Boolean)
fun viewTitle(index: Int): String
fun currentMenuIndex(): Int

View File

@ -26,14 +26,13 @@ class AttendanceDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
attendance = getSerializable(ARGUMENT_KEY) as Attendance
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(getString(R.string.all_details))
return inflater.inflate(R.layout.dialog_attendance, container, false)
}

View File

@ -10,11 +10,12 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_attendance.*
import javax.inject.Inject
class AttendanceFragment : BaseFragment(), AttendanceView {
class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MenuFragmentView {
@Inject
lateinit var presenter: AttendancePresenter
@ -24,6 +25,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = AttendanceFragment()
}
@ -33,41 +35,42 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this@AttendanceFragment)
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
messageContainer = attendanceRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun initView() {
attendanceAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnItemClickListener { presenter.onAttendanceItemSelected(getItem(it))}
attendanceAdapter.apply {
setOnItemClickListener { presenter.onAttendanceItemSelected(getItem(it)) }
}
attendanceRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = attendanceAdapter
}
attendanceSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) }
attendancePreviousButton.setOnClickListener { presenter.loadAttendanceForPreviousDay() }
attendanceNextButton.setOnClickListener { presenter.loadAttendanceForNextDay() }
attendanceSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() }
attendanceNextButton.setOnClickListener { presenter.onNextDay() }
}
override fun updateData(data: List<AttendanceItem>) {
attendanceAdapter.updateDataSet(data, true)
}
override fun clearData() {
attendanceAdapter.clear()
}
override fun updateNavigationDay(date: String) {
attendanceNavDate.text = date
}
override fun clearData() {
attendanceAdapter.clear()
}
override fun isViewEmpty() = attendanceAdapter.isEmpty
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) {
attendanceEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
@ -80,8 +83,8 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
attendanceRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showRefresh(show: Boolean) {
attendanceSwipe.isRefreshing = show
override fun hideRefresh() {
attendanceSwipe.isRefreshing = false
}
override fun showPreButton(show: Boolean) {
@ -102,8 +105,8 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
presenter.detachView()
}
}

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.ui.main.attendance
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -10,9 +12,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance.*
class AttendanceItem : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
lateinit var attendance: Attendance
class AttendanceItem(val attendance: Attendance) : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
@ -20,6 +20,16 @@ class AttendanceItem : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
override fun getLayoutRes(): Int = R.layout.item_attendance
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.apply {
attendanceItemNumber.text = attendance.number.toString()
attendanceItemSubject.text = attendance.subject
attendanceItemDescription.text = attendance.name
attendanceItemAlert.visibility = attendance.run { if (absence && !excused) VISIBLE else INVISIBLE }
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -34,22 +44,10 @@ class AttendanceItem : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
return attendance.hashCode()
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.bind(attendance)
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View
get() = contentView
fun bind(lesson: Attendance) {
attendanceItemNumber.text = lesson.number.toString()
attendanceItemSubject.text = lesson.subject
attendanceItemDescription.text = lesson.name
attendanceItemAlert.visibility = if (lesson.absence && !lesson.excused) View.VISIBLE else View.INVISIBLE
}
}
}

View File

@ -2,14 +2,15 @@ package io.github.wulkanowy.ui.main.attendance
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.SessionRepository
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 org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class AttendancePresenter @Inject constructor(
@ -19,79 +20,79 @@ class AttendancePresenter @Inject constructor(
private val sessionRepository: SessionRepository
) : BasePresenter<AttendanceView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().nearSchoolDayPrevOnWeekEnd
lateinit var currentDate: LocalDate
private set
override fun attachView(view: AttendanceView) {
super.attachView(view)
fun onAttachView(view: AttendanceView, date: Long?) {
super.onAttachView(view)
view.initView()
loadData(ofEpochDay(date ?: now().previousOrSameSchoolDay.toEpochDay()))
reloadView()
}
fun loadAttendanceForPreviousDay() = loadData(currentDate.previousWorkDay.toEpochDay())
fun loadAttendanceForNextDay() = loadData(currentDate.nextWorkDay.toEpochDay())
fun loadData(date: Long?, forceRefresh: Boolean = false) {
this.currentDate = LocalDate.ofEpochDay(date
?: currentDate.nearSchoolDayPrevOnWeekEnd.toEpochDay())
if (currentDate.isHolidays) return
disposable.clear()
disposable.add(sessionRepository.getSemesters()
.map { selectSemester(it, -1) }
.flatMap { attendanceRepository.getAttendance(it, currentDate, currentDate, forceRefresh) }
.map { createAttendanceItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doOnSubscribe {
view?.run {
showRefresh(forceRefresh)
showProgress(!forceRefresh)
if (!forceRefresh) {
showEmpty(false)
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)
})
fun onPreviousDay() {
loadData(currentDate.previousSchoolDay)
reloadView()
}
private fun createAttendanceItems(items: List<Attendance>): List<AttendanceItem> {
return items.map {
AttendanceItem().apply { attendance = it }
}
fun onNextDay() {
loadData(currentDate.nextSchoolDay)
reloadView()
}
fun onSwipeRefresh() {
loadData(currentDate, true)
}
fun onViewReselected() {
loadData(now().previousOrSameSchoolDay)
reloadView()
}
fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is AttendanceItem) view?.showAttendanceDialog(item.attendance)
}
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
}
}
private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
currentDate = date
disposable.apply {
clear()
add(sessionRepository.getSemesters()
.delay(200, MILLISECONDS)
.map { it.single { semester -> semester.current } }
.flatMap { attendanceRepository.getAttendance(it, date, date, forceRefresh) }
.map { items -> items.map { AttendanceItem(it) } }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
}
}
.subscribe({
view?.apply {
updateData(it)
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
}
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
}
)
}
}
private fun reloadView() {
view?.apply {
showProgress(true)
showContent(false)
showEmpty(false)
clearData()
showNextButton(!currentDate.plusDays(1).isHolidays)
showPreButton(!currentDate.minusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE \n dd.MM.YYYY").capitalize())
}
}
}

View File

@ -9,20 +9,20 @@ interface AttendanceView : BaseView {
fun updateData(data: List<AttendanceItem>)
fun clearData()
fun updateNavigationDay(date: String)
fun clearData()
fun isViewEmpty(): Boolean
fun hideRefresh()
fun showEmpty(show: Boolean)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun showRefresh(show: Boolean)
fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean)

View File

@ -26,14 +26,13 @@ class ExamDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
exam = getSerializable(ARGUMENT_KEY) as Exam
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
dialog.setTitle(getString(R.string.all_details))
return inflater.inflate(R.layout.dialog_exam, container, false)
}

View File

@ -11,11 +11,12 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_exam.*
import javax.inject.Inject
class ExamFragment : BaseFragment(), ExamView {
class ExamFragment : BaseFragment(), ExamView, MainView.MenuFragmentView {
@Inject
lateinit var presenter: ExamPresenter
@ -34,10 +35,8 @@ class ExamFragment : BaseFragment(), ExamView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this@ExamFragment)
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
messageContainer = examRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun initView() {
@ -48,9 +47,13 @@ class ExamFragment : BaseFragment(), ExamView {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = examAdapter
}
examSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) }
examPreviousButton.setOnClickListener { presenter.loadExamsForPreviousWeek() }
examNextButton.setOnClickListener { presenter.loadExamsForNextWeek()}
examSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
examNextButton.setOnClickListener { presenter.onNextWeek() }
}
override fun hideRefresh() {
examSwipe.isRefreshing = false
}
override fun updateData(data: List<ExamItem>) {
@ -61,6 +64,16 @@ class ExamFragment : BaseFragment(), ExamView {
examNavDate.text = date
}
override fun clearData() {
examAdapter.clear()
}
override fun isViewEmpty() = examAdapter.isEmpty
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) {
examEmpty.visibility = if (show) VISIBLE else GONE
}
@ -73,10 +86,6 @@ class ExamFragment : BaseFragment(), ExamView {
examRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showRefresh(show: Boolean) {
examSwipe.isRefreshing = show
}
override fun showPreButton(show: Boolean) {
examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE
}
@ -95,7 +104,7 @@ class ExamFragment : BaseFragment(), ExamView {
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
presenter.detachView()
}
}

View File

@ -12,15 +12,21 @@ import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.header_exam.*
import org.threeten.bp.LocalDate
class ExamHeader : AbstractHeaderItem<ExamHeader.ViewHolder>() {
class ExamHeader(private val date: LocalDate) : AbstractHeaderItem<ExamHeader.ViewHolder>() {
lateinit var date: LocalDate
override fun getLayoutRes() = R.layout.header_exam
override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?): ViewHolder {
return ViewHolder(view, adapter)
}
override fun getLayoutRes() = R.layout.header_exam
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.run {
examHeaderDay.text = date.weekDayName.capitalize()
examHeaderDate.text = date.toFormattedString()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -37,21 +43,9 @@ class ExamHeader : AbstractHeaderItem<ExamHeader.ViewHolder>() {
return date.hashCode()
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>?, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.run {
examHeaderDay.text = date.weekDayName.capitalize()
examHeaderDate.text = date.toFormattedString()
}
}
class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : ExpandableViewHolder(view, adapter),
LayoutContainer {
init {
contentView.setOnClickListener(this)
}
override val containerView: View
get() = contentView
}

View File

@ -13,21 +13,6 @@ import kotlinx.android.synthetic.main.item_exam.*
class ExamItem(header: ExamHeader, val exam: Exam) : AbstractSectionableItem<ExamItem.ViewHolder, ExamHeader>(header) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExamItem
if (exam != other.exam) return false
return true
}
override fun hashCode(): Int {
return exam.hashCode()
}
override fun getLayoutRes() = R.layout.item_exam
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ViewHolder {
@ -43,6 +28,21 @@ class ExamItem(header: ExamHeader, val exam: Exam) : AbstractSectionableItem<Exa
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ExamItem
if (exam != other.exam) return false
return true
}
override fun hashCode(): Int {
return exam.hashCode()
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer {

View File

@ -3,15 +3,15 @@ package io.github.wulkanowy.ui.main.exam
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.schedulers.SchedulersManager
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekFirstDayNextOnWeekEnd
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class ExamPresenter @Inject constructor(
@ -21,79 +21,89 @@ class ExamPresenter @Inject constructor(
private val sessionRepository: SessionRepository
) : BasePresenter<ExamView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().weekFirstDayNextOnWeekEnd
lateinit var currentDate: LocalDate
private set
override fun attachView(view: ExamView) {
super.attachView(view)
fun onAttachView(view: ExamView, date: Long?) {
super.onAttachView(view)
view.initView()
loadData(ofEpochDay(date ?: now().nextOrSameSchoolDay.toEpochDay()))
reloadView()
}
fun loadExamsForPreviousWeek() = loadData(currentDate.minusDays(7).toEpochDay())
fun loadExamsForNextWeek() = loadData(currentDate.plusDays(7).toEpochDay())
fun loadData(date: Long?, forceRefresh: Boolean = false) {
this.currentDate = LocalDate.ofEpochDay(date
?: currentDate.weekFirstDayNextOnWeekEnd.toEpochDay())
if (currentDate.isHolidays) return
disposable.clear()
disposable.add(sessionRepository.getSemesters()
.map { selectSemester(it, -1) }
.flatMap { examRepository.getExams(it, currentDate, currentDate.plusDays(4), forceRefresh) }
.map { it.groupBy { exam -> exam.date }.toSortedMap() }
.map { createExamItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doOnSubscribe {
view?.run {
showRefresh(forceRefresh)
showProgress(!forceRefresh)
if (!forceRefresh) showEmpty(false)
showContent(null == date && forceRefresh)
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek(currentDate.toFormattedString("dd.MM") +
"-${currentDate.plusDays(4).toFormattedString("dd.MM")}")
}
}
.doAfterSuccess {
view?.run {
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
}
}
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
}
}
.subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
fun onPreviousWeek() {
loadData(currentDate.minusDays(7))
reloadView()
}
private fun createExamItems(items: Map<LocalDate, List<Exam>>): List<ExamItem> {
return items.flatMap {
val header = ExamHeader().apply { date = it.key }
it.value.reversed().map { item ->
ExamItem(header, item)
}
}
fun onNextWeek() {
loadData(currentDate.plusDays(7))
reloadView()
}
fun onSwipeRefresh() {
loadData(currentDate, true)
}
fun onExamItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is ExamItem) view?.showExamDialog(item.exam)
}
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
}
fun onViewReselected() {
loadData(now().nextOrSameSchoolDay)
reloadView()
}
private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
currentDate = date
disposable.apply {
clear()
add(sessionRepository.getSemesters()
.delay(200, MILLISECONDS)
.map { it.single { semester -> semester.current } }
.flatMap {
examRepository.getExams(it, currentDate.monday, currentDate.friday, forceRefresh)
}.map { it.groupBy { exam -> exam.date }.toSortedMap() }
.map { createExamItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
}
}
.subscribe({
view?.apply {
updateData(it)
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
}
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
})
}
}
private fun createExamItems(items: Map<LocalDate, List<Exam>>): List<ExamItem> {
return items.flatMap {
ExamHeader(it.key).let { header ->
it.value.reversed().map { item -> ExamItem(header, item) }
}
}
}
private fun reloadView() {
view?.apply {
showProgress(true)
showContent(false)
showEmpty(false)
clearData()
showPreButton(!currentDate.minusDays(7).isHolidays)
showNextButton(!currentDate.plusDays(7).isHolidays)
updateNavigationWeek("${currentDate.toFormattedString("dd.MM")} - " +
currentDate.plusDays(4).toFormattedString("dd.MM"))
}
}
}

View File

@ -9,19 +9,23 @@ interface ExamView : BaseView {
fun updateData(data: List<ExamItem>)
fun updateNavigationWeek(date: String)
fun clearData()
fun isViewEmpty(): Boolean
fun hideRefresh()
fun showEmpty(show: Boolean)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun showRefresh(show: Boolean)
fun showNextButton(show: Boolean)
fun showPreButton(show: Boolean)
fun showExamDialog(exam: Exam)
fun updateNavigationWeek(date: String)
}

View File

@ -24,6 +24,8 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MenuFragmentView {
lateinit var pagerAdapter: BasePagerAdapter
companion object {
private const val SAVED_SEMESTER_KEY = "CURRENT_SEMESTER"
fun newInstance() = GradeFragment()
}
@ -38,7 +40,7 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MenuFragmentView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.attachView(this)
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY))
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
@ -113,8 +115,13 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MenuFragmentView {
(pagerAdapter.registeredFragments[index] as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVED_SEMESTER_KEY, presenter.selectedIndex)
}
override fun onDestroyView() {
super.onDestroyView()
presenter.detachView()
presenter.onDetachView()
}
}

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.schedulers.SchedulersManager
import io.reactivex.Completable
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class GradePresenter @Inject constructor(
@ -14,16 +14,18 @@ class GradePresenter @Inject constructor(
private val schedulers: SchedulersManager,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {
private var semesters = emptyList<Semester>()
var selectedIndex = 0
private set
private var selectedIndex = 0
private var semesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, String>()
override fun attachView(view: GradeView) {
super.attachView(view)
disposable.add(Completable.timer(150, TimeUnit.MILLISECONDS, schedulers.mainThread())
fun onAttachView(view: GradeView, savedIndex: Int?) {
super.onAttachView(view)
disposable.add(Completable.timer(150, MILLISECONDS, schedulers.mainThread())
.subscribe {
selectedIndex = savedIndex ?: 0
view.initView()
loadData()
})
@ -34,13 +36,13 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) view?.showSemesterDialog(selectedIndex)
if (semesters.isNotEmpty()) view?.showSemesterDialog(selectedIndex - 1)
return true
}
fun onSemesterSelected(index: Int) {
if (selectedIndex != index) {
selectedIndex = index
if (selectedIndex != index - 1) {
selectedIndex = index + 1
loadedSemesterId.clear()
view?.let {
notifyChildrenSemesterChange()
@ -67,9 +69,9 @@ class GradePresenter @Inject constructor(
private fun loadData() {
disposable.add(sessionRepository.getSemesters()
.map {
.doOnSuccess {
it.first { item -> item.current }.also { current ->
selectedIndex = current.semesterName - 1
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
}
}
@ -81,7 +83,7 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
semesters.first { it.semesterName == selectedIndex + 1 }.semesterId.also {
semesters.first { it.semesterName == selectedIndex }.semesterId.also {
if (forceRefresh || loadedSemesterId[index] != it) {
view?.notifyChildLoadData(index, it, forceRefresh)
}

View File

@ -30,6 +30,7 @@ class GradeDetailsDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
grade = getSerializable(ARGUMENT_KEY) as Grade
}
@ -48,7 +49,7 @@ class GradeDetailsDialog : DialogFragment() {
gradeDialogColorValue.text = getString(grade.colorStringId)
gradeDialogCommentValue.apply {
if (grade.comment.isEmpty()) {
if (grade.comment.isBlank()) {
visibility = GONE
gradeDialogComment.visibility = GONE
} else text = grade.comment
@ -59,15 +60,15 @@ class GradeDetailsDialog : DialogFragment() {
setBackgroundResource(grade.valueColor)
}
gradeDialogTeacherValue.text = if (grade.teacher.isEmpty()) {
gradeDialogTeacherValue.text = if (grade.teacher.isBlank()) {
getString(R.string.all_no_data)
} else grade.teacher
gradeDialogDescriptionValue.text = grade.run {
when {
description.isEmpty() && gradeSymbol.isNotEmpty() -> gradeSymbol
description.isEmpty() && gradeSymbol.isEmpty() -> getString(R.string.all_no_description)
gradeSymbol.isNotEmpty() && description.isNotEmpty() -> "$gradeSymbol - $description"
description.isBlank() && gradeSymbol.isNotBlank() -> gradeSymbol
description.isBlank() && gradeSymbol.isBlank() -> getString(R.string.all_no_description)
gradeSymbol.isNotBlank() && description.isNotBlank() -> "$gradeSymbol - $description"
else -> description
}
}

View File

@ -37,7 +37,8 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.attachView(this)
messageContainer = gradeDetailsRecycler
presenter.onAttachView(this)
}
override fun initView() {
@ -47,6 +48,8 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
setOnItemClickListener { presenter.onGradeItemSelected(getItem(it)) }
}
gradeDetailsAdapter.getItemCountOfTypes()
gradeDetailsRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeDetailsAdapter
@ -100,7 +103,7 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
}
override fun onParentLoadData(semesterId: String, forceRefresh: Boolean) {
presenter.loadData(semesterId, forceRefresh)
presenter.onParentViewLoadData(semesterId, forceRefresh)
}
override fun onParentReselected() {
@ -108,7 +111,7 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
}
override fun onParentChangeSemester() {
presenter.onParentChangeSemester()
presenter.onParentViewChangeSemester()
}
override fun notifyParentDataLoaded(semesterId: String) {
@ -129,6 +132,6 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
override fun onDestroyView() {
super.onDestroyView()
presenter.detachView()
presenter.onDetachView()
}
}

View File

@ -31,7 +31,7 @@ class GradeDetailsItem(val grade: Grade, private val weightString: String, priva
text = grade.entry
setBackgroundResource(valueColor)
}
gradeItemDescription.text = if (grade.description.isNotEmpty()) grade.description else grade.gradeSymbol
gradeItemDescription.text = if (grade.description.isNotBlank()) grade.description else grade.gradeSymbol
gradeItemDate.text = grade.date.toFormattedString()
gradeItemWeight.text = "$weightString: ${grade.weight}"
gradeItemNote.visibility = if (grade.isNew) VISIBLE else GONE

View File

@ -17,12 +17,12 @@ class GradeDetailsPresenter @Inject constructor(
private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeDetailsView>(errorHandler) {
override fun attachView(view: GradeDetailsView) {
super.attachView(view)
override fun onAttachView(view: GradeDetailsView) {
super.onAttachView(view)
view.initView()
}
fun loadData(semesterId: String, forceRefresh: Boolean) {
fun onParentViewLoadData(semesterId: String, forceRefresh: Boolean) {
disposable.add(sessionRepository.getSemesters()
.flatMap { gradeRepository.getGrades(it.first { item -> item.semesterId == semesterId }, forceRefresh) }
.map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) }
@ -76,7 +76,7 @@ class GradeDetailsPresenter @Inject constructor(
}
}
fun onParentChangeSemester() {
fun onParentViewChangeSemester() {
view?.run {
showProgress(true)
showRefresh(false)

View File

@ -33,7 +33,8 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.attachView(this)
messageContainer = gradeSummaryRecycler
presenter.onAttachView(this)
}
override fun initView() {
@ -81,7 +82,7 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh
}
override fun onParentLoadData(semesterId: String, forceRefresh: Boolean) {
presenter.loadData(semesterId, forceRefresh)
presenter.onParentViewLoadData(semesterId, forceRefresh)
}
override fun onParentReselected() {
@ -89,7 +90,7 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh
}
override fun onParentChangeSemester() {
presenter.onParentChangeSemester()
presenter.onParentViewChangeSemester()
}
override fun notifyParentDataLoaded(semesterId: String) {
@ -106,6 +107,6 @@ class GradeSummaryFragment : BaseFragment(), GradeSummaryView, GradeView.GradeCh
override fun onDestroyView() {
super.onDestroyView()
presenter.detachView()
presenter.onDetachView()
}
}

View File

@ -20,12 +20,12 @@ class GradeSummaryPresenter @Inject constructor(
private val schedulers: SchedulersManager)
: BasePresenter<GradeSummaryView>(errorHandler) {
override fun attachView(view: GradeSummaryView) {
super.attachView(view)
override fun onAttachView(view: GradeSummaryView) {
super.onAttachView(view)
view.initView()
}
fun loadData(semesterId: String, forceRefresh: Boolean) {
fun onParentViewLoadData(semesterId: String, forceRefresh: Boolean) {
disposable.add(sessionRepository.getSemesters()
.map { semester -> semester.first { it.semesterId == semesterId } }
.flatMap {
@ -76,7 +76,7 @@ class GradeSummaryPresenter @Inject constructor(
}
}
fun onParentChangeSemester() {
fun onParentViewChangeSemester() {
view?.run {
showProgress(true)
showRefresh(false)
@ -110,12 +110,12 @@ class GradeSummaryPresenter @Inject constructor(
private fun checkEmpty(gradeSummary: GradeSummary, averages: Map<String, Double>): Boolean {
return gradeSummary.run {
finalGrade.isEmpty() && predictedGrade.isEmpty() && averages[subject] == null
finalGrade.isBlank() && predictedGrade.isBlank() && averages[subject] == null
}
}
private fun formatAverage(average: Double, defaultValue: String = "-- --"): String {
return if (average == 0.0 || average.isNaN()) defaultValue
return if (average == 0.0) defaultValue
else format(FRANCE, "%.2f", average)
}
}

View File

@ -28,14 +28,13 @@ class TimetableDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme)
setStyle(STYLE_NO_TITLE, 0)
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)
}

View File

@ -10,11 +10,12 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
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.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_timetable.*
import javax.inject.Inject
class TimetableFragment : BaseFragment(), TimetableView {
class TimetableFragment : BaseFragment(), TimetableView, MainView.MenuFragmentView {
@Inject
lateinit var presenter: TimetablePresenter
@ -24,6 +25,7 @@ class TimetableFragment : BaseFragment(), TimetableView {
companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = TimetableFragment()
}
@ -33,25 +35,22 @@ class TimetableFragment : BaseFragment(), TimetableView {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this@TimetableFragment)
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
messageContainer = timetableRecycler
presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
}
override fun initView() {
timetableAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnItemClickListener { presenter.onTimetableItemSelected(getItem(it))}
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() }
timetableSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
timetablePreviousButton.setOnClickListener { presenter.onPreviousDay() }
timetableNextButton.setOnClickListener { presenter.onNextDay() }
}
override fun updateData(data: List<TimetableItem>) {
@ -68,6 +67,14 @@ class TimetableFragment : BaseFragment(), TimetableView {
override fun isViewEmpty() = timetableAdapter.isEmpty
override fun hideRefresh() {
timetableSwipe.isRefreshing = false
}
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) {
timetableEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
@ -80,10 +87,6 @@ class TimetableFragment : BaseFragment(), TimetableView {
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
}
@ -96,13 +99,15 @@ class TimetableFragment : BaseFragment(), TimetableView {
TimetableDialog.newInstance(lesson).show(fragmentManager, lesson.toString())
}
override fun roomString() = getString(R.string.timetable_room)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
presenter.detachView()
}
}

View File

@ -15,15 +15,29 @@ import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_timetable.*
class TimetableItem : AbstractFlexibleItem<TimetableItem.ViewHolder>() {
class TimetableItem(val lesson: Timetable, private val roomText: String)
: AbstractFlexibleItem<TimetableItem.ViewHolder>() {
lateinit var lesson: Timetable
override fun getLayoutRes(): Int = R.layout.item_timetable
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun getLayoutRes(): Int = R.layout.item_timetable
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?) {
holder.apply {
timetableItemNumber.text = lesson.number.toString()
timetableItemSubject.text = lesson.subject
timetableItemRoom.text = if (lesson.room.isNotBlank()) "$roomText ${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()
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -39,27 +53,10 @@ class TimetableItem : AbstractFlexibleItem<TimetableItem.ViewHolder>() {
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

@ -2,14 +2,15 @@ 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 org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.ofEpochDay
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class TimetablePresenter @Inject constructor(
@ -19,75 +20,78 @@ class TimetablePresenter @Inject constructor(
private val sessionRepository: SessionRepository
) : BasePresenter<TimetableView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().nearSchoolDayNextOnWeekEnd
lateinit var currentDate: LocalDate
private set
override fun attachView(view: TimetableView) {
super.attachView(view)
fun onAttachView(view: TimetableView, date: Long?) {
super.onAttachView(view)
view.initView()
loadData(ofEpochDay(date ?: now().nextOrSameSchoolDay.toEpochDay()))
reloadView()
}
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)
})
fun onPreviousDay() {
loadData(currentDate.previousSchoolDay)
reloadView()
}
private fun createTimetableItems(items: List<Timetable>): List<TimetableItem> {
return items.map {
TimetableItem().apply { lesson = it }
}
fun onNextDay() {
loadData(currentDate.nextSchoolDay)
reloadView()
}
fun onSwipeRefresh() {
loadData(currentDate, true)
}
fun onViewReselected() {
loadData(now().nextOrSameSchoolDay)
reloadView()
}
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
}
}
private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
currentDate = date
disposable.apply {
clear()
add(sessionRepository.getSemesters()
.delay(200, MILLISECONDS)
.map { it.single { semester -> semester.current } }
.flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) }
.map { items -> items.map { TimetableItem(it, view?.roomString().orEmpty()) } }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
}
}
.subscribe({
view?.apply {
updateData(it)
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
}
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
})
}
}
private fun reloadView() {
view?.apply {
showProgress(true)
showContent(false)
showEmpty(false)
clearData()
showNextButton(!currentDate.plusDays(1).isHolidays)
showPreButton(!currentDate.minusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE \n dd.MM.YYYY").capitalize())
}
}
}

View File

@ -11,20 +11,23 @@ interface TimetableView : BaseView {
fun updateNavigationDay(date: String)
fun isViewEmpty(): Boolean
fun clearData()
fun hideRefresh()
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()
fun roomString(): String
}

View File

@ -13,12 +13,7 @@ class SplashActivity : BaseActivity(), SplashView {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
presenter.attachView(this)
}
override fun onDestroy() {
super.onDestroy()
presenter.detachView()
presenter.onAttachView(this)
}
override fun openLoginView() {
@ -30,4 +25,9 @@ class SplashActivity : BaseActivity(), SplashView {
startActivity(MainActivity.getStartIntent(this))
finish()
}
override fun onDestroy() {
presenter.onDetachView()
super.onDestroy()
}
}

View File

@ -9,8 +9,8 @@ class SplashPresenter @Inject constructor(private val sessionRepository: Session
errorHandler: ErrorHandler)
: BasePresenter<SplashView>(errorHandler) {
override fun attachView(view: SplashView) {
super.attachView(view)
override fun onAttachView(view: SplashView) {
super.onAttachView(view)
view.run { if (sessionRepository.isSessionSaved) openMainView() else openLoginView() }
}
}

View File

@ -19,7 +19,7 @@ fun List<Grade>.calcAverage(): Double {
fun List<GradeSummary>.calcAverage(): Double {
return asSequence().mapNotNull {
if (it.finalGrade.matches("[0-6]".toRegex())) it.finalGrade.toDouble() else null
}.average()
}.average().let { if (it.isNaN()) 0.0 else it }
}
inline val Grade.valueColor: Int
@ -43,6 +43,7 @@ inline val Grade.colorStringId: Int
"F04C4C" -> R.string.all_red
"20A4F7" -> R.string.all_blue
"6ECD07" -> R.string.all_green
"B16CF1" -> R.string.all_purple
else -> R.string.all_empty_color
}
}

View File

@ -1,23 +1,24 @@
package io.github.wulkanowy.utils
import org.threeten.bp.DayOfWeek.*
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.DateTimeFormatter.ofPattern
import org.threeten.bp.temporal.TemporalAdjusters
import org.threeten.bp.temporal.TemporalAdjusters.*
import java.text.SimpleDateFormat
import java.util.*
private const val DATE_PATTERN = "yyyy-MM-dd"
private const val DATE_PATTERN = "dd.MM.yyyy"
fun Date.toLocalDate(): LocalDate {
return LocalDate.parse(SimpleDateFormat(DATE_PATTERN, Locale.getDefault()).format(this))
return Instant.ofEpochMilli(this.time).atZone(ZoneId.systemDefault()).toLocalDate()
}
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 Date.toLocalDateTime(): LocalDateTime {
return Instant.ofEpochMilli(this.time).atZone(ZoneId.systemDefault()).toLocalDateTime()
}
fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate {
return LocalDate.parse(this, DateTimeFormatter.ofPattern(format))
@ -25,9 +26,9 @@ fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate {
fun LocalDate.toFormattedString(format: String = DATE_PATTERN): String = this.format(ofPattern(format))
fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = this.format(DateTimeFormatter.ofPattern(format))
fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = this.format(ofPattern(format))
inline val LocalDate.nextWorkDay: LocalDate
inline val LocalDate.nextSchoolDay: LocalDate
get() {
return when (this.dayOfWeek) {
FRIDAY, SATURDAY, SUNDAY -> this.with(next(MONDAY))
@ -35,7 +36,7 @@ inline val LocalDate.nextWorkDay: LocalDate
}
}
inline val LocalDate.previousWorkDay: LocalDate
inline val LocalDate.previousSchoolDay: LocalDate
get() {
return when (this.dayOfWeek) {
SATURDAY, SUNDAY, MONDAY -> this.with(previous(FRIDAY))
@ -43,7 +44,15 @@ inline val LocalDate.previousWorkDay: LocalDate
}
}
inline val LocalDate.nearSchoolDayPrevOnWeekEnd: LocalDate
inline val LocalDate.nextOrSameSchoolDay: LocalDate
get() {
return when (this.dayOfWeek) {
SATURDAY, SUNDAY -> this.with(next(MONDAY))
else -> this
}
}
inline val LocalDate.previousOrSameSchoolDay: LocalDate
get() {
return when (this.dayOfWeek) {
SATURDAY, SUNDAY -> this.with(previous(FRIDAY))
@ -51,27 +60,14 @@ inline val LocalDate.nearSchoolDayPrevOnWeekEnd: LocalDate
}
}
inline val LocalDate.nearSchoolDayNextOnWeekEnd: LocalDate
get() {
return when (this.dayOfWeek) {
SATURDAY, SUNDAY -> this.with(next(MONDAY))
else -> this
}
}
inline val LocalDate.weekDayName: String
get() = this.format(ofPattern("EEEE", Locale.getDefault()))
inline val LocalDate.weekFirstDayAlwaysCurrent: LocalDate
get() = this.with(TemporalAdjusters.previousOrSame(MONDAY))
inline val LocalDate.monday: LocalDate
get() = this.with(MONDAY)
inline val LocalDate.weekFirstDayNextOnWeekEnd: LocalDate
get() {
return when (this.dayOfWeek) {
SATURDAY, SUNDAY -> this.with(next(MONDAY))
else -> this.with(previousOrSame(MONDAY))
}
}
inline val LocalDate.friday: LocalDate
get() = this.with(FRIDAY)
/**
* [Dz.U. 2016 poz. 1335](http://prawo.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20160001335)

View File

@ -11,7 +11,7 @@ import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties.*
import android.util.Base64
import android.util.Base64.DEFAULT
import android.util.Base64.*
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@ -20,8 +20,8 @@ import java.nio.charset.Charset
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.PublicKey
import java.util.*
import java.util.Calendar.YEAR
import javax.crypto.Cipher
import javax.crypto.Cipher.DECRYPT_MODE
import javax.crypto.Cipher.ENCRYPT_MODE
@ -30,131 +30,114 @@ import javax.crypto.CipherOutputStream
import javax.security.auth.x500.X500Principal
import kotlin.collections.ArrayList
object Scrambler {
private const val KEY_ALIAS = "USER_PASSWORD"
private const val KEY_ALIAS = "USER_PASSWORD"
private const val ALGORITHM_RSA = "RSA"
private const val ALGORITHM_RSA = "RSA"
private const val KEYSTORE_NAME = "AndroidKeyStore"
private const val KEYSTORE_NAME = "AndroidKeyStore"
private const val KEY_TRANSFORMATION_ALGORITHM = "RSA/ECB/PKCS1Padding"
private const val KEY_TRANSFORMATION_ALGORITHM = "RSA/ECB/PKCS1Padding"
private const val KEY_CIPHER_JELLY_PROVIDER = "AndroidOpenSSL"
private const val KEY_CIPHER_JELLY_PROVIDER = "AndroidOpenSSL"
private const val KEY_CIPHER_M_PROVIDER = "AndroidKeyStoreBCWorkaround"
private const val KEY_CIPHER_M_PROVIDER = "AndroidKeyStoreBCWorkaround"
private val KEY_CHARSET = Charset.forName("UTF-8")
private val KEY_CHARSET = Charset.forName("UTF-8")
private val isKeyPairExists: Boolean
get() = keyStore.getKey(KEY_ALIAS, null) != null
@JvmStatic
fun encrypt(plainText: String, context: Context): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
if (SDK_INT < JELLY_BEAN_MR2) {
return String(Base64.encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
return try {
if (!isKeyPairExist()) generateKeyPair(context)
val cipher = getCipher()
cipher.init(ENCRYPT_MODE, getPublicKey())
val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream, cipher)
cipherOutputStream.write(plainText.toByteArray(KEY_CHARSET))
cipherOutputStream.close()
Base64.encodeToString(outputStream.toByteArray(), DEFAULT)
} catch (exception: Exception) {
Timber.e(exception, "An error occurred while encrypting text")
String(Base64.encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
}
@JvmStatic
fun decrypt(cipherText: String): String {
if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
if (SDK_INT < JELLY_BEAN_MR2 || cipherText.length < 250) {
return String(Base64.decode(cipherText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
if (!isKeyPairExist()) throw ScramblerException("KeyPair doesn't exist")
try {
val cipher = getCipher()
cipher.init(DECRYPT_MODE, getPrivateKey())
val input = CipherInputStream(ByteArrayInputStream(Base64.decode(cipherText, DEFAULT)), cipher)
val values = ArrayList<Byte>()
var nextByte = 0
while ({ nextByte = input.read(); nextByte }() != -1) {
values.add(nextByte.toByte())
}
val bytes = ByteArray(values.size)
for (i in bytes.indices) {
bytes[i] = values[i]
}
return String(bytes, 0, bytes.size, KEY_CHARSET)
} catch (e: Exception) {
throw ScramblerException("An error occurred while decrypting text", e)
}
}
private fun getKeyStoreInstance(): KeyStore {
val keyStore = KeyStore.getInstance(KEYSTORE_NAME)
keyStore.load(null)
return keyStore
}
private fun getPublicKey(): PublicKey =
(getKeyStoreInstance().getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry)
.certificate.publicKey
private fun getPrivateKey(): PrivateKey =
(getKeyStoreInstance().getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry).privateKey
private fun getCipher(): Cipher {
private val cipher: Cipher
get() {
return if (SDK_INT >= M) Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_M_PROVIDER)
else Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_JELLY_PROVIDER)
}
@TargetApi(JELLY_BEAN_MR2)
private fun generateKeyPair(context: Context) {
val spec = if (SDK_INT >= M) {
KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
.setDigests(DIGEST_SHA256, DIGEST_SHA512)
.setCertificateSubject(X500Principal("CN=Wulkanowy"))
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_PKCS1)
.setSignaturePaddings(SIGNATURE_PADDING_RSA_PKCS1)
.setCertificateSerialNumber(BigInteger.TEN)
.build()
} else {
val start = Calendar.getInstance()
val end = Calendar.getInstance()
end.add(Calendar.YEAR, 99)
private val keyStore: KeyStore
get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) }
KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.time)
.setEndDate(end.time)
.build()
}
fun encrypt(plainText: String, context: Context): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
val generator = KeyPairGenerator.getInstance(ALGORITHM_RSA, KEYSTORE_NAME)
generator.initialize(spec)
generator.generateKeyPair()
Timber.i("A new KeyPair has been generated")
if (SDK_INT < JELLY_BEAN_MR2) {
return String(Base64.encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
return try {
if (!isKeyPairExists) generateKeyPair(context)
cipher.let {
it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey)
ByteArrayOutputStream().let { output ->
CipherOutputStream(output, it).apply {
write(plainText.toByteArray(KEY_CHARSET))
close()
}
encodeToString(output.toByteArray(), DEFAULT)
}
}
} catch (exception: Exception) {
Timber.e(exception, "An error occurred while encrypting text")
String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
private fun isKeyPairExist(): Boolean = getKeyStoreInstance().getKey(KEY_ALIAS, null) != null
}
fun decrypt(cipherText: String): String {
if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
if (SDK_INT < JELLY_BEAN_MR2 || cipherText.length < 250) {
return String(decode(cipherText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
}
if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist")
return try {
cipher.let {
it.init(DECRYPT_MODE, (keyStore.getKey(KEY_ALIAS, null) as PrivateKey))
CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input ->
val values = ArrayList<Byte>()
var nextByte = 0
while ({ nextByte = input.read(); nextByte }() != -1) {
values.add(nextByte.toByte())
}
val bytes = ByteArray(values.size)
for (i in bytes.indices) {
bytes[i] = values[i]
}
String(bytes, 0, bytes.size, KEY_CHARSET)
}
}
} catch (e: Exception) {
throw ScramblerException("An error occurred while decrypting text", e)
}
}
@TargetApi(JELLY_BEAN_MR2)
private fun generateKeyPair(context: Context) {
(if (SDK_INT >= M) {
KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
.setDigests(DIGEST_SHA256, DIGEST_SHA512)
.setCertificateSubject(X500Principal("CN=Wulkanowy"))
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_PKCS1)
.setSignaturePaddings(SIGNATURE_PADDING_RSA_PKCS1)
.setCertificateSerialNumber(BigInteger.TEN)
.build()
} else {
KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(Calendar.getInstance().time)
.setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time)
.build()
}).let {
KeyPairGenerator.getInstance(ALGORITHM_RSA, KEYSTORE_NAME).apply {
initialize(it)
genKeyPair()
}
}
Timber.i("A new KeyPair has been generated")
}

View File

@ -4,17 +4,24 @@
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:minWidth="300dp"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/attendance_dialog_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="@string/all_details"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_subject"
android:textIsSelectable="true"
android:textSize="17sp" />
<TextView
@ -81,10 +88,8 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="15dp"
android:padding="0dp"
android:text="@string/all_close"
android:textAllCaps="true"
android:textSize="15sp" />
</LinearLayout>
</ScrollView>

View File

@ -4,18 +4,25 @@
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:minWidth="300dp"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/exam_dialog_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="@string/all_details"
android:textSize="20sp" />
<TextView
android:id="@+id/examDialogSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_subject"
android:textIsSelectable="true"
android:textSize="17sp" />
<TextView
@ -33,7 +40,6 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/exam_type"
android:textIsSelectable="true"
android:textSize="17sp" />
<TextView
@ -102,8 +108,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="15dp"
android:padding="0dp"
android:layout_marginTop="20dp"
android:text="@string/all_close"
android:textAllCaps="true"
android:textSize="15sp" />

View File

@ -4,12 +4,20 @@
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:minWidth="300dp"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/exam_dialog_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="@string/all_details"
android:textSize="20sp" />
<TextView
android:id="@+id/timetableDialogChangesTitle"
android:layout_width="wrap_content"
@ -33,7 +41,6 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/timetable_lesson"
android:textIsSelectable="true"
android:textSize="17sp" />
<TextView
@ -119,7 +126,6 @@
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="15dp"
android:padding="0dp"
android:text="@string/all_close"
android:textAllCaps="true"
android:textSize="15sp" />

View File

@ -1,15 +1,13 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<FrameLayout
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
android:layout_height="match_parent"
android:layout_marginBottom="50dp">
<ProgressBar
android:id="@+id/attendanceProgress"
@ -42,8 +40,8 @@
<android.support.v7.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_main_attendance_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
@ -56,12 +54,14 @@
android:text="@string/attendance_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:elevation="10dp"
android:orientation="horizontal">
<Button
@ -72,7 +72,7 @@
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="start|center"
android:text="@string/prev"
android:text="@string/all_prev"
android:textAlignment="gravity" />
<TextView
@ -91,9 +91,7 @@
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="end|center"
android:text="@string/next"
android:text="@string/all_next"
android:textAlignment="gravity" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -1,63 +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/attendance_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/attendance_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/attendance_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/attendance_tab_fragment_no_item_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/attendance_tab_fragment_no_item_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="40dp"
android:minHeight="100dp"
android:minWidth="100dp"
app:tint="?android:attr/textColorPrimary"
app:srcCompat="@drawable/ic_menu_main_attendance_24dp"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/attendance_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/attendance_no_items"
android:textSize="20sp" />
</RelativeLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/attendance_tab_fragment_swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/attendance_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,15 +1,13 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<FrameLayout
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
android:layout_height="match_parent"
android:layout_marginBottom="50dp">
<ProgressBar
android:id="@+id/examProgress"
@ -42,8 +40,8 @@
<android.support.v7.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_main_exam_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
@ -56,12 +54,14 @@
android:text="@string/exam_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:elevation="10dp"
android:orientation="horizontal">
<Button
@ -72,7 +72,7 @@
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="start|center"
android:text="@string/prev"
android:text="@string/all_prev"
android:textAlignment="gravity" />
<TextView
@ -91,9 +91,7 @@
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="end|center"
android:text="@string/next"
android:text="@string/all_next"
android:textAlignment="gravity" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -7,7 +7,7 @@
<android.support.design.widget.TabLayout
android:id="@+id/gradeTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="48dp"
android:background="@color/colorPrimary"
android:elevation="5dp"
android:visibility="invisible"
@ -21,13 +21,13 @@
android:id="@+id/gradeViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/gradeTabLayout"
android:layout_marginTop="48dp"
android:visibility="invisible" />
<ProgressBar
android:id="@+id/gradeProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:indeterminate="true" />
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -1,15 +1,13 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<FrameLayout
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
android:layout_height="match_parent"
android:layout_marginBottom="50dp">
<ProgressBar
android:id="@+id/timetableProgress"
@ -42,8 +40,8 @@
<android.support.v7.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="100dp"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_main_timetable_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
@ -56,12 +54,14 @@
android:text="@string/timetable_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>
</android.support.design.widget.CoordinatorLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:elevation="10dp"
android:orientation="horizontal">
<Button
@ -72,7 +72,7 @@
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="start|center"
android:text="@string/prev"
android:text="@string/all_prev"
android:textAlignment="gravity" />
<TextView
@ -91,9 +91,7 @@
android:layout_weight="1"
android:drawablePadding="4dp"
android:gravity="end|center"
android:text="@string/next"
android:text="@string/all_next"
android:textAlignment="gravity" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -1,86 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless">
<RelativeLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/attendance_header_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_toLeftOf="@id/attendance_header_alert_image"
android:layout_toStartOf="@+id/attendance_header_alert_image"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/app_name"
android:textSize="19sp" />
<TextView
android:id="@+id/attendance_header_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/attendance_header_day"
android:layout_marginTop="5dp"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="14sp" />
<TextView
android:id="@+id/attendance_header_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/attendance_header_date"
android:layout_toRightOf="@+id/attendance_header_date"
android:layout_below="@+id/attendance_header_day"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="14sp" />
<TextView
android:id="@+id/attendance_header_free_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginLeft="180dp"
android:layout_marginStart="180dp"
android:gravity="end"
android:maxLines="2"
android:text="@string/attendance_no_items"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="16sp" />
<ImageView
android:id="@+id/attendance_header_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_all_note_24dp"
tools:ignore="contentDescription" />
</RelativeLayout>
</FrameLayout>

View File

@ -1,19 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorControlHighlight"
android:minHeight="40dp"
android:paddingBottom="10dp"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingTop="10dp"
android:paddingRight="20dp"
android:paddingTop="10dp">
android:paddingBottom="10dp">
<TextView
android:id="@+id/examHeaderDay"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_weight="1"
android:text="@string/app_name"
android:textSize="18sp" />
@ -21,15 +22,10 @@
android:id="@+id/examHeaderDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_toEndOf="@id/examHeaderDay"
android:layout_toRightOf="@id/examHeaderDay"
android:layout_marginLeft="10dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="13sp" />
</RelativeLayout>
</LinearLayout>

View File

@ -1,22 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorControlHighlight"
android:paddingBottom="7dp"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingTop="7dp"
android:paddingRight="20dp"
android:paddingTop="7dp">
android:paddingBottom="7dp">
<TextView
android:id="@+id/gradeSummaryHeaderName"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="40dp"
android:layout_marginRight="40dp"
android:layout_toLeftOf="@id/gradeSummaryHeaderAverage"
android:layout_toStartOf="@id/gradeSummaryHeaderAverage"
android:layout_weight="1"
android:text="@string/app_name"
android:textSize="17sp" />
@ -24,11 +23,8 @@
android:id="@+id/gradeSummaryHeaderAverage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

View File

@ -1,71 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless">
<RelativeLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/timetable_header_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_toLeftOf="@id/timetable_header_alert_image"
android:layout_toStartOf="@+id/timetable_header_alert_image"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/app_name"
android:textSize="19sp" />
<TextView
android:id="@+id/timetable_header_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/timetable_header_day"
android:layout_marginTop="5dp"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="14sp" />
<TextView
android:id="@+id/timetable_header_free_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginLeft="180dp"
android:layout_marginStart="180dp"
android:gravity="end"
android:maxLines="2"
android:text="@string/app_name"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="16sp" />
<ImageView
android:id="@+id/timetable_header_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_all_note_24dp"
tools:ignore="contentDescription" />
</RelativeLayout>
</FrameLayout>

View File

@ -4,6 +4,7 @@
android:id="@+id/attendanceItemContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:paddingStart="12dp"
android:paddingLeft="12dp"

View File

@ -2,14 +2,15 @@
android:id="@+id/exams_subitem_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless">
<TextView
android:id="@+id/examItemSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:text="@string/app_name"
android:textSize="15sp" />
@ -18,11 +19,11 @@
android:id="@+id/examItemType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/examItemSubject"
android:layout_alignStart="@id/examItemSubject"
android:layout_below="@id/examItemSubject"
android:layout_marginBottom="5dp"
android:layout_alignStart="@id/examItemSubject"
android:layout_alignLeft="@id/examItemSubject"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:text="@string/app_name"
android:textSize="13sp" />
@ -30,15 +31,15 @@
android:id="@+id/examItemTeacher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/examItemSubject"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@id/examItemSubject"
android:layout_marginBottom="10dp"
android:layout_marginEnd="20dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="20dp"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="10dp"
android:layout_toEndOf="@id/examItemType"
android:layout_toRightOf="@id/examItemType"
android:gravity="end"

View File

@ -1,16 +1,18 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:minHeight="35dp">
android:minHeight="35dp"
android:orientation="horizontal">
<TextView
android:id="@+id/gradeSummaryItemTitle"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="20dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/grade_summary_predicted_grade"
android:textSize="14sp" />
@ -18,16 +20,12 @@
android:id="@+id/gradeSummaryItemGrade"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="25dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="25dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_toEndOf="@id/gradeSummaryItemTitle"
android:layout_toRightOf="@id/gradeSummaryItemTitle"
android:layout_marginLeft="10dp"
android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end"
android:text="@string/app_name"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

View File

@ -4,13 +4,14 @@
android:id="@+id/timetable_subitem_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:paddingBottom="7dp"
android:paddingEnd="12dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingStart="12dp"
android:paddingTop="7dp">
android:paddingLeft="12dp"
android:paddingTop="7dp"
android:paddingEnd="12dp"
android:paddingRight="12dp"
android:paddingBottom="7dp">
<TextView
android:id="@+id/timetableItemNumber"
@ -28,10 +29,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginEnd="40dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="40dp"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="40dp"
android:layout_marginRight="40dp"
android:layout_toEndOf="@+id/timetableItemNumber"
android:layout_toRightOf="@+id/timetableItemNumber"
android:ellipsize="end"
@ -44,9 +45,9 @@
android:id="@+id/timetableItemTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/timetableItemNumber"
android:layout_alignLeft="@id/timetableItemSubject"
android:layout_alignStart="@id/timetableItemSubject"
android:layout_alignLeft="@id/timetableItemSubject"
android:layout_alignBottom="@+id/timetableItemNumber"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="?android:attr/android:textColorSecondary"
@ -57,10 +58,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/timetableItemNumber"
android:layout_marginEnd="40dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="40dp"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginEnd="40dp"
android:layout_marginRight="40dp"
android:layout_toEndOf="@+id/timetableItemTime"
android:layout_toRightOf="@+id/timetableItemTime"
android:maxLines="1"

View File

@ -104,6 +104,8 @@
<string name="all_cancel">Anuluj</string>
<string name="all_no_data">Brak danych</string>
<string name="all_subject">Przedmiot</string>
<string name="all_prev">Poprzedni</string>
<string name="all_next">Następny</string>
<!--Timetable Widget-->
@ -157,6 +159,7 @@
<string name="all_red">Czerwony</string>
<string name="all_blue">Niebieski</string>
<string name="all_green">Zielony</string>
<string name="all_purple">Fioletowy</string>
<string name="all_empty_color">Brak koloru</string>
@ -166,7 +169,4 @@
<string name="all_sync_fail">Podczas synchronizacji wystąpił błąd</string>
<string name="all_timeout">Zbyt długie oczekiwanie na połączenie</string>
<string name="all_login_failed">Logowanie nie powiodło się. Spróbuj ponownie lub zrestartuj aplikację</string>
<string name="prev">Poprzedni</string>
<string name="next">Następny</string>
</resources>

View File

@ -59,7 +59,7 @@
<!--Timetable-->
<string name="timetable_lesson">Lesson</string>
<string name="timetable_room">Room %s</string>
<string name="timetable_room">Room</string>
<string name="timetable_group">Group</string>
<string name="timetable_time">Hours</string>
<string name="timetable_changes">Changes</string>
@ -99,6 +99,8 @@
<string name="all_cancel">Cancel</string>
<string name="all_no_data">No data</string>
<string name="all_subject">Subject</string>
<string name="all_prev">Prev</string>
<string name="all_next">Next</string>
<!--Timetable Widget-->
@ -150,6 +152,7 @@
<string name="all_red">Red</string>
<string name="all_blue">Blue</string>
<string name="all_green">Green</string>
<string name="all_purple">Purple</string>
<string name="all_empty_color">No color</string>
@ -159,6 +162,4 @@
<string name="all_sync_fail">There was an error during synchronization</string>
<string name="all_timeout">Too long wait for connection</string>
<string name="all_login_failed">Login is failed. Try again or restart the app</string>
<string name="prev">Prev</string>
<string name="next">Next</string>
</resources>