1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-02-21 20:24:44 +01:00

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

@ -7,7 +7,7 @@ references:
container_config: &container_config container_config: &container_config
docker: docker:
- image: circleci/android:api-27-alpha - image: circleci/android:api-28-alpha
working_directory: *workspace_root working_directory: *workspace_root
environment: environment:
environment: environment:

View File

@ -73,6 +73,7 @@ ext.supportVersion = "28.0.0"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation('com.github.wulkanowy:api:a5667f8000') { exclude module: "threetenbp" } implementation('com.github.wulkanowy:api:a5667f8000') { exclude module: "threetenbp" }
implementation "com.android.support:support-v4:$supportVersion" implementation "com.android.support:support-v4:$supportVersion"
implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:appcompat-v7:$supportVersion"
implementation "com.android.support:design:$supportVersion" implementation "com.android.support:design:$supportVersion"
@ -116,6 +117,7 @@ dependencies {
testImplementation "junit:junit:4.12" testImplementation "junit:junit:4.12"
testImplementation "io.mockk:mockk:1.8.8" testImplementation "io.mockk:mockk:1.8.8"
testImplementation "org.mockito:mockito-inline:2.21.0" testImplementation "org.mockito:mockito-inline:2.21.0"
testImplementation 'org.threeten:threetenbp:1.3.7'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "org.mockito:mockito-android:2.21.0" androidTestImplementation "org.mockito:mockito-android:2.21.0"

View File

@ -34,9 +34,9 @@ class AttendanceLocalTest {
@Test @Test
fun saveAndReadTest() { fun saveAndReadTest() {
attendanceLocal.saveAttendance(listOf( attendanceLocal.saveAttendance(listOf(
Attendance(0, "1", "2", LocalDate.of(2018, 9, 10), 0, "", ""), Attendance("1", "2", LocalDate.of(2018, 9, 10), 0, "", ""),
Attendance(0, "1", "2", LocalDate.of(2018, 9, 14), 0, "", ""), Attendance("1", "2", LocalDate.of(2018, 9, 14), 0, "", ""),
Attendance(0, "1", "2", LocalDate.of(2018, 9, 17), 0, "", "") Attendance("1", "2", LocalDate.of(2018, 9, 17), 0, "", "")
)) ))
val attendance = attendanceLocal val attendance = attendanceLocal

View File

@ -34,9 +34,9 @@ class ExamLocalTest {
@Test @Test
fun saveAndReadTest() { fun saveAndReadTest() {
examLocal.saveExams(listOf( examLocal.saveExams(listOf(
Exam(0, "1", "2", LocalDate.of(2018, 9, 10), LocalDate.now(), "", "", "", "", "", ""), Exam("1", "2", LocalDate.of(2018, 9, 10), LocalDate.now(), "", "", "", "", "", ""),
Exam(0, "1", "2", LocalDate.of(2018, 9, 14), LocalDate.now(), "", "", "", "", "", ""), Exam("1", "2", LocalDate.of(2018, 9, 14), LocalDate.now(), "", "", "", "", "", ""),
Exam(0, "1", "2", LocalDate.of(2018, 9, 17), LocalDate.now(), "", "", "", "", "", "") Exam("1", "2", LocalDate.of(2018, 9, 17), LocalDate.now(), "", "", "", "", "", "")
)) ))
val exams = examLocal val exams = examLocal

View File

@ -11,6 +11,7 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import kotlin.test.assertEquals import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -33,14 +34,17 @@ class TimetableLocalTest {
@Test @Test
fun saveAndReadTest() { fun saveAndReadTest() {
timetableDb.saveLessons(listOf( timetableDb.saveTimetable(listOf(
Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 10)), Timetable("1", "2", 1, LocalDateTime.now(), LocalDateTime.now(),
Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 14)), LocalDate.of(2018, 9, 10), "", "", "", "", ""),
Timetable(studentId = "1", diaryId = "2", date = LocalDate.of(2018, 9, 17)) // in next week Timetable("1", "2", 1, LocalDateTime.now(), LocalDateTime.now(),
LocalDate.of(2018, 9, 14), "", "", "", "", ""),
Timetable("1", "2", 1, LocalDateTime.now(), LocalDateTime.now(),
LocalDate.of(2018, 9, 17), "", "", "", "", "")
)) ))
val exams = timetableDb.getLessons( val exams = timetableDb.getTimetable(
Semester(studentId = "1", diaryId = "2", semesterId = "3", diaryName = "", semesterName = 1), Semester(0, "1", "2", "3", "", 1),
LocalDate.of(2018, 9, 10), LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14) LocalDate.of(2018, 9, 14)
).blockingGet() ).blockingGet()

View File

@ -17,32 +17,32 @@ class ScramblerTest {
@Test @Test
fun encryptDecryptTest() { fun encryptDecryptTest() {
assertEquals("TEST", Scrambler.decrypt(Scrambler.encrypt("TEST", assertEquals("TEST", decrypt(encrypt("TEST",
InstrumentationRegistry.getTargetContext()))) InstrumentationRegistry.getTargetContext())))
} }
@Test @Test
fun emptyTextEncryptTest() { fun emptyTextEncryptTest() {
assertFailsWith<ScramblerException> { assertFailsWith<ScramblerException> {
Scrambler.decrypt("") decrypt("")
} }
assertFailsWith<ScramblerException> { assertFailsWith<ScramblerException> {
Scrambler.encrypt("", InstrumentationRegistry.getTargetContext()) encrypt("", InstrumentationRegistry.getTargetContext())
} }
} }
@Test @Test
@SdkSuppress(minSdkVersion = 18) @SdkSuppress(minSdkVersion = 18)
fun emptyKeyStoreTest() { fun emptyKeyStoreTest() {
val text = Scrambler.encrypt("test", InstrumentationRegistry.getTargetContext()) val text = encrypt("test", InstrumentationRegistry.getTargetContext())
val keyStore = KeyStore.getInstance("AndroidKeyStore") val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null) keyStore.load(null)
keyStore.deleteEntry("USER_PASSWORD") keyStore.deleteEntry("USER_PASSWORD")
assertFailsWith<ScramblerException> { assertFailsWith<ScramblerException> {
Scrambler.decrypt(text) decrypt(text)
} }
} }
} }

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.db.dao package io.github.wulkanowy.data.db.dao
import android.arch.persistence.room.Dao import android.arch.persistence.room.Dao
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query import android.arch.persistence.room.Query
@ -13,6 +14,9 @@ interface GradeSummaryDao {
@Insert(onConflict = REPLACE) @Insert(onConflict = REPLACE)
fun insertAll(gradesSummary: List<GradeSummary>) fun insertAll(gradesSummary: List<GradeSummary>)
@Delete
fun deleteAll(gradesSummary: List<GradeSummary>)
@Query("SELECT * FROM grades_summary WHERE student_id = :studentId AND semester_id = :semesterId") @Query("SELECT * FROM grades_summary WHERE student_id = :studentId AND semester_id = :semesterId")
fun getGradesSummary(semesterId: String, studentId: String): Maybe<List<GradeSummary>> 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.Dao
import android.arch.persistence.room.Insert import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query import android.arch.persistence.room.Query
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Single import io.reactivex.Single
@ -10,7 +9,7 @@ import io.reactivex.Single
@Dao @Dao
interface SemesterDao { interface SemesterDao {
@Insert(onConflict = REPLACE) @Insert
fun insertAll(semester: List<Semester>) fun insertAll(semester: List<Semester>)
@Query("SELECT * FROM Semesters WHERE student_id = :studentId") @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.Dao
import android.arch.persistence.room.Insert import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query import android.arch.persistence.room.Query
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe import io.reactivex.Maybe
@ -10,7 +9,7 @@ import io.reactivex.Maybe
@Dao @Dao
interface StudentDao { interface StudentDao {
@Insert(onConflict = REPLACE) @Insert
fun insert(student: Student): Long fun insert(student: Student): Long
@Query("SELECT * FROM Students WHERE id = :id") @Query("SELECT * FROM Students WHERE id = :id")

View File

@ -9,9 +9,6 @@ import java.io.Serializable
@Entity(tableName = "Attendance") @Entity(tableName = "Attendance")
data class Attendance( data class Attendance(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id") @ColumnInfo(name = "student_id")
var studentId: String, var studentId: String,
@ -37,4 +34,8 @@ data class Attendance(
var excused: Boolean = false, var excused: Boolean = false,
var deleted: 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") @Entity(tableName = "Exams")
data class Exam( data class Exam(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id") @ColumnInfo(name = "student_id")
var studentId: String, var studentId: String,
@ -35,4 +32,8 @@ data class Exam(
@ColumnInfo(name = "teacher_symbol") @ColumnInfo(name = "teacher_symbol")
var teacherSymbol: String 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.ColumnInfo
import android.arch.persistence.room.Entity import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey import android.arch.persistence.room.PrimaryKey
@Entity(tableName = "Grades_Summary", @Entity(tableName = "Grades_Summary")
indices = [Index(value = ["semester_id", "student_id", "subject"], unique = true)])
data class GradeSummary( data class GradeSummary(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "semester_id") @ColumnInfo(name = "semester_id")
var semesterId: String, var semesterId: String,
@ -23,4 +18,8 @@ data class GradeSummary(
var predictedGrade: String, var predictedGrade: String,
var finalGrade: 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.ColumnInfo
import android.arch.persistence.room.Entity import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey import android.arch.persistence.room.PrimaryKey
@Entity(tableName = "Semesters", @Entity(tableName = "Semesters")
indices = [Index(value = ["semester_id", "diary_id", "student_id"], unique = true)])
data class Semester( data class Semester(
@PrimaryKey(autoGenerate = true) @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.ColumnInfo
import android.arch.persistence.room.Entity import android.arch.persistence.room.Entity
import android.arch.persistence.room.Index
import android.arch.persistence.room.PrimaryKey import android.arch.persistence.room.PrimaryKey
@Entity(tableName = "Students", @Entity(tableName = "Students")
indices = [Index(value = ["student_id", "student_name"], unique = true)])
data class Student( data class Student(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)

View File

@ -10,14 +10,11 @@ import java.io.Serializable
@Entity(tableName = "Timetable") @Entity(tableName = "Timetable")
data class Timetable( data class Timetable(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,
@ColumnInfo(name = "student_id") @ColumnInfo(name = "student_id")
var studentId: String = "", var studentId: String,
@ColumnInfo(name = "diary_id") @ColumnInfo(name = "diary_id")
var diaryId: String = "", var diaryId: String,
val number: Int = 0, val number: Int = 0,
@ -27,17 +24,21 @@ data class Timetable(
val date: LocalDate, 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 changes: Boolean = false,
val canceled: 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.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.AttendanceLocal import io.github.wulkanowy.data.repositories.local.AttendanceLocal
import io.github.wulkanowy.data.repositories.remote.AttendanceRemote 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 io.reactivex.Single
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.TemporalAdjusters
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -22,24 +21,25 @@ class AttendanceRepository @Inject constructor(
private val remote: AttendanceRemote private val remote: AttendanceRemote
) { ) {
fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Attendance>> { fun getAttendance(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean)
val start = startDate.weekFirstDayAlwaysCurrent : Single<List<Attendance>> {
val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY)) return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
return local.getAttendance(semester, start, end).filter { !forceRefresh } local.getAttendance(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getAttendance(semester, start, end) if (it) remote.getAttendance(semester, dates.first, dates.second)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
}.flatMap { newLessons -> }.flatMap { newAttendance ->
local.getAttendance(semester, start, end).toSingle(emptyList()).map { grades -> local.getAttendance(semester, dates.first, dates.second)
local.deleteAttendance(grades - newLessons) .toSingle(emptyList())
local.saveAttendance(newLessons - grades) .doOnSuccess { oldAttendance ->
newLessons local.deleteAttendance(oldAttendance - newAttendance)
} local.saveAttendance(newAttendance - oldAttendance)
}).map { list -> }
list.asSequence().filter { }.flatMap {
it.date in startDate..endDate local.getAttendance(semester, dates.first, dates.second)
}.toList() .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.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.ExamLocal import io.github.wulkanowy.data.repositories.local.ExamLocal
import io.github.wulkanowy.data.repositories.remote.ExamRemote 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 io.reactivex.Single
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.TemporalAdjusters
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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>> { fun getExams(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Exam>> {
val start = startDate.weekFirstDayAlwaysCurrent return Single.fromCallable { startDate.monday to endDate.friday }
val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY)) .flatMap { dates ->
local.getExams(semester, dates.first, dates.second).filter { !forceRefresh }
return local.getExams(semester, start, end).filter { !forceRefresh } .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { .flatMap {
if (it) remote.getExams(semester, start, end) if (it) remote.getExams(semester, dates.first, dates.second)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
}.flatMap { newExams -> }.flatMap { newExams ->
local.getExams(semester, start, end).toSingle(emptyList()).map { grades -> local.getExams(semester, dates.first, dates.second)
local.deleteExams(grades - newExams) .toSingle(emptyList())
local.saveExams(newExams - grades) .doOnSuccess { oldExams ->
newExams local.deleteExams(oldExams - newExams)
} local.saveExams(newExams - oldExams)
}).map { list -> }
list.asSequence().filter { }.flatMap {
it.date in startDate..endDate local.getExams(semester, dates.first, dates.second)
}.toList() .toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
} }
} }
} }

View File

@ -24,7 +24,12 @@ class GradeSummaryRepository @Inject constructor(
.flatMap { .flatMap {
if (it) remote.getGradeSummary(semester) if (it) remote.getGradeSummary(semester)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
} }.flatMap { newGradesSummary ->
).doOnSuccess { local.saveGradesSummary(it) } 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.db.entities.Timetable
import io.github.wulkanowy.data.repositories.local.TimetableLocal import io.github.wulkanowy.data.repositories.local.TimetableLocal
import io.github.wulkanowy.data.repositories.remote.TimetableRemote 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 io.reactivex.Single
import org.threeten.bp.DayOfWeek
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.temporal.TemporalAdjusters
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -22,24 +21,26 @@ class TimetableRepository @Inject constructor(
private val remote: TimetableRemote private val remote: TimetableRemote
) { ) {
fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Timetable>> { fun getTimetable(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false)
val start = startDate.weekFirstDayAlwaysCurrent : Single<List<Timetable>> {
val end = endDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY)) return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
return local.getLessons(semester, start, end).filter { !forceRefresh } local.getTimetable(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
if (it) remote.getLessons(semester, start, end) .flatMap {
else Single.error(UnknownHostException()) if (it) remote.getTimetable(semester, dates.first, dates.second)
}.flatMap { newLessons -> else Single.error(UnknownHostException())
local.getLessons(semester, start, end).toSingle(emptyList()).map { lessons -> }.flatMap { newTimetable ->
local.deleteLessons(lessons - newLessons) local.getTimetable(semester, dates.first, dates.second)
local.saveLessons(newLessons - lessons) .toSingle(emptyList())
newLessons .doOnSuccess { oldTimetable ->
} local.deleteTimetable(oldTimetable - newTimetable)
}).map { list -> local.saveTimetable(newTimetable - oldTimetable)
list.asSequence().filter { }
it.date in startDate..endDate }.flatMap {
}.toList() 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>) { fun saveGradesSummary(gradesSummary: List<GradeSummary>) {
gradeSummaryDb.insertAll(gradesSummary) 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.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.security.Scrambler.decrypt import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.Scrambler.encrypt import io.github.wulkanowy.utils.security.encrypt
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single

View File

@ -9,16 +9,16 @@ import javax.inject.Inject
class TimetableLocal @Inject constructor(private val timetableDb: TimetableDao) { 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) return timetableDb.getTimetable(semester.diaryId, semester.studentId, startDate, endDate)
.filter { !it.isEmpty() } .filter { !it.isEmpty() }
} }
fun saveLessons(lessons: List<Timetable>) { fun saveTimetable(timetables: List<Timetable>) {
timetableDb.insertAll(lessons) timetableDb.insertAll(timetables)
} }
fun deleteLessons(exams: List<Timetable>) { fun deleteTimetable(timetables: List<Timetable>) {
timetableDb.deleteAll(exams) timetableDb.deleteAll(timetables)
} }
} }

View File

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

View File

@ -11,7 +11,7 @@ import javax.inject.Inject
class TimetableRemote @Inject constructor(private val api: Api) { 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 { return Single.just(api.run {
if (diaryId != semester.diaryId) { if (diaryId != semester.diaryId) {
diaryId = semester.diaryId diaryId = semester.diaryId

View File

@ -1,10 +1,16 @@
package io.github.wulkanowy.ui.base 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 import dagger.android.support.DaggerFragment
abstract class BaseFragment : DaggerFragment(), BaseView { abstract class BaseFragment : DaggerFragment(), BaseView {
protected var messageContainer: View? = null
override fun showMessage(text: String) { 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 var view: T? = null
val isViewAttached: Boolean open fun onAttachView(view: T) {
get() = view != null
open fun attachView(view: T) {
this.view = view this.view = view
errorHandler.showErrorMessage = { view.showMessage(it) } errorHandler.showErrorMessage = { view.showMessage(it) }
} }
open fun detachView() { open fun onDetachView() {
view = null view = null
disposable.clear() disposable.clear()
errorHandler.clear() errorHandler.clear()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,14 +26,13 @@ class AttendanceDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { arguments?.run {
attendance = getSerializable(ARGUMENT_KEY) as Attendance attendance = getSerializable(ARGUMENT_KEY) as Attendance
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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) 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.R
import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_attendance.* import kotlinx.android.synthetic.main.fragment_attendance.*
import javax.inject.Inject import javax.inject.Inject
class AttendanceFragment : BaseFragment(), AttendanceView { class AttendanceFragment : BaseFragment(), AttendanceView, MainView.MenuFragmentView {
@Inject @Inject
lateinit var presenter: AttendancePresenter lateinit var presenter: AttendancePresenter
@ -24,6 +25,7 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
companion object { companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE" private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = AttendanceFragment() fun newInstance() = AttendanceFragment()
} }
@ -33,41 +35,42 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
presenter.run { messageContainer = attendanceRecycler
attachView(this@AttendanceFragment) presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
} }
override fun initView() { override fun initView() {
attendanceAdapter.run { attendanceAdapter.apply {
isAutoCollapseOnExpand = true setOnItemClickListener { presenter.onAttendanceItemSelected(getItem(it)) }
isAutoScrollOnExpand = true
setOnItemClickListener { presenter.onAttendanceItemSelected(getItem(it))}
} }
attendanceRecycler.run { attendanceRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = attendanceAdapter adapter = attendanceAdapter
} }
attendanceSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) } attendanceSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
attendancePreviousButton.setOnClickListener { presenter.loadAttendanceForPreviousDay() } attendancePreviousButton.setOnClickListener { presenter.onPreviousDay() }
attendanceNextButton.setOnClickListener { presenter.loadAttendanceForNextDay() } attendanceNextButton.setOnClickListener { presenter.onNextDay() }
} }
override fun updateData(data: List<AttendanceItem>) { override fun updateData(data: List<AttendanceItem>) {
attendanceAdapter.updateDataSet(data, true) attendanceAdapter.updateDataSet(data, true)
} }
override fun clearData() {
attendanceAdapter.clear()
}
override fun updateNavigationDay(date: String) { override fun updateNavigationDay(date: String) {
attendanceNavDate.text = date attendanceNavDate.text = date
} }
override fun clearData() {
attendanceAdapter.clear()
}
override fun isViewEmpty() = attendanceAdapter.isEmpty override fun isViewEmpty() = attendanceAdapter.isEmpty
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
attendanceEmpty.visibility = if (show) View.VISIBLE else View.GONE 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 attendanceRecycler.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun showRefresh(show: Boolean) { override fun hideRefresh() {
attendanceSwipe.isRefreshing = show attendanceSwipe.isRefreshing = false
} }
override fun showPreButton(show: Boolean) { override fun showPreButton(show: Boolean) {
@ -102,8 +105,8 @@ class AttendanceFragment : BaseFragment(), AttendanceView {
} }
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView() super.onDestroyView()
presenter.detachView()
} }
} }

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.ui.main.attendance package io.github.wulkanowy.ui.main.attendance
import android.view.View import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible 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.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_attendance.* import kotlinx.android.synthetic.main.item_attendance.*
class AttendanceItem : AbstractFlexibleItem<AttendanceItem.ViewHolder>() { class AttendanceItem(val attendance: Attendance) : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
lateinit var attendance: Attendance
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter) return ViewHolder(view, adapter)
@ -20,6 +20,16 @@ class AttendanceItem : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
override fun getLayoutRes(): Int = R.layout.item_attendance 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@ -34,22 +44,10 @@ class AttendanceItem : AbstractFlexibleItem<AttendanceItem.ViewHolder>() {
return attendance.hashCode() 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), class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer { LayoutContainer {
override val containerView: View override val containerView: View
get() = contentView 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 eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler 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.AttendanceRepository
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.schedulers.SchedulersManager import io.github.wulkanowy.utils.schedulers.SchedulersManager
import org.threeten.bp.LocalDate 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 import javax.inject.Inject
class AttendancePresenter @Inject constructor( class AttendancePresenter @Inject constructor(
@ -19,79 +20,79 @@ class AttendancePresenter @Inject constructor(
private val sessionRepository: SessionRepository private val sessionRepository: SessionRepository
) : BasePresenter<AttendanceView>(errorHandler) { ) : BasePresenter<AttendanceView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().nearSchoolDayPrevOnWeekEnd lateinit var currentDate: LocalDate
private set private set
override fun attachView(view: AttendanceView) { fun onAttachView(view: AttendanceView, date: Long?) {
super.attachView(view) super.onAttachView(view)
view.initView() view.initView()
loadData(ofEpochDay(date ?: now().previousOrSameSchoolDay.toEpochDay()))
reloadView()
} }
fun loadAttendanceForPreviousDay() = loadData(currentDate.previousWorkDay.toEpochDay()) fun onPreviousDay() {
loadData(currentDate.previousSchoolDay)
fun loadAttendanceForNextDay() = loadData(currentDate.nextWorkDay.toEpochDay()) reloadView()
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)
})
} }
private fun createAttendanceItems(items: List<Attendance>): List<AttendanceItem> { fun onNextDay() {
return items.map { loadData(currentDate.nextSchoolDay)
AttendanceItem().apply { attendance = it } reloadView()
} }
fun onSwipeRefresh() {
loadData(currentDate, true)
}
fun onViewReselected() {
loadData(now().previousOrSameSchoolDay)
reloadView()
} }
fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) { fun onAttendanceItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is AttendanceItem) view?.showAttendanceDialog(item.attendance) if (item is AttendanceItem) view?.showAttendanceDialog(item.attendance)
} }
private fun selectSemester(semesters: List<Semester>, index: Int): Semester { private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
return semesters.single { it.current }.let { currentSemester -> currentDate = date
if (index == -1) currentSemester disposable.apply {
else semesters.single { semester -> clear()
semester.run { add(sessionRepository.getSemesters()
semesterName - 1 == index && diaryId == currentSemester.diaryId .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 updateData(data: List<AttendanceItem>)
fun clearData()
fun updateNavigationDay(date: String) fun updateNavigationDay(date: String)
fun clearData()
fun isViewEmpty(): Boolean fun isViewEmpty(): Boolean
fun hideRefresh()
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showRefresh(show: Boolean)
fun showPreButton(show: Boolean) fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean) fun showNextButton(show: Boolean)

View File

@ -26,14 +26,13 @@ class ExamDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { arguments?.run {
exam = getSerializable(ARGUMENT_KEY) as Exam exam = getSerializable(ARGUMENT_KEY) as Exam
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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) 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.R
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_exam.* import kotlinx.android.synthetic.main.fragment_exam.*
import javax.inject.Inject import javax.inject.Inject
class ExamFragment : BaseFragment(), ExamView { class ExamFragment : BaseFragment(), ExamView, MainView.MenuFragmentView {
@Inject @Inject
lateinit var presenter: ExamPresenter lateinit var presenter: ExamPresenter
@ -34,10 +35,8 @@ class ExamFragment : BaseFragment(), ExamView {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
presenter.run { messageContainer = examRecycler
attachView(this@ExamFragment) presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
} }
override fun initView() { override fun initView() {
@ -48,9 +47,13 @@ class ExamFragment : BaseFragment(), ExamView {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = examAdapter adapter = examAdapter
} }
examSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) } examSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
examPreviousButton.setOnClickListener { presenter.loadExamsForPreviousWeek() } examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
examNextButton.setOnClickListener { presenter.loadExamsForNextWeek()} examNextButton.setOnClickListener { presenter.onNextWeek() }
}
override fun hideRefresh() {
examSwipe.isRefreshing = false
} }
override fun updateData(data: List<ExamItem>) { override fun updateData(data: List<ExamItem>) {
@ -61,6 +64,16 @@ class ExamFragment : BaseFragment(), ExamView {
examNavDate.text = date examNavDate.text = date
} }
override fun clearData() {
examAdapter.clear()
}
override fun isViewEmpty() = examAdapter.isEmpty
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
examEmpty.visibility = if (show) VISIBLE else GONE examEmpty.visibility = if (show) VISIBLE else GONE
} }
@ -73,10 +86,6 @@ class ExamFragment : BaseFragment(), ExamView {
examRecycler.visibility = if (show) VISIBLE else GONE examRecycler.visibility = if (show) VISIBLE else GONE
} }
override fun showRefresh(show: Boolean) {
examSwipe.isRefreshing = show
}
override fun showPreButton(show: Boolean) { override fun showPreButton(show: Boolean) {
examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE examPreviousButton.visibility = if (show) VISIBLE else INVISIBLE
} }
@ -95,7 +104,7 @@ class ExamFragment : BaseFragment(), ExamView {
} }
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView() super.onDestroyView()
presenter.detachView()
} }
} }

View File

@ -12,15 +12,21 @@ import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.header_exam.* import kotlinx.android.synthetic.main.header_exam.*
import org.threeten.bp.LocalDate 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 { override fun createViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?): ViewHolder {
return ViewHolder(view, adapter) 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -37,21 +43,9 @@ class ExamHeader : AbstractHeaderItem<ExamHeader.ViewHolder>() {
return date.hashCode() 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), class ViewHolder(view: View?, adapter: FlexibleAdapter<IFlexible<*>>?) : ExpandableViewHolder(view, adapter),
LayoutContainer { LayoutContainer {
init {
contentView.setOnClickListener(this)
}
override val containerView: View override val containerView: View
get() = contentView 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) { 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 getLayoutRes() = R.layout.item_exam
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ViewHolder { 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), class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer { LayoutContainer {

View File

@ -3,15 +3,15 @@ package io.github.wulkanowy.ui.main.exam
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Exam 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.ExamRepository
import io.github.wulkanowy.data.repositories.SessionRepository import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter 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.schedulers.SchedulersManager
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekFirstDayNextOnWeekEnd
import org.threeten.bp.LocalDate 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 import javax.inject.Inject
class ExamPresenter @Inject constructor( class ExamPresenter @Inject constructor(
@ -21,79 +21,89 @@ class ExamPresenter @Inject constructor(
private val sessionRepository: SessionRepository private val sessionRepository: SessionRepository
) : BasePresenter<ExamView>(errorHandler) { ) : BasePresenter<ExamView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().weekFirstDayNextOnWeekEnd lateinit var currentDate: LocalDate
private set private set
override fun attachView(view: ExamView) { fun onAttachView(view: ExamView, date: Long?) {
super.attachView(view) super.onAttachView(view)
view.initView() view.initView()
loadData(ofEpochDay(date ?: now().nextOrSameSchoolDay.toEpochDay()))
reloadView()
} }
fun loadExamsForPreviousWeek() = loadData(currentDate.minusDays(7).toEpochDay()) fun onPreviousWeek() {
loadData(currentDate.minusDays(7))
fun loadExamsForNextWeek() = loadData(currentDate.plusDays(7).toEpochDay()) reloadView()
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) })
} }
private fun createExamItems(items: Map<LocalDate, List<Exam>>): List<ExamItem> { fun onNextWeek() {
return items.flatMap { loadData(currentDate.plusDays(7))
val header = ExamHeader().apply { date = it.key } reloadView()
it.value.reversed().map { item -> }
ExamItem(header, item)
} fun onSwipeRefresh() {
} loadData(currentDate, true)
} }
fun onExamItemSelected(item: AbstractFlexibleItem<*>?) { fun onExamItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is ExamItem) view?.showExamDialog(item.exam) if (item is ExamItem) view?.showExamDialog(item.exam)
} }
private fun selectSemester(semesters: List<Semester>, index: Int): Semester { fun onViewReselected() {
return semesters.single { it.current }.let { currentSemester -> loadData(now().nextOrSameSchoolDay)
if (index == -1) currentSemester reloadView()
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 {
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 updateData(data: List<ExamItem>)
fun updateNavigationWeek(date: String)
fun clearData()
fun isViewEmpty(): Boolean
fun hideRefresh()
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showRefresh(show: Boolean)
fun showNextButton(show: Boolean) fun showNextButton(show: Boolean)
fun showPreButton(show: Boolean) fun showPreButton(show: Boolean)
fun showExamDialog(exam: Exam) fun showExamDialog(exam: Exam)
fun updateNavigationWeek(date: String)
} }

View File

@ -24,6 +24,8 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MenuFragmentView {
lateinit var pagerAdapter: BasePagerAdapter lateinit var pagerAdapter: BasePagerAdapter
companion object { companion object {
private const val SAVED_SEMESTER_KEY = "CURRENT_SEMESTER"
fun newInstance() = GradeFragment() fun newInstance() = GradeFragment()
} }
@ -38,7 +40,7 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MenuFragmentView {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
presenter.attachView(this) presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY))
} }
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
@ -113,8 +115,13 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MenuFragmentView {
(pagerAdapter.registeredFragments[index] as? GradeView.GradeChildView)?.onParentChangeSemester() (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() { override fun onDestroyView() {
super.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.ui.base.BasePresenter
import io.github.wulkanowy.utils.schedulers.SchedulersManager import io.github.wulkanowy.utils.schedulers.SchedulersManager
import io.reactivex.Completable import io.reactivex.Completable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class GradePresenter @Inject constructor( class GradePresenter @Inject constructor(
@ -14,16 +14,18 @@ class GradePresenter @Inject constructor(
private val schedulers: SchedulersManager, private val schedulers: SchedulersManager,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) { 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>() private val loadedSemesterId = mutableMapOf<Int, String>()
override fun attachView(view: GradeView) { fun onAttachView(view: GradeView, savedIndex: Int?) {
super.attachView(view) super.onAttachView(view)
disposable.add(Completable.timer(150, TimeUnit.MILLISECONDS, schedulers.mainThread()) disposable.add(Completable.timer(150, MILLISECONDS, schedulers.mainThread())
.subscribe { .subscribe {
selectedIndex = savedIndex ?: 0
view.initView() view.initView()
loadData() loadData()
}) })
@ -34,13 +36,13 @@ class GradePresenter @Inject constructor(
} }
fun onSemesterSwitch(): Boolean { fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) view?.showSemesterDialog(selectedIndex) if (semesters.isNotEmpty()) view?.showSemesterDialog(selectedIndex - 1)
return true return true
} }
fun onSemesterSelected(index: Int) { fun onSemesterSelected(index: Int) {
if (selectedIndex != index) { if (selectedIndex != index - 1) {
selectedIndex = index selectedIndex = index + 1
loadedSemesterId.clear() loadedSemesterId.clear()
view?.let { view?.let {
notifyChildrenSemesterChange() notifyChildrenSemesterChange()
@ -67,9 +69,9 @@ class GradePresenter @Inject constructor(
private fun loadData() { private fun loadData() {
disposable.add(sessionRepository.getSemesters() disposable.add(sessionRepository.getSemesters()
.map { .doOnSuccess {
it.first { item -> item.current }.also { current -> 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 } semesters = it.filter { semester -> semester.diaryId == current.diaryId }
} }
} }
@ -81,7 +83,7 @@ class GradePresenter @Inject constructor(
} }
private fun loadChild(index: Int, forceRefresh: Boolean = false) { 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) { if (forceRefresh || loadedSemesterId[index] != it) {
view?.notifyChildLoadData(index, it, forceRefresh) view?.notifyChildLoadData(index, it, forceRefresh)
} }

View File

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

View File

@ -37,7 +37,8 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
presenter.attachView(this) messageContainer = gradeDetailsRecycler
presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
@ -47,6 +48,8 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
setOnItemClickListener { presenter.onGradeItemSelected(getItem(it)) } setOnItemClickListener { presenter.onGradeItemSelected(getItem(it)) }
} }
gradeDetailsAdapter.getItemCountOfTypes()
gradeDetailsRecycler.run { gradeDetailsRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeDetailsAdapter adapter = gradeDetailsAdapter
@ -100,7 +103,7 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
} }
override fun onParentLoadData(semesterId: String, forceRefresh: Boolean) { override fun onParentLoadData(semesterId: String, forceRefresh: Boolean) {
presenter.loadData(semesterId, forceRefresh) presenter.onParentViewLoadData(semesterId, forceRefresh)
} }
override fun onParentReselected() { override fun onParentReselected() {
@ -108,7 +111,7 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
} }
override fun onParentChangeSemester() { override fun onParentChangeSemester() {
presenter.onParentChangeSemester() presenter.onParentViewChangeSemester()
} }
override fun notifyParentDataLoaded(semesterId: String) { override fun notifyParentDataLoaded(semesterId: String) {
@ -129,6 +132,6 @@ class GradeDetailsFragment : BaseFragment(), GradeDetailsView, GradeView.GradeCh
override fun onDestroyView() { override fun onDestroyView() {
super.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 text = grade.entry
setBackgroundResource(valueColor) 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() gradeItemDate.text = grade.date.toFormattedString()
gradeItemWeight.text = "$weightString: ${grade.weight}" gradeItemWeight.text = "$weightString: ${grade.weight}"
gradeItemNote.visibility = if (grade.isNew) VISIBLE else GONE gradeItemNote.visibility = if (grade.isNew) VISIBLE else GONE

View File

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

View File

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

View File

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

View File

@ -28,14 +28,13 @@ class TimetableDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogFragmentTheme) setStyle(STYLE_NO_TITLE, 0)
arguments?.run { arguments?.run {
lesson = getSerializable(ARGUMENT_KEY) as Timetable lesson = getSerializable(ARGUMENT_KEY) as Timetable
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 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) 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.R
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_timetable.* import kotlinx.android.synthetic.main.fragment_timetable.*
import javax.inject.Inject import javax.inject.Inject
class TimetableFragment : BaseFragment(), TimetableView { class TimetableFragment : BaseFragment(), TimetableView, MainView.MenuFragmentView {
@Inject @Inject
lateinit var presenter: TimetablePresenter lateinit var presenter: TimetablePresenter
@ -24,6 +25,7 @@ class TimetableFragment : BaseFragment(), TimetableView {
companion object { companion object {
private const val SAVED_DATE_KEY = "CURRENT_DATE" private const val SAVED_DATE_KEY = "CURRENT_DATE"
fun newInstance() = TimetableFragment() fun newInstance() = TimetableFragment()
} }
@ -33,25 +35,22 @@ class TimetableFragment : BaseFragment(), TimetableView {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
presenter.run { messageContainer = timetableRecycler
attachView(this@TimetableFragment) presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY))
loadData(date = savedInstanceState?.getLong(SAVED_DATE_KEY))
}
} }
override fun initView() { override fun initView() {
timetableAdapter.run { timetableAdapter.run {
isAutoCollapseOnExpand = true setOnItemClickListener { presenter.onTimetableItemSelected(getItem(it)) }
isAutoScrollOnExpand = true
setOnItemClickListener { presenter.onTimetableItemSelected(getItem(it))}
} }
timetableRecycler.run { timetableRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context) layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = timetableAdapter adapter = timetableAdapter
} }
timetableSwipe.setOnRefreshListener { presenter.loadData(date = null, forceRefresh = true) } timetableSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
timetablePreviousButton.setOnClickListener { presenter.loadTimetableForPreviousDay() } timetablePreviousButton.setOnClickListener { presenter.onPreviousDay() }
timetableNextButton.setOnClickListener { presenter.loadTimetableForNextDay() } timetableNextButton.setOnClickListener { presenter.onNextDay() }
} }
override fun updateData(data: List<TimetableItem>) { override fun updateData(data: List<TimetableItem>) {
@ -68,6 +67,14 @@ class TimetableFragment : BaseFragment(), TimetableView {
override fun isViewEmpty() = timetableAdapter.isEmpty override fun isViewEmpty() = timetableAdapter.isEmpty
override fun hideRefresh() {
timetableSwipe.isRefreshing = false
}
override fun onFragmentReselected() {
presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
timetableEmpty.visibility = if (show) View.VISIBLE else View.GONE 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 timetableRecycler.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun showRefresh(show: Boolean) {
timetableSwipe.isRefreshing = show
}
override fun showPreButton(show: Boolean) { override fun showPreButton(show: Boolean) {
timetablePreviousButton.visibility = if (show) View.VISIBLE else View.INVISIBLE timetablePreviousButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
} }
@ -96,13 +99,15 @@ class TimetableFragment : BaseFragment(), TimetableView {
TimetableDialog.newInstance(lesson).show(fragmentManager, lesson.toString()) TimetableDialog.newInstance(lesson).show(fragmentManager, lesson.toString())
} }
override fun roomString() = getString(R.string.timetable_room)
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
} }
override fun onDestroyView() { override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView() super.onDestroyView()
presenter.detachView()
} }
} }

View File

@ -15,15 +15,29 @@ import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_timetable.* 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 { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter) 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -39,27 +53,10 @@ class TimetableItem : AbstractFlexibleItem<TimetableItem.ViewHolder>() {
return lesson.hashCode() 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), class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer { LayoutContainer {
override val containerView: View override val containerView: View
get() = contentView 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 eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler 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.SessionRepository
import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.schedulers.SchedulersManager import io.github.wulkanowy.utils.schedulers.SchedulersManager
import org.threeten.bp.LocalDate 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 import javax.inject.Inject
class TimetablePresenter @Inject constructor( class TimetablePresenter @Inject constructor(
@ -19,75 +20,78 @@ class TimetablePresenter @Inject constructor(
private val sessionRepository: SessionRepository private val sessionRepository: SessionRepository
) : BasePresenter<TimetableView>(errorHandler) { ) : BasePresenter<TimetableView>(errorHandler) {
var currentDate: LocalDate = LocalDate.now().nearSchoolDayNextOnWeekEnd lateinit var currentDate: LocalDate
private set private set
override fun attachView(view: TimetableView) { fun onAttachView(view: TimetableView, date: Long?) {
super.attachView(view) super.onAttachView(view)
view.initView() view.initView()
loadData(ofEpochDay(date ?: now().nextOrSameSchoolDay.toEpochDay()))
reloadView()
} }
fun loadTimetableForPreviousDay() = loadData(currentDate.previousWorkDay.toEpochDay()) fun onPreviousDay() {
loadData(currentDate.previousSchoolDay)
fun loadTimetableForNextDay() = loadData(currentDate.nextWorkDay.toEpochDay()) reloadView()
fun loadData(date: Long?, forceRefresh: Boolean = false) {
this.currentDate = LocalDate.ofEpochDay(date ?: currentDate.nearSchoolDayNextOnWeekEnd.toEpochDay())
if (currentDate.isHolidays) return
disposable.clear()
disposable.add(sessionRepository.getSemesters()
.map { selectSemester(it, -1) }
.flatMap { timetableRepository.getTimetable(it, currentDate, currentDate, forceRefresh) }
.map { createTimetableItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doOnSubscribe {
view?.run {
showRefresh(forceRefresh)
showProgress(!forceRefresh)
if (!forceRefresh) clearData()
showPreButton(!currentDate.minusDays(1).isHolidays)
showNextButton(!currentDate.plusDays(1).isHolidays)
updateNavigationDay(currentDate.toFormattedString("EEEE \n dd.MM.YYYY").capitalize())
}
}
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
}
}
.subscribe({
view?.run {
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
updateData(it)
}
}) {
view?.run { showEmpty(isViewEmpty()) }
errorHandler.proceed(it)
})
} }
private fun createTimetableItems(items: List<Timetable>): List<TimetableItem> { fun onNextDay() {
return items.map { loadData(currentDate.nextSchoolDay)
TimetableItem().apply { lesson = it } reloadView()
} }
fun onSwipeRefresh() {
loadData(currentDate, true)
}
fun onViewReselected() {
loadData(now().nextOrSameSchoolDay)
reloadView()
} }
fun onTimetableItemSelected(item: AbstractFlexibleItem<*>?) { fun onTimetableItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is TimetableItem) view?.showTimetableDialog(item.lesson) if (item is TimetableItem) view?.showTimetableDialog(item.lesson)
} }
private fun selectSemester(semesters: List<Semester>, index: Int): Semester { private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
return semesters.single { it.current }.let { currentSemester -> currentDate = date
if (index == -1) currentSemester disposable.apply {
else semesters.single { semester -> clear()
semester.run { add(sessionRepository.getSemesters()
semesterName - 1 == index && diaryId == currentSemester.diaryId .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 updateNavigationDay(date: String)
fun isViewEmpty(): Boolean
fun clearData()
fun hideRefresh()
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)
fun showProgress(show: Boolean) fun showProgress(show: Boolean)
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showRefresh(show: Boolean)
fun showPreButton(show: Boolean) fun showPreButton(show: Boolean)
fun showNextButton(show: Boolean) fun showNextButton(show: Boolean)
fun showTimetableDialog(lesson: Timetable) fun showTimetableDialog(lesson: Timetable)
fun isViewEmpty(): Boolean fun roomString(): String
fun clearData()
} }

View File

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

View File

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

View File

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

View File

@ -1,23 +1,24 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import org.threeten.bp.DayOfWeek.* import org.threeten.bp.DayOfWeek.*
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.DateTimeFormatter.ofPattern import org.threeten.bp.format.DateTimeFormatter.ofPattern
import org.threeten.bp.temporal.TemporalAdjusters
import org.threeten.bp.temporal.TemporalAdjusters.* import org.threeten.bp.temporal.TemporalAdjusters.*
import java.text.SimpleDateFormat
import java.util.* import java.util.*
private const val DATE_PATTERN = "yyyy-MM-dd" private const val DATE_PATTERN = "dd.MM.yyyy"
fun Date.toLocalDate(): LocalDate { 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", fun Date.toLocalDateTime(): LocalDateTime {
Locale.getDefault()).format(this), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) return Instant.ofEpochMilli(this.time).atZone(ZoneId.systemDefault()).toLocalDateTime()
}
fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate { fun String.toLocalDate(format: String = DATE_PATTERN): LocalDate {
return LocalDate.parse(this, DateTimeFormatter.ofPattern(format)) return LocalDate.parse(this, DateTimeFormatter.ofPattern(format))
@ -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 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() { get() {
return when (this.dayOfWeek) { return when (this.dayOfWeek) {
FRIDAY, SATURDAY, SUNDAY -> this.with(next(MONDAY)) 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() { get() {
return when (this.dayOfWeek) { return when (this.dayOfWeek) {
SATURDAY, SUNDAY, MONDAY -> this.with(previous(FRIDAY)) 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() { get() {
return when (this.dayOfWeek) { return when (this.dayOfWeek) {
SATURDAY, SUNDAY -> this.with(previous(FRIDAY)) 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 inline val LocalDate.weekDayName: String
get() = this.format(ofPattern("EEEE", Locale.getDefault())) get() = this.format(ofPattern("EEEE", Locale.getDefault()))
inline val LocalDate.weekFirstDayAlwaysCurrent: LocalDate inline val LocalDate.monday: LocalDate
get() = this.with(TemporalAdjusters.previousOrSame(MONDAY)) get() = this.with(MONDAY)
inline val LocalDate.weekFirstDayNextOnWeekEnd: LocalDate inline val LocalDate.friday: LocalDate
get() { get() = this.with(FRIDAY)
return when (this.dayOfWeek) {
SATURDAY, SUNDAY -> this.with(next(MONDAY))
else -> this.with(previousOrSame(MONDAY))
}
}
/** /**
* [Dz.U. 2016 poz. 1335](http://prawo.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20160001335) * [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.KeyGenParameterSpec
import android.security.keystore.KeyProperties.* import android.security.keystore.KeyProperties.*
import android.util.Base64 import android.util.Base64
import android.util.Base64.DEFAULT import android.util.Base64.*
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -20,8 +20,8 @@ import java.nio.charset.Charset
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.Calendar.YEAR
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.Cipher.DECRYPT_MODE import javax.crypto.Cipher.DECRYPT_MODE
import javax.crypto.Cipher.ENCRYPT_MODE import javax.crypto.Cipher.ENCRYPT_MODE
@ -30,131 +30,114 @@ import javax.crypto.CipherOutputStream
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
import kotlin.collections.ArrayList 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 private val cipher: Cipher
fun encrypt(plainText: String, context: Context): String { get() {
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 {
return if (SDK_INT >= M) Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_M_PROVIDER) return if (SDK_INT >= M) Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_M_PROVIDER)
else Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_JELLY_PROVIDER) else Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_JELLY_PROVIDER)
} }
@TargetApi(JELLY_BEAN_MR2) private val keyStore: KeyStore
private fun generateKeyPair(context: Context) { get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) }
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)
KeyPairGeneratorSpec.Builder(context) fun encrypt(plainText: String, context: Context): String {
.setAlias(KEY_ALIAS) if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(start.time)
.setEndDate(end.time)
.build()
}
val generator = KeyPairGenerator.getInstance(ALGORITHM_RSA, KEYSTORE_NAME) if (SDK_INT < JELLY_BEAN_MR2) {
generator.initialize(spec) return String(Base64.encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
generator.generateKeyPair() }
Timber.i("A new KeyPair has been generated") 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"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="300dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="300dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="20dp"> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/all_subject" android:text="@string/all_subject"
android:textIsSelectable="true"
android:textSize="17sp" /> android:textSize="17sp" />
<TextView <TextView
@ -81,10 +88,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="15dp" android:layout_marginTop="15dp"
android:padding="0dp"
android:text="@string/all_close" android:text="@string/all_close"
android:textAllCaps="true" android:textAllCaps="true"
android:textSize="15sp" /> android:textSize="15sp" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

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

View File

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

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

View File

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

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"?> <?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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorControlHighlight" android:background="?attr/colorControlHighlight"
android:minHeight="40dp" android:minHeight="40dp"
android:paddingBottom="10dp" android:orientation="horizontal"
android:paddingLeft="20dp" android:paddingLeft="20dp"
android:paddingTop="10dp"
android:paddingRight="20dp" android:paddingRight="20dp"
android:paddingTop="10dp"> android:paddingBottom="10dp">
<TextView <TextView
android:id="@+id/examHeaderDay" android:id="@+id/examHeaderDay"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_weight="1"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="18sp" /> android:textSize="18sp" />
@ -21,15 +22,10 @@
android:id="@+id/examHeaderDate" android:id="@+id/examHeaderDate"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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_marginStart="10dp"
android:layout_toEndOf="@id/examHeaderDay" android:layout_marginLeft="10dp"
android:layout_toRightOf="@id/examHeaderDay"
android:gravity="end" android:gravity="end"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="13sp" /> android:textSize="13sp" />
</RelativeLayout> </LinearLayout>

View File

@ -1,22 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorControlHighlight" android:background="?attr/colorControlHighlight"
android:paddingBottom="7dp" android:orientation="horizontal"
android:paddingLeft="20dp" android:paddingLeft="20dp"
android:paddingTop="7dp"
android:paddingRight="20dp" android:paddingRight="20dp"
android:paddingTop="7dp"> android:paddingBottom="7dp">
<TextView <TextView
android:id="@+id/gradeSummaryHeaderName" android:id="@+id/gradeSummaryHeaderName"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="40dp" android:layout_marginEnd="40dp"
android:layout_marginRight="40dp" android:layout_marginRight="40dp"
android:layout_toLeftOf="@id/gradeSummaryHeaderAverage" android:layout_weight="1"
android:layout_toStartOf="@id/gradeSummaryHeaderAverage"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="17sp" /> android:textSize="17sp" />
@ -24,11 +23,8 @@
android:id="@+id/gradeSummaryHeaderAverage" android:id="@+id/gradeSummaryHeaderAverage"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="end" android:gravity="end"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="12sp" /> 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:id="@+id/attendanceItemContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless" android:foreground="?attr/selectableItemBackgroundBorderless"
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingLeft="12dp" android:paddingLeft="12dp"

View File

@ -2,14 +2,15 @@
android:id="@+id/exams_subitem_container" android:id="@+id/exams_subitem_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless"> android:foreground="?attr/selectableItemBackgroundBorderless">
<TextView <TextView
android:id="@+id/examItemSubject" android:id="@+id/examItemSubject"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="15sp" /> android:textSize="15sp" />
@ -18,11 +19,11 @@
android:id="@+id/examItemType" android:id="@+id/examItemType"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignLeft="@id/examItemSubject"
android:layout_alignStart="@id/examItemSubject"
android:layout_below="@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_marginTop="5dp"
android:layout_marginBottom="5dp"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="13sp" /> android:textSize="13sp" />
@ -30,15 +31,15 @@
android:id="@+id/examItemTeacher" android:id="@+id/examItemTeacher"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/examItemSubject"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="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_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:layout_marginBottom="10dp"
android:layout_toEndOf="@id/examItemType" android:layout_toEndOf="@id/examItemType"
android:layout_toRightOf="@id/examItemType" android:layout_toRightOf="@id/examItemType"
android:gravity="end" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider" android:background="@drawable/ic_all_divider"
android:minHeight="35dp"> android:minHeight="35dp"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/gradeSummaryItemTitle" android:id="@+id/gradeSummaryItemTitle"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_weight="1"
android:text="@string/grade_summary_predicted_grade" android:text="@string/grade_summary_predicted_grade"
android:textSize="14sp" /> android:textSize="14sp" />
@ -18,16 +20,12 @@
android:id="@+id/gradeSummaryItemGrade" android:id="@+id/gradeSummaryItemGrade"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_gravity="center_vertical"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="25dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="25dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_toEndOf="@id/gradeSummaryItemTitle" android:layout_marginLeft="10dp"
android:layout_toRightOf="@id/gradeSummaryItemTitle" android:layout_marginEnd="25dp"
android:layout_marginRight="25dp"
android:gravity="end" android:gravity="end"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="12sp" /> android:textSize="12sp" />
</RelativeLayout> </LinearLayout>

View File

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

View File

@ -104,6 +104,8 @@
<string name="all_cancel">Anuluj</string> <string name="all_cancel">Anuluj</string>
<string name="all_no_data">Brak danych</string> <string name="all_no_data">Brak danych</string>
<string name="all_subject">Przedmiot</string> <string name="all_subject">Przedmiot</string>
<string name="all_prev">Poprzedni</string>
<string name="all_next">Następny</string>
<!--Timetable Widget--> <!--Timetable Widget-->
@ -157,6 +159,7 @@
<string name="all_red">Czerwony</string> <string name="all_red">Czerwony</string>
<string name="all_blue">Niebieski</string> <string name="all_blue">Niebieski</string>
<string name="all_green">Zielony</string> <string name="all_green">Zielony</string>
<string name="all_purple">Fioletowy</string>
<string name="all_empty_color">Brak koloru</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_sync_fail">Podczas synchronizacji wystąpił błąd</string>
<string name="all_timeout">Zbyt długie oczekiwanie na połączenie</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="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> </resources>

View File

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

View File

@ -11,7 +11,6 @@ import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import java.sql.Date import java.sql.Date
class TimetableRemoteTest { class TimetableRemoteTest {
@ -41,7 +40,7 @@ class TimetableRemoteTest {
every { semesterMock.studentId } returns "1" every { semesterMock.studentId } returns "1"
every { semesterMock.diaryId } returns "1" every { semesterMock.diaryId } returns "1"
val timetable = TimetableRemote(mockApi).getLessons(semesterMock, val timetable = TimetableRemote(mockApi).getTimetable(semesterMock,
LocalDate.of(2018, 9, 10), LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 15) LocalDate.of(2018, 9, 15)
).blockingGet() ).blockingGet()

View File

@ -22,7 +22,7 @@ class LoginPresenterTest {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
clearInvocations(loginView) clearInvocations(loginView)
presenter = LoginPresenter(errorHandler) presenter = LoginPresenter(errorHandler)
presenter.attachView(loginView) presenter.onAttachView(loginView)
} }
@Test @Test

View File

@ -31,7 +31,7 @@ class LoginFormPresenterTest {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
clearInvocations(repository, loginFormView) clearInvocations(repository, loginFormView)
presenter = LoginFormPresenter(TestSchedulers(), errorHandler, repository) presenter = LoginFormPresenter(TestSchedulers(), errorHandler, repository)
presenter.attachView(loginFormView) presenter.onAttachView(loginFormView)
} }
@Test @Test

View File

@ -35,7 +35,7 @@ class LoginOptionsPresenterTest {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
clearInvocations(repository, loginOptionsView) clearInvocations(repository, loginOptionsView)
presenter = LoginOptionsPresenter(errorHandler, repository, TestSchedulers()) presenter = LoginOptionsPresenter(errorHandler, repository, TestSchedulers())
presenter.attachView(loginOptionsView) presenter.onAttachView(loginOptionsView)
} }
@Test @Test

View File

@ -23,7 +23,7 @@ class MainPresenterTest {
clearInvocations(mainView) clearInvocations(mainView)
presenter = MainPresenter(errorHandler) presenter = MainPresenter(errorHandler)
presenter.attachView(mainView) presenter.onAttachView(mainView)
} }
@Test @Test

View File

@ -31,14 +31,14 @@ class SplashPresenterTest {
@Test @Test
fun testOpenLoginView() { fun testOpenLoginView() {
doReturn(false).`when`(sessionRepository).isSessionSaved doReturn(false).`when`(sessionRepository).isSessionSaved
presenter.attachView(splashView) presenter.onAttachView(splashView)
verify(splashView).openLoginView() verify(splashView).openLoginView()
} }
@Test @Test
fun testMainMainView() { fun testMainMainView() {
doReturn(true).`when`(sessionRepository).isSessionSaved doReturn(true).`when`(sessionRepository).isSessionSaved
presenter.attachView(splashView) presenter.onAttachView(splashView)
verify(splashView).openMainView() verify(splashView).openMainView()
} }
} }

View File

@ -34,14 +34,10 @@ class GradeExtensionTest {
@Test @Test
fun calcSummaryAverage() { fun calcSummaryAverage() {
assertEquals(2.5, listOf( assertEquals(2.5, listOf(
GradeSummary(0, "", "", "", "", GradeSummary("", "", "", "", "5"),
"5"), GradeSummary("", "", "", "", "-5"),
GradeSummary(0, "", "", "", "", GradeSummary("", "", "", "", "test"),
"-5"), GradeSummary("", "", "", "", "0")
GradeSummary(0, "", "", "", "",
"test"),
GradeSummary(0, "", "", "", "",
"0")
).calcAverage(), 0.005) ).calcAverage(), 0.005)
} }
} }

View File

@ -14,33 +14,33 @@ class TimeExtensionTest {
} }
@Test @Test
fun toFormattedStringTest() { fun toFormattedStringLocalDateTest() {
assertEquals("2018-10-01", LocalDate.of(2018, 10, 1).toFormattedString()) assertEquals("01.10.2018", LocalDate.of(2018, 10, 1).toFormattedString())
assertEquals("2018-10.01", LocalDate.of(2018, 10, 1).toFormattedString("yyyy-MM.dd")) assertEquals("2018-10.01", LocalDate.of(2018, 10, 1).toFormattedString("yyyy-MM.dd"))
} }
@Test @Test
fun toFormat_LocalDateTime() { fun toFormattedStringLocalDateTimeTest() {
assertEquals("2018-10-01", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString()) assertEquals("01.10.2018", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString())
assertEquals("2018-10-01 10:00:00", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString("uuuu-MM-dd HH:mm:ss")) assertEquals("2018-10-01 10:00:00", LocalDateTime.of(2018, 10, 1, 10, 0, 0).toFormattedString("uuuu-MM-dd HH:mm:ss"))
} }
@Test @Test
fun weekFirstDayAlwaysCurrentTest() { fun mondayTest() {
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 2).weekFirstDayAlwaysCurrent) assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 2).monday)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 5).weekFirstDayAlwaysCurrent) assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 5).monday)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 6).weekFirstDayAlwaysCurrent) assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 6).monday)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 7).weekFirstDayAlwaysCurrent) assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 7).monday)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 8).weekFirstDayAlwaysCurrent) assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 8).monday)
} }
@Test @Test
fun weekFirstDayNextOnWeekEndTest() { fun fridayTest() {
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 2).weekFirstDayNextOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 2).friday)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 5).weekFirstDayNextOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 5).friday)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 6).weekFirstDayNextOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 6).friday)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 7).weekFirstDayNextOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 7).friday)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 8).weekFirstDayNextOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 12), LocalDate.of(2018, 10, 8).friday)
} }
@Test @Test
@ -53,43 +53,44 @@ class TimeExtensionTest {
@Test @Test
fun nextSchoolDayTest() { fun nextSchoolDayTest() {
assertEquals(LocalDate.of(2018, 10, 2), LocalDate.of(2018, 10, 1).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 2), LocalDate.of(2018, 10, 1).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 3), LocalDate.of(2018, 10, 2).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 3), LocalDate.of(2018, 10, 2).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 4), LocalDate.of(2018, 10, 3).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 4), LocalDate.of(2018, 10, 3).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 4).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 4).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 5).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 5).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 6).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 6).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 7).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 7).nextSchoolDay)
assertEquals(LocalDate.of(2018, 10, 9), LocalDate.of(2018, 10, 8).nextWorkDay) assertEquals(LocalDate.of(2018, 10, 9), LocalDate.of(2018, 10, 8).nextSchoolDay)
} }
@Test @Test
fun previousSchoolDayTest() { fun previousSchoolDayTest() {
assertEquals(LocalDate.of(2018, 10, 9), LocalDate.of(2018, 10, 10).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 9), LocalDate.of(2018, 10, 10).previousSchoolDay)
assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 9).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 8), LocalDate.of(2018, 10, 9).previousSchoolDay)
assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 8).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 8).previousSchoolDay)
assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 7).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 7).previousSchoolDay)
assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 6).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 5), LocalDate.of(2018, 10, 6).previousSchoolDay)
assertEquals(LocalDate.of(2018, 10, 4), LocalDate.of(2018, 10, 5).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 4), LocalDate.of(2018, 10, 5).previousSchoolDay)
assertEquals(LocalDate.of(2018, 10, 3), LocalDate.of(2018, 10, 4).previousWorkDay) assertEquals(LocalDate.of(2018, 10, 3), LocalDate.of(2018, 10, 4).previousSchoolDay)
}
@Test
fun nextOrSameSchoolDayTest() {
assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 28).nextOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 9, 29).nextOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 9, 30).nextOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 1).nextOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 10, 2), LocalDate.of(2018, 10, 2).nextOrSameSchoolDay)
} }
@Test @Test
fun nearSchoolDayPrevOnWeekEndTest() { fun previousOrSameSchoolDayTest() {
assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 28).nearSchoolDayPrevOnWeekEnd) assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 28).previousOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 29).nearSchoolDayPrevOnWeekEnd) assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 29).previousOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 30).nearSchoolDayPrevOnWeekEnd) assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 30).previousOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 1).nearSchoolDayPrevOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 1).previousOrSameSchoolDay)
assertEquals(LocalDate.of(2018, 10, 2), LocalDate.of(2018, 10, 2).nearSchoolDayPrevOnWeekEnd) assertEquals(LocalDate.of(2018, 10, 2), LocalDate.of(2018, 10, 2).previousOrSameSchoolDay)
}
@Test
fun nearSchoolDayNextOnWeekEndTest() {
assertEquals(LocalDate.of(2018, 9, 28), LocalDate.of(2018, 9, 28).nearSchoolDayNextOnWeekEnd)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 9, 29).nearSchoolDayNextOnWeekEnd)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 9, 30).nearSchoolDayNextOnWeekEnd)
assertEquals(LocalDate.of(2018, 10, 1), LocalDate.of(2018, 10, 1).nearSchoolDayNextOnWeekEnd)
assertEquals(LocalDate.of(2018, 10, 2), LocalDate.of(2018, 10, 2).nearSchoolDayNextOnWeekEnd)
} }
@Test @Test