diff --git a/app/build.gradle b/app/build.gradle index 73121d5a..24c91a3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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() diff --git a/app/sampledata/check/ic_check.xml b/app/sampledata/check/ic_check.xml new file mode 100644 index 00000000..f621023c --- /dev/null +++ b/app/sampledata/check/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index 7ea38470..bb471068 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -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) \ No newline at end of file +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 listOfNotEmpty(vararg elements: T): List = elements.filterNot { it.isEmpty() } + +fun List.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) +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index a2dbdeb0..e84f210f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -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 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/mobidziennik/data/api/MobidziennikApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/mobidziennik/data/api/MobidziennikApiTimetable.kt index ce938865..3f529a7c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/mobidziennik/data/api/MobidziennikApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/mobidziennik/data/api/MobidziennikApiTimetable.kt @@ -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) { 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) { 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) { } } } - } + }*/ } } \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/models/Data.kt index 394aaeaf..bac36618 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/api/v2/models/Data.kt @@ -136,6 +136,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore) var lessonsToRemove: DataRemoveModel? = null val lessonList = mutableListOf() val lessonChangeList = mutableListOf() + val lessonNewList = mutableListOf() var gradesToRemove: DataRemoveModel? = null val gradeList = mutableListOf() @@ -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) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.java index 800dcc69..046c78e8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.java @@ -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() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt index 0feaaa6f..abc75820 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/Lesson.kt @@ -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 -} \ No newline at end of file + + 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) +); +*/ \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/LessonFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/LessonFull.kt new file mode 100644 index 00000000..ee573ac2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/LessonFull.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/TimetableDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/TimetableDao.kt new file mode 100644 index 00000000..ee565eff --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/modules/timetable/TimetableDao.kt @@ -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) + + @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> +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt new file mode 100644 index 00000000..b29a7e72 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetableFragment.kt @@ -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() + + 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) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetablePagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetablePagerAdapter.kt new file mode 100644 index 00000000..84d7a004 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/TimetablePagerAdapter.kt @@ -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) : 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/day/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/day/TimetableDayFragment.kt new file mode 100644 index 00000000..92044328 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/v2/day/TimetableDayFragment.kt @@ -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() + 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> { lessons -> + buildLessonViews(lessons) + }) + } + + private fun buildLessonViews(lessons: List) { + val eventViews = mutableListOf() + val eventTimeRanges = mutableListOf() + + // 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().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().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().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) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java index 3b69cdc9..a4ab40fe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java @@ -182,6 +182,10 @@ public class Date implements Comparable { } } + public boolean isLeap() { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + public static Date getToday() { Calendar cal = Calendar.getInstance(); diff --git a/app/src/main/res/drawable-v21/bg_rounded_ripple_4dp.xml b/app/src/main/res/drawable-v21/bg_rounded_ripple_4dp.xml new file mode 100644 index 00000000..662d846f --- /dev/null +++ b/app/src/main/res/drawable-v21/bg_rounded_ripple_4dp.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rounded_ripple_4dp.xml b/app/src/main/res/drawable/bg_rounded_ripple_4dp.xml new file mode 100644 index 00000000..ca5a7911 --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_ripple_4dp.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rounded_ripple_4dp_pressed.xml b/app/src/main/res/drawable/bg_rounded_ripple_4dp_pressed.xml new file mode 100644 index 00000000..aadf2255 --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_ripple_4dp_pressed.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/timetable_lesson_annotation.xml b/app/src/main/res/drawable/timetable_lesson_annotation.xml new file mode 100644 index 00000000..b22dc045 --- /dev/null +++ b/app/src/main/res/drawable/timetable_lesson_annotation.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/timetable_lesson_bg_dark.xml b/app/src/main/res/drawable/timetable_lesson_bg_dark.xml new file mode 100644 index 00000000..718c9f4e --- /dev/null +++ b/app/src/main/res/drawable/timetable_lesson_bg_dark.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/timetable_lesson_bg_light.xml b/app/src/main/res/drawable/timetable_lesson_bg_light.xml new file mode 100644 index 00000000..7a17b6cd --- /dev/null +++ b/app/src/main/res/drawable/timetable_lesson_bg_light.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_timetable_v2.xml b/app/src/main/res/layout/fragment_timetable_v2.xml new file mode 100644 index 00000000..78893ddf --- /dev/null +++ b/app/src/main/res/layout/fragment_timetable_v2.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_timetable_v2_day.xml b/app/src/main/res/layout/fragment_timetable_v2_day.xml new file mode 100644 index 00000000..9912a5ef --- /dev/null +++ b/app/src/main/res/layout/fragment_timetable_v2_day.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/timetable_hour_label.xml b/app/src/main/res/layout/timetable_hour_label.xml new file mode 100644 index 00000000..4daa29c6 --- /dev/null +++ b/app/src/main/res/layout/timetable_hour_label.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/timetable_lesson.xml b/app/src/main/res/layout/timetable_lesson.xml new file mode 100644 index 00000000..4b857f8f --- /dev/null +++ b/app/src/main/res/layout/timetable_lesson.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index b7deaa68..972b8254 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,4 +3,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dafa8099..dd006b4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -985,4 +985,10 @@ Nie udało się otworzyć ustawień Tworzenie powiadomień Librus - logowanie + Dzisiaj + Lekcja odwołana + Zastępstwo + Zastępstwo: zamiast %s + Lekcja przeniesiona na godz. %s + Lekcja przeniesiona na %s, godz. %s diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 116c7a40..6e2b64de 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,4 @@ - +