[APIv2/UI] Add new Timetable module. Implement in Mobidziennik.

This commit is contained in:
Kuba Szczodrzyński 2019-11-10 17:53:10 +01:00
parent 01ac26e67b
commit 1b75424604
27 changed files with 946 additions and 17 deletions

View File

@ -166,6 +166,10 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:${versions.work}"
implementation 'com.hypertrack:hyperlog:0.0.10'
implementation 'com.github.kuba2k2:RecyclerTabLayout:700f980584'
implementation 'com.linkedin.android.tachyon:tachyon:1.0.2'
}
repositories {
mavenCentral()

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4caf50"
android:pathData="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/>
</vector>

View File

@ -6,8 +6,13 @@ import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.*
import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.util.LongSparseArray
import android.util.SparseArray
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.util.forEach
import com.google.gson.JsonArray
@ -326,4 +331,64 @@ fun String.crc32(): Long {
return crc.value
}
fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this)
fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this)
fun CharSequence?.asColoredSpannable(colorInt: Int): Spannable {
val spannable = SpannableString(this)
spannable.setSpan(ForegroundColorSpan(colorInt), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable
}
fun CharSequence?.asStrikethroughSpannable(): Spannable {
val spannable = SpannableString(this)
spannable.setSpan(StrikethroughSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable
}
/**
* Returns a new read-only list only of those given elements, that are not empty.
* Applies for CharSequence and descendants.
*/
fun <T : CharSequence> listOfNotEmpty(vararg elements: T): List<T> = elements.filterNot { it.isEmpty() }
fun List<CharSequence>.concat(delimiter: String? = null): CharSequence {
if (this.isEmpty()) {
return ""
}
if (this.size == 1) {
return this[0]
}
var spanned = false
for (piece in this) {
if (piece is Spanned) {
spanned = true
break
}
}
var first = true
if (spanned) {
val ssb = SpannableStringBuilder()
for (piece in this) {
if (!first && delimiter != null)
ssb.append(delimiter)
first = false
ssb.append(piece)
}
return SpannedString(ssb)
} else {
val sb = StringBuilder()
for (piece in this) {
if (!first && delimiter != null)
sb.append(delimiter)
first = false
sb.append(piece)
}
return sb.toString()
}
}
fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) {
text = context.getString(resid, formatArgs)
}

View File

@ -62,7 +62,7 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.v2.TimetableFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils

View File

@ -5,15 +5,77 @@
package pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.api
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
init {
for (lessonStr in rows) {
val lessons = rows.filterNot { it.isEmpty() }.map { it.split("|") }
for (lesson in lessons) {
val date = Date.fromYmd(lesson[2])
val startTime = Time.fromYmdHm(lesson[3])
val endTime = Time.fromYmdHm(lesson[4])
val id = date.combineWith(startTime) / 1000L
val subjectId = data.subjectList.singleOrNull { it.longName == lesson[5] }?.id ?: -1
val teacherId = data.teacherList.singleOrNull { it.fullNameLastFirst == (lesson[7]+" "+lesson[6]).fixName() }?.id ?: -1
val teamId = data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.id ?: -1
val classroom = lesson[11]
Lesson(data.profileId, id).also {
when (lesson[1]) {
"plan_lekcji", "lekcja" -> {
it.type = Lesson.TYPE_NORMAL
it.date = date
it.startTime = startTime
it.endTime = endTime
it.subjectId = subjectId
it.teacherId = teacherId
it.teamId = teamId
it.classroom = classroom
}
"lekcja_odwolana" -> {
it.type = Lesson.TYPE_CANCELLED
it.date = date
it.startTime = startTime
it.endTime = endTime
it.oldSubjectId = subjectId
//it.oldTeacherId = teacherId
it.oldTeamId = teamId
//it.oldClassroom = classroom
}
"zastepstwo" -> {
it.type = Lesson.TYPE_CHANGE
it.date = date
it.startTime = startTime
it.endTime = endTime
it.subjectId = subjectId
it.teacherId = teacherId
it.teamId = teamId
it.classroom = classroom
}
}
if (it.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LESSON_CHANGE,
it.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
System.currentTimeMillis()
))
}
data.lessonNewList += it
}
}
/*for (lessonStr in rows) {
if (lessonStr.isNotEmpty()) {
val lesson = lessonStr.split("|")
@ -76,9 +138,9 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
if (originalLesson == null) {
// original lesson doesn't exist, save a new addition
// TODO
/*if (!RegisterLessonChange.existsAddition(app.profile, registerLessonChange)) {
*//*if (!RegisterLessonChange.existsAddition(app.profile, registerLessonChange)) {
app.profile.timetable.addLessonAddition(registerLessonChange);
}*/
}*//*
} else {
// original lesson exists, so we need to compare them
if (!lessonChange.matches(originalLesson)) {
@ -108,6 +170,6 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
}
}
}
}
}*/
}
}

View File

@ -136,6 +136,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
var lessonsToRemove: DataRemoveModel? = null
val lessonList = mutableListOf<Lesson>()
val lessonChangeList = mutableListOf<LessonChange>()
val lessonNewList = mutableListOf<pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson>()
var gradesToRemove: DataRemoveModel? = null
val gradeList = mutableListOf<Grade>()
@ -195,6 +196,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
lessonList.clear()
lessonChangeList.clear()
lessonNewList.clear()
gradeList.clear()
noticeList.clear()
attendanceList.clear()
@ -282,6 +284,10 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
}
if (lessonChangeList.isNotEmpty())
db.lessonChangeDao().addAll(lessonChangeList)
if (lessonNewList.isNotEmpty()) {
db.timetableDao().clear(profile.id)
db.timetableDao() += lessonNewList
}
if (gradeList.isNotEmpty()) {
db.gradeDao().addAll(gradeList)
}

View File

@ -72,6 +72,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsenceTypeDao
import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherDao;
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team;
import pl.szczodrzynski.edziennik.data.db.modules.teams.TeamDao;
import pl.szczodrzynski.edziennik.data.db.modules.timetable.TimetableDao;
import pl.szczodrzynski.edziennik.utils.models.Date;
@Database(entities = {
@ -103,7 +104,8 @@ import pl.szczodrzynski.edziennik.utils.models.Date;
Classroom.class,
NoticeType.class,
AttendanceType.class,
Metadata.class}, version = 63)
pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson.class,
Metadata.class}, version = 64)
@TypeConverters({
ConverterTime.class,
ConverterDate.class,
@ -141,6 +143,7 @@ public abstract class AppDb extends RoomDatabase {
public abstract ClassroomDao classroomDao();
public abstract NoticeTypeDao noticeTypeDao();
public abstract AttendanceTypeDao attendanceTypeDao();
public abstract TimetableDao timetableDao();
public abstract MetadataDao metadataDao();
private static volatile AppDb INSTANCE;
@ -729,6 +732,37 @@ public abstract class AppDb extends RoomDatabase {
database.execSQL("ALTER TABLE profiles ADD COLUMN studentSchoolYear TEXT DEFAULT NULL");
}
};
private static final Migration MIGRATION_63_64 = new Migration(63, 64) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
//database.execSQL("ALTER TABLE lessons RENAME TO lessonsOld;");
database.execSQL("CREATE TABLE timetable (" +
"profileId INTEGER NOT NULL," +
"id INTEGER NOT NULL," +
"type INTEGER NOT NULL," +
"date TEXT DEFAULT NULL," +
"lessonNumber INTEGER DEFAULT NULL," +
"startTime TEXT DEFAULT NULL," +
"endTime TEXT DEFAULT NULL," +
"subjectId INTEGER DEFAULT NULL," +
"teacherId INTEGER DEFAULT NULL," +
"teamId INTEGER DEFAULT NULL," +
"classroom TEXT DEFAULT NULL," +
"oldDate TEXT DEFAULT NULL," +
"oldLessonNumber INTEGER DEFAULT NULL," +
"oldStartTime TEXT DEFAULT NULL," +
"oldEndTime TEXT DEFAULT NULL," +
"oldSubjectId INTEGER DEFAULT NULL," +
"oldTeacherId INTEGER DEFAULT NULL," +
"oldTeamId INTEGER DEFAULT NULL," +
"oldClassroom TEXT DEFAULT NULL," +
"PRIMARY KEY(id));");
database.execSQL("CREATE INDEX index_lessons_profileId_type_date ON timetable (profileId, type, date);");
database.execSQL("CREATE INDEX index_lessons_profileId_type_oldDate ON timetable (profileId, type, oldDate);");
}
};
public static AppDb getDatabase(final Context context) {
@ -789,7 +823,8 @@ public abstract class AppDb extends RoomDatabase {
MIGRATION_59_60,
MIGRATION_60_61,
MIGRATION_61_62,
MIGRATION_62_63
MIGRATION_62_63,
MIGRATION_63_64
)
.allowMainThreadQueries()
//.fallbackToDestructiveMigration()

View File

@ -4,11 +4,18 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
open class Lesson(val profileId: Int) {
@Entity(tableName = "timetable",
indices = [
Index(value = ["profileId", "type", "date"]),
Index(value = ["profileId", "type", "oldDate"])
])
open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
companion object {
const val TYPE_NORMAL = 0
const val TYPE_CANCELLED = 1
@ -17,15 +24,14 @@ open class Lesson(val profileId: Int) {
const val TYPE_SHIFTED_TARGET = 4 /* target lesson */
}
@ColumnInfo(name = "lessonType")
var type: Int = TYPE_NORMAL
var date: Date? = null
var lessonNumber: Int? = null
var startTime: Time? = null
var endTime: Time? = null
var teacherId: Long? = null
var subjectId: Long? = null
var teacherId: Long? = null
var teamId: Long? = null
var classroom: String? = null
@ -33,8 +39,58 @@ open class Lesson(val profileId: Int) {
var oldLessonNumber: Int? = null
var oldStartTime: Time? = null
var oldEndTime: Time? = null
var oldTeacherId: Long? = null
var oldSubjectId: Long? = null
var oldTeacherId: Long? = null
var oldTeamId: Long? = null
var oldClassroom: String? = null
}
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +
"type=$type, " +
"date=$date, " +
"lessonNumber=$lessonNumber, " +
"startTime=$startTime, " +
"endTime=$endTime, " +
"subjectId=$subjectId, " +
"teacherId=$teacherId, " +
"teamId=$teamId, " +
"classroom=$classroom, " +
"oldDate=$oldDate, " +
"oldLessonNumber=$oldLessonNumber, " +
"oldStartTime=$oldStartTime, " +
"oldEndTime=$oldEndTime, " +
"oldSubjectId=$oldSubjectId, " +
"oldTeacherId=$oldTeacherId, " +
"oldTeamId=$oldTeamId, " +
"oldClassroom=$oldClassroom)"
}
}
/*
DROP TABLE lessons;
DROP TABLE lessonChanges;
CREATE TABLE lessons (
profileId INTEGER NOT NULL,
type INTEGER NOT NULL,
date TEXT DEFAULT NULL,
lessonNumber INTEGER DEFAULT NULL,
startTime TEXT DEFAULT NULL,
endTime TEXT DEFAULT NULL,
teacherId INTEGER DEFAULT NULL,
subjectId INTEGER DEFAULT NULL,
teamId INTEGER DEFAULT NULL,
classroom TEXT DEFAULT NULL,
oldDate TEXT DEFAULT NULL,
oldLessonNumber INTEGER DEFAULT NULL,
oldStartTime TEXT DEFAULT NULL,
oldEndTime TEXT DEFAULT NULL,
oldTeacherId INTEGER DEFAULT NULL,
oldSubjectId INTEGER DEFAULT NULL,
oldTeamId INTEGER DEFAULT NULL,
oldClassroom TEXT DEFAULT NULL,
PRIMARY KEY(profileId)
);
*/

View File

@ -0,0 +1,63 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
var subjectName: String? = null
var teacherName: String? = null
var teamName: String? = null
var oldSubjectName: String? = null
var oldTeacherName: String? = null
var oldTeamName: String? = null
val displayDate: Date?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldDate
return date ?: oldDate
}
val displayStartTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldStartTime
return startTime ?: oldStartTime
}
val displayEndTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldEndTime
return endTime ?: oldEndTime
}
val displaySubjectName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldSubjectName
return subjectName ?: oldSubjectName
}
val displayTeacherName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldTeacherName
return teacherName ?: oldTeacherName
}
val displayTeamName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldTeamName
return teamName ?: oldTeamName
}
val displayClassroom: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldClassroom
return classroom ?: oldClassroom
}
// metadata
var seen: Boolean = false
var notified: Boolean = false
var addedDate: Long = 0
}

View File

@ -0,0 +1,41 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.utils.models.Date
@Dao
interface TimetableDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
operator fun plusAssign(lessonList: List<Lesson>)
@Query("DELETE FROM timetable WHERE profileId = :profileId")
fun clear(profileId: Int)
@Query("""
SELECT
timetable.*,
subjects.subjectLongName AS subjectName,
teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName,
teams.teamName AS teamName,
oldS.subjectLongName AS oldSubjectName,
oldT.teacherName ||" "|| oldT.teacherSurname AS oldTeacherName,
oldG.teamName AS oldTeamName,
metadata.seen, metadata.notified, metadata.addedDate
FROM timetable
LEFT JOIN subjects USING(profileId, subjectId)
LEFT JOIN teachers USING(profileId, teacherId)
LEFT JOIN teams USING(profileId, teamId)
LEFT JOIN subjects AS oldS ON timetable.profileId = oldS.profileId AND timetable.oldSubjectId = oldS.subjectId
LEFT JOIN teachers AS oldT ON timetable.profileId = oldT.profileId AND timetable.oldTeacherId = oldT.teacherId
LEFT JOIN teams AS oldG ON timetable.profileId = oldG.profileId AND timetable.oldTeamId = oldG.teamId
LEFT JOIN metadata ON id = thingId AND thingType = ${Metadata.TYPE_LESSON_CHANGE} AND metadata.profileId = timetable.profileId
WHERE timetable.profileId = :profileId AND (type != 3 AND date = :date) OR (type = 3 AND oldDate = :date)
""")
fun getForDate(profileId: Int, date: Date) : LiveData<List<LessonFull>>
}

View File

@ -0,0 +1,99 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable.v2
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
class TimetableFragment : Fragment() {
companion object {
private const val TAG = "TimetableFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2Binding
private var fabShown = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
if (context == null)
return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false)
// activity, context and profile is valid
b = FragmentTimetableV2Binding.inflate(inflater)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
val items = mutableListOf<Date>()
val monthDayCount = listOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
val today = Date.getToday().value
val yearStart = app.profile.dateSemester1Start?.clone() ?: return
val yearEnd = app.profile.dateYearEnd ?: return
while (yearStart.value <= yearEnd.value) {
items += yearStart.clone()
var maxDays = monthDayCount[yearStart.month-1]
if (yearStart.month == 2 && yearStart.isLeap)
maxDays++
yearStart.day++
if (yearStart.day > maxDays) {
yearStart.day = 1
yearStart.month++
}
if (yearStart.month > 12) {
yearStart.month = 1
yearStart.year++
}
}
val pagerAdapter = TimetablePagerAdapter(fragmentManager ?: return, items)
b.viewPager.adapter = pagerAdapter
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
activity.navView.bottomBar.fabEnable = items[position].value != today
if (activity.navView.bottomBar.fabEnable && !fabShown) {
activity.gainAttentionFAB()
fabShown = true
}
}
})
b.tabLayout.setUpWithViewPager(b.viewPager)
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, false)
//activity.navView.bottomBar.fabEnable = true
activity.navView.bottomBar.fabExtendedText = getString(R.string.timetable_today)
activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon.cmd_calendar_today
activity.navView.setFabOnClickListener(View.OnClickListener {
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, true)
})
}
}

View File

@ -0,0 +1,29 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable.v2
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import pl.szczodrzynski.edziennik.utils.models.Date
class TimetablePagerAdapter(val fragmentManager: FragmentManager, val items: List<Date>) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
companion object {
private const val TAG = "TimetablePagerAdapter"
}
override fun getItem(position: Int): Fragment {
return pl.szczodrzynski.edziennik.ui.modules.timetable.v2.day.TimetableDayFragment(items[position])
/*return TimetableDayFragment().apply {
arguments = Bundle().also {
it.putLong("date", items[position].value.toLong())
}
}*/
}
override fun getCount(): Int {
return items.size
}
override fun getPageTitle(position: Int): CharSequence? {
return items[position].formattedStringShort
}
}

View File

@ -0,0 +1,185 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable.v2.day
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import com.linkedin.android.tachyon.DayView
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2DayBinding
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.getColorFromAttr
import java.util.*
class TimetableDayFragment(val date: Date) : Fragment() {
companion object {
private const val TAG = "TimetableDayFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTimetableV2DayBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
if (context == null)
return null
app = activity.application as App
b = FragmentTimetableV2DayBinding.inflate(inflater)
Log.d(TAG, "onCreateView, date=$date")
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
Log.d(TAG, "onViewCreated, date=$date")
b.date.text = date.formattedString
// Inflate a label view for each hour the day view will display
val hourLabelViews = ArrayList<View>()
for (i in b.day.startHour..b.day.endHour) {
val hourLabelView = layoutInflater.inflate(R.layout.timetable_hour_label, b.day, false) as TextView
hourLabelView.text = "$i:00"
hourLabelViews.add(hourLabelView)
}
b.day.setHourLabelViews(hourLabelViews)
app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer<List<LessonFull>> { lessons ->
buildLessonViews(lessons)
})
}
private fun buildLessonViews(lessons: List<LessonFull>) {
val eventViews = mutableListOf<View>()
val eventTimeRanges = mutableListOf<DayView.EventTimeRange>()
// Reclaim all of the existing event views so we can reuse them if needed, this process
// can be useful if your day view is hosted in a recycler view for example
val recycled = b.day.removeEventViews()
var remaining = recycled?.size ?: 0
val arrowRight = ""
val bullet = ""
val colorSecondary = getColorFromAttr(activity, android.R.attr.textColorSecondary)
for (lesson in lessons) {
val startTime = lesson.displayStartTime ?: continue
val endTime = lesson.displayEndTime ?: continue
// Try to recycle an existing event view if there are enough left, otherwise inflate
// a new one
val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, b.day, false))
?: continue
val lb = TimetableLessonBinding.bind(eventView)
eventViews += eventView
eventView.tag = lesson
eventView.setOnClickListener {
Log.d(TAG, "Clicked ${it.tag}")
}
val timeRange = "${startTime.stringHM} - ${endTime.stringHM}".asColoredSpannable(colorSecondary)
// teacher
val teacherInfo = if (lesson.teacherId != null && lesson.teacherId == lesson.oldTeacherId)
lesson.teacherName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) }
lesson.teacherName?.let { add(it) }
}.concat(arrowRight)
// team
val teamInfo = if (lesson.teamId != null && lesson.teamId == lesson.oldTeamId)
lesson.teamName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) }
lesson.teamName?.let { add(it) }
}.concat(arrowRight)
// classroom
val classroomInfo = if (lesson.classroom != null && lesson.classroom == lesson.oldClassroom)
lesson.classroom ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) }
lesson.classroom?.let { add(it) }
}.concat(arrowRight)
lb.subjectName.text = lesson.displaySubjectName?.let { if (lesson.type == Lesson.TYPE_CANCELLED) it.asStrikethroughSpannable().asColoredSpannable(colorSecondary) else it }
lb.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet)
lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet)
//lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
when (lesson.type) {
Lesson.TYPE_NORMAL -> {
lb.annotation.visibility = View.GONE
}
Lesson.TYPE_CANCELLED -> {
lb.annotation.visibility = View.VISIBLE
lb.annotation.setText(R.string.timetable_lesson_cancelled)
lb.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(activity, R.attr.timetable_lesson_cancelled_color),
PorterDuff.Mode.SRC_ATOP
)
//lb.subjectName.typeface = Typeface.DEFAULT
}
Lesson.TYPE_CHANGE -> {
lb.annotation.visibility = View.VISIBLE
if (lesson.subjectId != lesson.oldSubjectId && lesson.teacherId != lesson.oldTeacherId) {
lb.annotation.setText(
R.string.timetable_lesson_change_format,
"${lesson.oldSubjectName ?: "?"}, ${lesson.oldTeacherName ?: "?"}"
)
}
else if (lesson.subjectId != lesson.oldSubjectId) {
lb.annotation.setText(
R.string.timetable_lesson_change_format,
lesson.oldSubjectName ?: "?"
)
}
else if (lesson.teacherId != lesson.oldTeacherId) {
lb.annotation.setText(
R.string.timetable_lesson_change_format,
lesson.oldTeacherName ?: "?"
)
}
else {
lb.annotation.setText(R.string.timetable_lesson_change)
}
lb.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(activity, R.attr.timetable_lesson_cancelled_color),
PorterDuff.Mode.SRC_ATOP
)
}
}
// The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here
val startMinute = 60 * (lesson.displayStartTime?.hour ?: 0) + (lesson.displayStartTime?.minute ?: 0)
val endMinute = startMinute + 45
eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute))
}
b.day.setEventViews(eventViews, eventTimeRanges)
b.dayScroll.scrollTo(0, b.day.firstEventTop)
}
}

View File

@ -182,6 +182,10 @@ public class Date implements Comparable<Date> {
}
}
public boolean isLeap() {
return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0);
}
public static Date getToday()
{
Calendar cal = Calendar.getInstance();

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#000000" />
<corners android:radius="4dp" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/bg_rounded_edittext_pressed" />
<item android:state_focused="true" android:drawable="@drawable/bg_rounded_edittext_pressed" />
<item android:state_selected="true" android:drawable="@drawable/bg_rounded_edittext_pressed" />
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<stroke android:color="@color/dividerColor" android:width="1dp" />
<solid android:color="#DCDCDC" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:shape="rectangle">
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="#2196f3" tools:color="?timetable_lesson_cancelled_color" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="@color/colorSurface_4dp" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<stroke android:width="1dp" android:color="#1e000000" />
</shape>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface"
style="@style/Widget.MaterialComponents.AppBarLayout.Surface">
<com.nshmura.recyclertablayout.RecyclerTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorSurface_6dp"
app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab"
app:rtl_tabIndicatorColor="?colorPrimary"
app:rtl_tabMinWidth="90dp"
app:rtl_tabMaxWidth="300dp"
app:rtl_tabSelectedTextColor="?colorPrimary"
app:rtl_tabPaddingStart="16dp"
app:rtl_tabPaddingEnd="16dp"
app:rtl_tabPaddingTop="12dp"
app:rtl_tabPaddingBottom="12dp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_blank_fragment"
android:textSize="24sp"
android:visibility="gone"/>
<ScrollView
android:id="@+id/dayScroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.linkedin.android.tachyon.DayView
android:id="@+id/day"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:dividerHeight="1dp"
app:endHour="18"
app:eventMargin="2dp"
app:halfHourDividerColor="#e0e0e0"
app:halfHourHeight="60dp"
app:hourDividerColor="#b0b0b0"
app:hourLabelMarginEnd="10dp"
app:hourLabelWidth="40dp"
app:startHour="5" />
</ScrollView>
</LinearLayout>
</layout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 LinkedIn Corporation -->
<!-- All Rights Reserved. -->
<!-- -->
<!-- Licensed under the BSD 2-Clause License (the "License"). See License in the project root -->
<!-- for license information. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Base.TextAppearance.AppCompat.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewEnd"
tools:text="1 PM" />

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:foreground="@drawable/bg_rounded_ripple_4dp"
tools:padding="32dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_height="90dp"
android:background="?timetable_lesson_bg"
android:orientation="vertical"
android:paddingBottom="4dp">
<TextView
android:id="@+id/annotation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/timetable_lesson_annotation"
android:fontFamily="sans-serif-condensed"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:textColor="#000"
android:textSize="12sp"
android:textStyle="italic"
android:text="@string/timetable_lesson_cancelled"
android:visibility="gone"
tools:visibility="visible"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingTop="4dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/subjectName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
android:maxLines="2"
android:textSize="16sp"
android:textStyle="bold"
tools:text="pracownia urządzeń techniki komputerowej nazwa przedmiotu jest bardzo długa i pewnie się tu nie zmieści w ogóle" />
<ImageView
android:id="@+id/attendanceIcon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_weight="0"
tools:srcCompat="@sample/check"
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/imageView4"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_weight="0"
app:srcCompat="@drawable/bg_circle"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="bottom"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/detailsFirst"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:singleLine="true"
tools:text="8:10 - 8:55 • 015 językowa → 016 językowa" />
<TextView
android:id="@+id/detailsSecond"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:singleLine="true"
android:textAppearance="@style/NavView.TextView.Helper"
android:textSize="12sp"
tools:text="Paweł Informatyczny • 2b3T n1" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</layout>

View File

@ -3,4 +3,8 @@
<attr name="colorSection" format="color" />
<attr name="cardBackgroundDimmed" format="color" />
<attr name="cardBackgroundHighlight" format="color" />
<attr name="timetable_lesson_bg" format="reference" />
<attr name="timetable_lesson_cancelled_color" format="color" />
<attr name="timetable_lesson_change_color" format="color" />
<attr name="timetable_lesson_shifted_color" format="color" />
</resources>

View File

@ -985,4 +985,10 @@
<string name="app_manager_open_failed">Nie udało się otworzyć ustawień</string>
<string name="edziennik_notification_api_notify_title">Tworzenie powiadomień</string>
<string name="login_librus_captcha_title">Librus - logowanie</string>
<string name="timetable_today">Dzisiaj</string>
<string name="timetable_lesson_cancelled">Lekcja odwołana</string>
<string name="timetable_lesson_change">Zastępstwo</string>
<string name="timetable_lesson_change_format">Zastępstwo: zamiast %s</string>
<string name="timetable_lesson_shifted_same_day">Lekcja przeniesiona na godz. %s</string>
<string name="timetable_lesson_shifted_other_day">Lekcja przeniesiona na %s, godz. %s</string>
</resources>

View File

@ -1,4 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="colorAccent">@color/colorAccent</item>
@ -95,6 +95,11 @@
<item name="mal_color_secondary">?android:textColorSecondary</item>
<item name="mal_card_background">?colorSurface</item>
<item name="mal_divider_color">@color/dividerColor</item>
<item name="timetable_lesson_bg">@drawable/timetable_lesson_bg_light</item>
<item name="timetable_lesson_cancelled_color">#9f9f9f</item>
<item name="timetable_lesson_change_color">#ffb300</item>
<item name="timetable_lesson_shifted_color">#4caf50</item>
</style>
<style name="AppTheme.Dark" parent="NavView.Dark">
<item name="colorPrimary">#64b5f6</item>
@ -119,6 +124,11 @@
<item name="mal_color_secondary">@color/secondaryTextDark</item>
<item name="mal_card_background">?colorSurface</item>
<item name="mal_divider_color">@color/dividerColor</item>
<item name="timetable_lesson_bg">@drawable/timetable_lesson_bg_dark</item>
<item name="timetable_lesson_cancelled_color">#838383</item>
<item name="timetable_lesson_change_color">#ffb300</item>
<item name="timetable_lesson_shifted_color">#4caf50</item>
</style>