diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8cbe1a6..f50c002 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,8 @@ android { applicationId = "ru.n08i40k.polytechnic.next" minSdk = 26 targetSdk = 35 - versionCode = 19 - versionName = "2.0.3" + versionCode = 20 + versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -128,6 +128,9 @@ dependencies { implementation(libs.androidx.navigation.compose) testImplementation(libs.junit) + testImplementation(libs.mockito.kotlin) + + androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt index 62a952b..4d75c5b 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt @@ -1,8 +1,10 @@ package ru.n08i40k.polytechnic.next.data.schedule import ru.n08i40k.polytechnic.next.data.MyResult -import ru.n08i40k.polytechnic.next.model.Group +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher interface ScheduleRepository { - suspend fun getGroup(): MyResult + suspend fun getGroup(): MyResult + + suspend fun getTeacher(name: String): MyResult } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt index e063068..73f75fc 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt @@ -11,7 +11,7 @@ import kotlinx.datetime.toInstant import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository import ru.n08i40k.polytechnic.next.model.Day -import ru.n08i40k.polytechnic.next.model.Group +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.LessonTime import ru.n08i40k.polytechnic.next.model.LessonType @@ -31,14 +31,15 @@ private fun genBreak(start: Instant, end: Instant): Lesson { start, end ), - subGroups = listOf() + subGroups = listOf(), + group = null ) } class FakeScheduleRepository : ScheduleRepository { @Suppress("SpellCheckingInspection") companion object { - val exampleGroup = Group( + val exampleGroup = GroupOrTeacher( name = "ИС-214/23", days = arrayListOf( Day( name = "Понедельник", @@ -52,7 +53,8 @@ class FakeScheduleRepository : ScheduleRepository { genLocalDateTime(8, 30), genLocalDateTime(8, 40), ), - subGroups = listOf() + subGroups = listOf(), + group = null ), genBreak( genLocalDateTime(8, 40), @@ -66,7 +68,8 @@ class FakeScheduleRepository : ScheduleRepository { genLocalDateTime(8, 45), genLocalDateTime(9, 15), ), - subGroups = listOf() + subGroups = listOf(), + group = null ), genBreak( genLocalDateTime(9, 15), @@ -86,7 +89,8 @@ class FakeScheduleRepository : ScheduleRepository { number = 1, cabinet = "43" ) - ) + ), + group = null ), genBreak( genLocalDateTime(10, 45), @@ -111,7 +115,8 @@ class FakeScheduleRepository : ScheduleRepository { number = 2, cabinet = "44" ), - ) + ), + group = null ), genBreak( genLocalDateTime(12, 15), @@ -136,7 +141,118 @@ class FakeScheduleRepository : ScheduleRepository { number = 2, cabinet = "41" ), - ) + ), + group = null + ), + ) + ) + ) + ) + + val exampleTeacher = GroupOrTeacher( + name = "Хомченко Н.Е.", days = arrayListOf( + Day( + name = "Понедельник", + date = LocalDateTime.now().toInstant(TimeZone.currentSystemDefault()), + lessons = listOf( + Lesson( + type = LessonType.ADDITIONAL, + defaultRange = null, + name = "Линейка", + time = LessonTime( + genLocalDateTime(8, 30), + genLocalDateTime(8, 40), + ), + subGroups = listOf(), + group = "ИС-214/23" + ), + genBreak( + genLocalDateTime(8, 40), + genLocalDateTime(8, 45), + ), + Lesson( + type = LessonType.ADDITIONAL, + defaultRange = null, + name = "Разговор о важном", + time = LessonTime( + genLocalDateTime(8, 45), + genLocalDateTime(9, 15), + ), + subGroups = listOf(), + group = "ИС-214/23" + ), + genBreak( + genLocalDateTime(9, 15), + genLocalDateTime(9, 25), + ), + Lesson( + type = LessonType.DEFAULT, + defaultRange = listOf(1, 1), + name = "МДК.05.01 Проектирование и дизайн информационных систем", + time = LessonTime( + genLocalDateTime(9, 25), + genLocalDateTime(10, 45), + ), + subGroups = listOf( + SubGroup( + teacher = "Ивашова А.Н.", + number = 1, + cabinet = "43" + ) + ), + group = "ИС-214/23" + ), + genBreak( + genLocalDateTime(10, 45), + genLocalDateTime(10, 55), + ), + Lesson( + type = LessonType.DEFAULT, + defaultRange = listOf(2, 2), + name = "Основы проектирования баз данных", + time = LessonTime( + genLocalDateTime(10, 55), + genLocalDateTime(12, 15), + ), + subGroups = listOf( + SubGroup( + teacher = "Чинарева Е.А.", + number = 1, + cabinet = "21" + ), + SubGroup( + teacher = "Ивашова А.Н.", + number = 2, + cabinet = "44" + ), + ), + group = "ИС-214/23" + ), + genBreak( + genLocalDateTime(12, 15), + genLocalDateTime(12, 35), + ), + Lesson( + type = LessonType.DEFAULT, + defaultRange = listOf(3, 3), + name = "Операционные системы и среды", + time = LessonTime( + genLocalDateTime(12, 35), + genLocalDateTime(13, 55), + ), + subGroups = listOf( + SubGroup( + teacher = "Сергачева А.О.", + number = 1, + cabinet = "42" + ), + SubGroup( + teacher = "Воронцева Н.В.", + number = 2, + cabinet = "41" + ), + ), + group = "ИС-214/23" ), ) ) @@ -144,11 +260,12 @@ class FakeScheduleRepository : ScheduleRepository { ) } - private val group = MutableStateFlow(exampleGroup) + private val group = MutableStateFlow(exampleGroup) + private val teacher = MutableStateFlow(exampleTeacher) private var updateCounter: Int = 0 - override suspend fun getGroup(): MyResult { + override suspend fun getGroup(): MyResult { return withContext(Dispatchers.IO) { delay(1500) if (updateCounter++ % 3 == 0) MyResult.Failure( @@ -157,4 +274,14 @@ class FakeScheduleRepository : ScheduleRepository { else MyResult.Success(group.value!!) } } + + override suspend fun getTeacher(name: String): MyResult { + return withContext(Dispatchers.IO) { + delay(1500) + if (updateCounter++ % 3 == 0) MyResult.Failure( + IllegalStateException() + ) + else MyResult.Success(teacher.value!!) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt index 528908c..20adb34 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt @@ -5,12 +5,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository -import ru.n08i40k.polytechnic.next.model.Group +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet +import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacher import ru.n08i40k.polytechnic.next.network.tryFuture class RemoteScheduleRepository(private val context: Context) : ScheduleRepository { - override suspend fun getGroup(): MyResult = + override suspend fun getGroup(): MyResult = withContext(Dispatchers.IO) { val response = tryFuture { ScheduleGet( @@ -25,4 +26,21 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor is MyResult.Success -> MyResult.Success(response.data.group) } } + + override suspend fun getTeacher(name: String): MyResult = + withContext(Dispatchers.IO) { + val response = tryFuture { + ScheduleGetTeacher( + context, + name, + it, + it + ) + } + + when (response) { + is MyResult.Failure -> response + is MyResult.Success -> MyResult.Success(response.data.teacher) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt index 1b6dc7c..3bbe795 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt @@ -15,11 +15,13 @@ import ru.n08i40k.polytechnic.next.utils.now @Parcelize @Suppress("unused", "MemberVisibilityCanBePrivate") @Serializable -class Day( +data class Day( val name: String, + @Serializable(with = InstantAsLongSerializer::class) @SerialName("date") private val dateMillis: Long, + val lessons: List ) : Parcelable { constructor(name: String, date: Instant, lessons: List) : this( @@ -29,30 +31,19 @@ class Day( val date: Instant get() = Instant.fromEpochMilliseconds(dateMillis) - fun distanceToNextByLocalDateTime(from: LocalDateTime): Pair? { - val toIdx = lessons - .map { it.time.start } - .indexOfFirst { it.dateTime > from } + fun distanceToNext(from: LocalDateTime): Pair? { + val nextIndex = lessons.map { it.time.start }.indexOfFirst { it.dateTime >= from } - if (toIdx == -1) - return null + if (nextIndex == -1) return null - return Pair(toIdx, lessons[toIdx].time.start.dayMinutes - from.dayMinutes) + return Pair(nextIndex, lessons[nextIndex].time.start.dayMinutes - from.dayMinutes) } - fun distanceToNextByIdx(from: Int? = null): Pair? { - val fromLesson = if (from != null) lessons[from] else null + fun distanceToNext(fromIndex: Int? = null): Pair? { + val fromLesson = fromIndex?.let { lessons[fromIndex] } + val fromTime = fromLesson?.time?.end?.dateTime ?: LocalDateTime.now() - if (from != null && fromLesson == null) - throw NullPointerException("Lesson (by given index) and it's time should be non-null!") - - val fromTime = - if (from != null) - fromLesson!!.time.end.dateTime - else - LocalDateTime.now() - - return distanceToNextByLocalDateTime(fromTime) + return distanceToNext(fromTime) } // current @@ -63,8 +54,7 @@ class Day( for (lessonIdx in lessons.indices) { val lesson = lessons[lessonIdx] - if (lesson.time.start.dateTime <= now && now < lesson.time.end.dateTime) - return lessonIdx + if (lesson.time.start.dateTime <= now && now < lesson.time.end.dateTime) return lessonIdx } return null diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt similarity index 96% rename from app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt index 0e6a42b..afcab34 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt @@ -8,7 +8,7 @@ import java.util.Calendar @Suppress("MemberVisibilityCanBePrivate") @Parcelize @Serializable -data class Group( +data class GroupOrTeacher( val name: String, val days: List ) : Parcelable { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt index afbdd2f..4defb88 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt @@ -15,6 +15,7 @@ data class Lesson( val defaultRange: List?, val name: String?, val time: LessonTime, + val group: String? = null, val subGroups: List ) : Parcelable { val duration: Int @@ -26,6 +27,10 @@ data class Lesson( } fun getNameAndCabinetsShort(context: Context): String { + val name = + if (type == LessonType.BREAK) context.getString(R.string.lesson_break) + else this.name + val limitedName = name!! limit 15 val cabinets = subGroups.map { it.cabinet } diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt index 2954e58..d14c6d1 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt @@ -4,7 +4,7 @@ import android.content.Context import com.android.volley.Response import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import ru.n08i40k.polytechnic.next.model.Group +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.network.request.CachedRequest class ScheduleGet( @@ -18,13 +18,10 @@ class ScheduleGet( { listener.onResponse(Json.decodeFromString(it)) }, errorListener ) { - @Serializable - data class RequestDto(val name: String) - @Serializable data class ResponseDto( val updatedAt: String, - val group: Group, + val group: GroupOrTeacher, val updated: ArrayList, ) } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt new file mode 100644 index 0000000..508999e --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt @@ -0,0 +1,28 @@ +package ru.n08i40k.polytechnic.next.network.request.schedule + +import android.content.Context +import com.android.volley.Response +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher +import ru.n08i40k.polytechnic.next.network.request.CachedRequest + +class ScheduleGetTeacher( + context: Context, + teacher: String, + listener: Response.Listener, + errorListener: Response.ErrorListener? = null +) : CachedRequest( + context, + Method.GET, + "v2/schedule/teacher/$teacher", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener +) { + @Serializable + data class ResponseDto( + val updatedAt: String, + val teacher: GroupOrTeacher, + val updated: ArrayList, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt new file mode 100644 index 0000000..5622311 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt @@ -0,0 +1,24 @@ +package ru.n08i40k.polytechnic.next.network.request.schedule + +import android.content.Context +import com.android.volley.Response +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import ru.n08i40k.polytechnic.next.network.RequestBase + +class ScheduleGetTeacherNames( + context: Context, + listener: Response.Listener, + errorListener: Response.ErrorListener? = null +) : RequestBase( + context, + Method.GET, + "v2/schedule/teacher-names", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener +) { + @Serializable + data class ResponseDto( + val names: ArrayList, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt index c7ab817..9333eb3 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt @@ -16,20 +16,19 @@ import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.model.Day -import ru.n08i40k.polytechnic.next.model.Group -import ru.n08i40k.polytechnic.next.model.Lesson +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.utils.dayMinutes import ru.n08i40k.polytechnic.next.utils.fmtAsClock import ru.n08i40k.polytechnic.next.utils.getDayMinutes +import ru.n08i40k.polytechnic.next.utils.now import java.util.Calendar import java.util.logging.Logger -@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") class CurrentLessonViewService : Service() { companion object { private const val NOTIFICATION_STATUS_ID = 1337 private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1 - private const val UPDATE_INTERVAL = 60_000L + private const val UPDATE_INTERVAL = 1_000L suspend fun startService(application: PolytechnicApplication) { if (!application.hasNotificationPermission()) @@ -59,61 +58,32 @@ class CurrentLessonViewService : Service() { } } - private var day: Day? = null + private lateinit var day: Day private val handler = Handler(Looper.getMainLooper()) + private val updateRunnable = object : Runnable { override fun run() { - val logger = Logger.getLogger("CLV.updateRunnable") + val (currentIndex, currentLesson) = day.currentKV ?: (null to null) + val (nextIndex, _) = day.distanceToNext(currentIndex) + ?: (null to null) - if (day == null || day!!.lessons.isEmpty()) { - logger.warning("Stopping, because day is null or empty!") - stopSelf() - return - } - - val currentMinutes = Calendar.getInstance() - .get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance() - .get(Calendar.MINUTE) - - val currentLessonEntry = day!!.currentKV - val currentLessonIdx: Int? = currentLessonEntry?.first - val currentLesson: Lesson? = currentLessonEntry?.second - - val nextLessonEntry = day!!.distanceToNextByIdx(currentLessonIdx) - val nextLesson = - if (nextLessonEntry == null) - null - else - day!!.lessons[nextLessonEntry.first] + val nextLesson = nextIndex?.let { day.lessons[nextIndex] } if (currentLesson == null && nextLesson == null) { - val notification = NotificationCompat - .Builder(applicationContext, NotificationChannels.LESSON_VIEW) - .setSmallIcon(R.drawable.schedule) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setContentTitle(getString(R.string.lessons_end_notification_title)) - .setContentText(getString(R.string.lessons_end_notification_description)) - .build() - getNotificationManager().notify(NOTIFICATION_END_ID, notification) - - stopSelf() + onLessonsEnd() return } - val firstLessonIdx = - day!!.distanceToNextByLocalDateTime(LocalDateTime(1, 1, 1, 0, 0))?.first - ?: throw NullPointerException("Is this even real?") - val distanceToFirst = day!!.lessons[firstLessonIdx]!!.time!!.start.dayMinutes - currentMinutes + handler.postDelayed(this, UPDATE_INTERVAL) - val currentLessonDelay = - if (currentLesson == null) // Если эта пара - перемена, то конец перемены через (результат getDistanceToNext) - nextLessonEntry!!.second - else // Если эта пара - обычная пара, то конец пары через (конец этой пары - текущее кол-во минут) - currentLesson.time!!.end.dayMinutes - currentMinutes + val context = this@CurrentLessonViewService + val currentMinutes = LocalDateTime.now().dayMinutes + + val distanceToFirst = day.first!!.time.start.dayMinutes - currentMinutes val currentLessonName = - currentLesson?.getNameAndCabinetsShort(this@CurrentLessonViewService) + currentLesson?.getNameAndCabinetsShort(context) ?: run { if (distanceToFirst > 0) getString(R.string.lessons_not_started) @@ -122,41 +92,42 @@ class CurrentLessonViewService : Service() { } val nextLessonName = - if (currentLesson == null) // Если текущая пара - перемена - nextLesson!!.getNameAndCabinetsShort(this@CurrentLessonViewService) - else if (nextLesson == null) // Если текущая пара - последняя - getString(R.string.lessons_end) - else // Если после текущей пары есть ещё пара(ы) - getString(R.string.lesson_break) + nextLesson?.getNameAndCabinetsShort(context) ?: getString(R.string.lessons_end) - val nextLessonTotal = - if (currentLesson == null) - nextLesson!!.time!!.start - else - currentLesson.time!!.end + val nextLessonIn = + (currentLesson?.time?.end ?: nextLesson!!.time.start).dayMinutes val notification = createNotification( getString( - if (distanceToFirst > 0) - R.string.waiting_for_day_start_notification_title - else - R.string.lesson_going_notification_title, - currentLessonDelay / 60, - currentLessonDelay % 60 + if (distanceToFirst > 0) R.string.waiting_for_day_start_notification_title + else R.string.lesson_going_notification_title, + (nextLessonIn - currentMinutes) / 60, + (nextLessonIn - currentMinutes) % 60 ), getString( R.string.lesson_going_notification_description, currentLessonName, - nextLessonTotal.dayMinutes.fmtAsClock(), + nextLessonIn.fmtAsClock(), nextLessonName, ) ) getNotificationManager().notify(NOTIFICATION_STATUS_ID, notification) - - handler.postDelayed(this, UPDATE_INTERVAL) } } + private fun onLessonsEnd() { + val notification = NotificationCompat + .Builder(applicationContext, NotificationChannels.LESSON_VIEW) + .setSmallIcon(R.drawable.schedule) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentTitle(getString(R.string.lessons_end_notification_title)) + .setContentText(getString(R.string.lessons_end_notification_description)) + .build() + getNotificationManager().notify(NOTIFICATION_END_ID, notification) + + stopSelf() + } + private fun createNotification( title: String? = null, description: String? = null @@ -176,7 +147,7 @@ class CurrentLessonViewService : Service() { return getSystemService(NOTIFICATION_SERVICE) as NotificationManager } - private fun updateSchedule(group: Group?) { + private fun updateSchedule(group: GroupOrTeacher?) { val logger = Logger.getLogger("CLV") if (group == null) { @@ -223,7 +194,7 @@ class CurrentLessonViewService : Service() { updateSchedule( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra("group", Group::class.java) + intent.getParcelableExtra("group", GroupOrTeacher::class.java) } else { @Suppress("DEPRECATION") intent.getParcelableExtra("group") diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt index feda080..1911aa9 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt @@ -5,6 +5,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.DateRange +import androidx.compose.material.icons.filled.Person import androidx.compose.ui.graphics.vector.ImageVector import ru.n08i40k.polytechnic.next.R @@ -19,6 +20,7 @@ object Constants { val bottomNavItem = listOf( BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"), BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", true), - BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule") + BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule"), + BottomNavItem(R.string.teachers, Icons.Filled.Person, "teacher-schedule") ) } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt index 18bbf7c..f832243 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt @@ -70,12 +70,14 @@ import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Download import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen -import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen +import ru.n08i40k.polytechnic.next.ui.main.schedule.group.GroupScheduleScreen +import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.TeacherScheduleScreen +import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigViewModel import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel -import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel +import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.profileViewModel @@ -83,7 +85,8 @@ import ru.n08i40k.polytechnic.next.ui.model.profileViewModel private fun NavHostContainer( navController: NavHostController, padding: PaddingValues, - scheduleViewModel: ScheduleViewModel, + groupScheduleViewModel: GroupScheduleViewModel, + teacherScheduleViewModel: TeacherScheduleViewModel, scheduleReplacerViewModel: ScheduleReplacerViewModel? ) { val context = LocalContext.current @@ -121,7 +124,15 @@ private fun NavHostContainer( } composable("schedule") { - ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() } + GroupScheduleScreen(groupScheduleViewModel) { groupScheduleViewModel.refresh() } + } + + composable("teacher-schedule") { + TeacherScheduleScreen(teacherScheduleViewModel) { + if (it.isNotEmpty()) teacherScheduleViewModel.fetch( + it + ) + } } if (scheduleReplacerViewModel != null) { @@ -283,8 +294,12 @@ fun MainScreen( ) // schedule view model - val scheduleViewModel = - hiltViewModel(LocalContext.current as ComponentActivity) + val groupScheduleViewModel = + hiltViewModel(LocalContext.current as ComponentActivity) + + // teacher view model + val teacherScheduleViewModel = + hiltViewModel(LocalContext.current as ComponentActivity) // schedule replacer view model val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle() @@ -312,7 +327,8 @@ fun MainScreen( NavHostContainer( navController, paddingValues, - scheduleViewModel, + groupScheduleViewModel, + teacherScheduleViewModel, scheduleReplacerViewModel ) } diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt index 754a83e..dc4b078 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt @@ -38,7 +38,7 @@ import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.settings.settingsDataStore -import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel +import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.profileViewModel @Preview(showBackground = true) @@ -176,8 +176,8 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile } if (groupChanging) { - val scheduleViewModel = - hiltViewModel(LocalContext.current as ComponentActivity) + val groupScheduleViewModel = + hiltViewModel(LocalContext.current as ComponentActivity) ChangeGroupDialog( context, @@ -194,7 +194,7 @@ internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile .clear() } context.profileViewModel!!.refreshProfile { - scheduleViewModel.refreshGroup() + groupScheduleViewModel.refresh() } } ) { groupChanging = false } diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt index 0759eb5..46905c9 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt @@ -102,7 +102,7 @@ fun DayCard( text = stringResource(when (distance) { -1 -> R.string.yesterday 0 -> R.string.today - 1 -> R.string.tommorow + 1 -> R.string.tomorrow else -> throw RuntimeException() }), ) diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt index 2cde061..6d18979 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.util.lerp import kotlinx.datetime.LocalDateTime import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository -import ru.n08i40k.polytechnic.next.model.Group +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard import ru.n08i40k.polytechnic.next.utils.dateTime import ru.n08i40k.polytechnic.next.utils.now @@ -25,9 +25,9 @@ import java.util.Calendar import java.util.logging.Level import kotlin.math.absoluteValue -private fun isScheduleOutdated(group: Group): Boolean { +private fun isScheduleOutdated(groupOrTeacher: GroupOrTeacher): Boolean { val nowDateTime = LocalDateTime.now() - val lastDay = group.days.lastOrNull() ?: return true + val lastDay = groupOrTeacher.days.lastOrNull() ?: return true val lastLesson = lastDay.last ?: return true return nowDateTime > lastLesson.time.end.dateTime @@ -35,17 +35,17 @@ private fun isScheduleOutdated(group: Group): Boolean { @Preview @Composable -fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) { +fun DayPager(groupOrTeacher: GroupOrTeacher = FakeScheduleRepository.exampleGroup) { val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2) val calendarDay = if (currentDay == -1) 6 else currentDay val pagerState = rememberPagerState( initialPage = calendarDay - .coerceAtMost(group.days.size - 1), - pageCount = { group.days.size }) + .coerceAtMost(groupOrTeacher.days.size - 1), + pageCount = { groupOrTeacher.days.size }) Column { - if (isScheduleOutdated(group)) { + if (isScheduleOutdated(groupOrTeacher)) { NotificationCard( level = Level.WARNING, title = stringResource(R.string.outdated_schedule) @@ -73,7 +73,7 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) { start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f) ) }, - day = group.days[page], + day = groupOrTeacher.days[page], distance = page - currentDay ) } diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/LessonView.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/LessonView.kt index a179df6..884d44c 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/LessonView.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/LessonView.kt @@ -117,6 +117,7 @@ private fun LessonViewRow( timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO, name: String = "Test", subGroups: List = listOf(), + group: String? = "ИС-214/23", cardColors: CardColors = CardDefaults.cardColors(), verticalPadding: Dp = 10.dp, now: Boolean = true, @@ -201,6 +202,16 @@ private fun LessonViewRow( color = contentColor ) + if (group != null) { + Text( + text = group, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = contentColor + ) + } + for (subGroup in subGroups) { Text( text = subGroup.teacher, @@ -212,8 +223,10 @@ private fun LessonViewRow( } Column(modifier = Modifier.wrapContentWidth()) { - if (subGroups.size != 1) - Text(text = "") + if (subGroups.size != 1) { + for (i in 0..<(if (group != null) 2 else 1)) + Text(text = "") + } for (subGroup in subGroups) { Text( text = subGroup.cabinet, @@ -244,6 +257,7 @@ fun FreeLessonRow( LessonTimeFormat.ONLY_MINUTES_DURATION, stringResource(R.string.lesson_break), lesson.subGroups, + lesson.group, cardColors, 2.5.dp, now @@ -264,6 +278,7 @@ fun LessonRow( LessonTimeFormat.FROM_TO, lesson.name!!, lesson.subGroups, + lesson.group, cardColors, 5.dp, now diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/GroupScheduleScreen.kt similarity index 75% rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/GroupScheduleScreen.kt index 60a37f9..52287a4 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/GroupScheduleScreen.kt @@ -1,4 +1,4 @@ -package ru.n08i40k.polytechnic.next.ui.main.schedule +package ru.n08i40k.polytechnic.next.ui.main.schedule.group import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -26,9 +26,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.delay import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.data.MockAppContainer +import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager +import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleUiState +import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent -import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState -import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel @Composable private fun rememberUpdatedLifecycleOwner(): LifecycleOwner { @@ -38,11 +39,11 @@ private fun rememberUpdatedLifecycleOwner(): LifecycleOwner { @Preview(showBackground = true, showSystemUi = true) @Composable -fun ScheduleScreen( - scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer(LocalContext.current)), +fun GroupScheduleScreen( + groupScheduleViewModel: GroupScheduleViewModel = GroupScheduleViewModel(MockAppContainer(LocalContext.current)), onRefresh: () -> Unit = {} ) { - val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle() + val uiState by groupScheduleViewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(uiState) { delay(120_000) onRefresh() @@ -69,25 +70,25 @@ fun ScheduleScreen( LoadingContent( empty = when (uiState) { - is ScheduleUiState.NoSchedule -> uiState.isLoading - is ScheduleUiState.HasSchedule -> false + is GroupScheduleUiState.NoData -> uiState.isLoading + is GroupScheduleUiState.HasData -> false }, loading = uiState.isLoading, onRefresh = onRefresh, verticalArrangement = Arrangement.Top ) { when (uiState) { - is ScheduleUiState.HasSchedule -> { + is GroupScheduleUiState.HasData -> { Column { - val hasSchedule = uiState as ScheduleUiState.HasSchedule + val hasData = uiState as GroupScheduleUiState.HasData - UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates) + UpdateInfo(hasData.lastUpdateAt, hasData.updateDates) Spacer(Modifier.height(10.dp)) - DayPager(hasSchedule.group) + DayPager(hasData.group) } } - is ScheduleUiState.NoSchedule -> { + is GroupScheduleUiState.NoData -> { if (!uiState.isLoading) { TextButton(onClick = onRefresh, modifier = Modifier.fillMaxSize()) { Text(stringResource(R.string.reload), textAlign = TextAlign.Center) diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/Paskhalko.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/Paskhalko.kt similarity index 90% rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/Paskhalko.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/Paskhalko.kt index ca94cdd..2b86bf1 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/Paskhalko.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/Paskhalko.kt @@ -1,4 +1,4 @@ -package ru.n08i40k.polytechnic.next.ui.main.schedule +package ru.n08i40k.polytechnic.next.ui.main.schedule.group import androidx.compose.foundation.Image import androidx.compose.runtime.Composable diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/UpdateInfo.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/UpdateInfo.kt similarity index 98% rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/UpdateInfo.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/UpdateInfo.kt index dacae64..99f6d21 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/UpdateInfo.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/UpdateInfo.kt @@ -1,4 +1,4 @@ -package ru.n08i40k.polytechnic.next.ui.main.schedule +package ru.n08i40k.polytechnic.next.ui.main.schedule.group import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/SearchBox.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/SearchBox.kt new file mode 100644 index 0000000..4eeaad7 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/SearchBox.kt @@ -0,0 +1,90 @@ +package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBox( + title: String, + onSearchAttempt: (String) -> Unit, + variants: List, +) { + var value by remember { mutableStateOf("") } + + val searchableVariants = + remember(variants.size) { variants.map { it.replace(" ", "").replace(".", "").lowercase() } } + val filteredVariants = remember(searchableVariants, value) { + searchableVariants.filter { it.contains(value) } + } + + var dropdownExpanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = dropdownExpanded, + onExpandedChange = {} + ) { + Row(modifier = Modifier.fillMaxWidth()) { + TextField( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { + if (it.hasFocus) + dropdownExpanded = true + } + .menuAnchor(MenuAnchorType.PrimaryEditable, true), + label = { Text(title) }, + value = value, + onValueChange = { + value = it + dropdownExpanded = true + }, + trailingIcon = { + IconButton(onClick = { onSearchAttempt(value) }) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Search" + ) + } + }, + singleLine = true, + ) + } + + ExposedDropdownMenu( + expanded = dropdownExpanded, + onDismissRequest = { dropdownExpanded = false } + ) { + filteredVariants.forEach { + val fullVariant = variants[searchableVariants.indexOf(it)] + + DropdownMenuItem( + text = { Text(fullVariant) }, + onClick = { + value = fullVariant + onSearchAttempt(value) + + dropdownExpanded = false + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/TeacherScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/TeacherScheduleScreen.kt new file mode 100644 index 0000000..6419ac9 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/TeacherScheduleScreen.kt @@ -0,0 +1,114 @@ +package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.delay +import ru.n08i40k.polytechnic.next.R +import ru.n08i40k.polytechnic.next.data.MockAppContainer +import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager +import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleUiState +import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel +import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent + +@Composable +private fun rememberUpdatedLifecycleOwner(): LifecycleOwner { + val lifecycleOwner = LocalLifecycleOwner.current + return remember { lifecycleOwner } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +fun TeacherScheduleScreen( + teacherScheduleViewModel: TeacherScheduleViewModel = TeacherScheduleViewModel( + MockAppContainer( + LocalContext.current + ) + ), + fetch: (String) -> Unit = {} +) { + var teacherName by remember { mutableStateOf("") } + + val uiState by teacherScheduleViewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(uiState) { + delay(120_000) + fetch(teacherName) + } + + val lifecycleOwner = rememberUpdatedLifecycleOwner() + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + fetch(teacherName) + } + + else -> Unit + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + Column(Modifier.fillMaxSize()) { + TeacherSearchBox(onSearchAttempt = { + teacherName = it + fetch(it) + }) + + Spacer(Modifier.height(10.dp)) + + LoadingContent( + empty = when (uiState) { + is TeacherScheduleUiState.NoData -> uiState.isLoading + is TeacherScheduleUiState.HasData -> false + }, + loading = uiState.isLoading, + ) { + when (uiState) { + is TeacherScheduleUiState.HasData -> { + Column { + val hasData = uiState as TeacherScheduleUiState.HasData + + DayPager(hasData.teacher) + } + } + + is TeacherScheduleUiState.NoData -> { + if (!uiState.isLoading) { + Text( + modifier = Modifier.fillMaxSize(), + text = stringResource(R.string.teacher_not_selected), + textAlign = TextAlign.Center + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/TeacherSearchBox.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/TeacherSearchBox.kt new file mode 100644 index 0000000..0c34c91 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/TeacherSearchBox.kt @@ -0,0 +1,46 @@ +package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import ru.n08i40k.polytechnic.next.R +import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacherNames + +@Composable +private fun getTeacherNames(context: Context): ArrayList { + val teacherNames = remember { arrayListOf() } + + LaunchedEffect(teacherNames) { + ScheduleGetTeacherNames(context, { + teacherNames.clear() + teacherNames.addAll(it.names) + }, { + teacherNames.clear() + }).send() + } + + return teacherNames +} + +@Preview(showBackground = true) +@Composable +fun TeacherSearchBox( + onSearchAttempt: (String) -> Unit = {}, +) { + val teachers = getTeacherNames(LocalContext.current) + val focusManager = LocalFocusManager.current + + SearchBox( + stringResource(R.string.teacher_name), + { + focusManager.clearFocus(true) + onSearchAttempt(it) + }, + teachers, + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupScheduleViewModel.kt similarity index 73% rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupScheduleViewModel.kt index 314322e..c81575b 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupScheduleViewModel.kt @@ -12,55 +12,55 @@ import kotlinx.coroutines.launch import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.MyResult -import ru.n08i40k.polytechnic.next.model.Group +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import java.util.Date import javax.inject.Inject -sealed interface ScheduleUiState { +sealed interface GroupScheduleUiState { val isLoading: Boolean - data class NoSchedule( + data class NoData( override val isLoading: Boolean - ) : ScheduleUiState + ) : GroupScheduleUiState - data class HasSchedule( - val group: Group, + data class HasData( + val group: GroupOrTeacher, val updateDates: UpdateDates, val lastUpdateAt: Long, override val isLoading: Boolean - ) : ScheduleUiState + ) : GroupScheduleUiState } -private data class ScheduleViewModelState( - val group: Group? = null, +private data class GroupScheduleViewModelState( + val group: GroupOrTeacher? = null, val updateDates: UpdateDates? = null, val lastUpdateAt: Long = 0, val isLoading: Boolean = false ) { - fun toUiState(): ScheduleUiState = if (group == null) { - ScheduleUiState.NoSchedule(isLoading) + fun toUiState(): GroupScheduleUiState = if (group == null) { + GroupScheduleUiState.NoData(isLoading) } else { - ScheduleUiState.HasSchedule(group, updateDates!!, lastUpdateAt, isLoading) + GroupScheduleUiState.HasData(group, updateDates!!, lastUpdateAt, isLoading) } } @HiltViewModel -class ScheduleViewModel @Inject constructor( +class GroupScheduleViewModel @Inject constructor( appContainer: AppContainer ) : ViewModel() { private val scheduleRepository = appContainer.scheduleRepository private val networkCacheRepository = appContainer.networkCacheRepository - private val viewModelState = MutableStateFlow(ScheduleViewModelState(isLoading = true)) + private val viewModelState = MutableStateFlow(GroupScheduleViewModelState(isLoading = true)) val uiState = viewModelState - .map(ScheduleViewModelState::toUiState) + .map(GroupScheduleViewModelState::toUiState) .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState()) init { - refreshGroup() + refresh() } - fun refreshGroup() { + fun refresh() { viewModelState.update { it.copy(isLoading = true) } viewModelScope.launch { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherScheduleViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherScheduleViewModel.kt new file mode 100644 index 0000000..1bddf3a --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherScheduleViewModel.kt @@ -0,0 +1,100 @@ +package ru.n08i40k.polytechnic.next.ui.model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.n08i40k.polytechnic.next.UpdateDates +import ru.n08i40k.polytechnic.next.data.AppContainer +import ru.n08i40k.polytechnic.next.data.MyResult +import ru.n08i40k.polytechnic.next.model.GroupOrTeacher +import java.util.Date +import javax.inject.Inject + +sealed interface TeacherScheduleUiState { + val isLoading: Boolean + + data class NoData( + override val isLoading: Boolean + ) : TeacherScheduleUiState + + data class HasData( + val teacher: GroupOrTeacher, + val updateDates: UpdateDates, + val lastUpdateAt: Long, + override val isLoading: Boolean + ) : TeacherScheduleUiState +} + +private data class TeacherScheduleViewModelState( + val teacher: GroupOrTeacher? = null, + val updateDates: UpdateDates? = null, + val lastUpdateAt: Long = 0, + val isLoading: Boolean = false +) { + fun toUiState(): TeacherScheduleUiState = if (teacher == null) { + TeacherScheduleUiState.NoData(isLoading) + } else { + TeacherScheduleUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading) + } +} + +@HiltViewModel +class TeacherScheduleViewModel @Inject constructor( + appContainer: AppContainer +) : ViewModel() { + private val scheduleRepository = appContainer.scheduleRepository + private val networkCacheRepository = appContainer.networkCacheRepository + private val viewModelState = MutableStateFlow(TeacherScheduleViewModelState(isLoading = true)) + + val uiState = viewModelState + .map(TeacherScheduleViewModelState::toUiState) + .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState()) + + init { + fetch(null) + } + + fun fetch(name: String?) { + if (name == null) { + viewModelState.update { + it.copy( + teacher = null, + isLoading = false + ) + } + return + } + + viewModelState.update { it.copy(isLoading = true) } + + viewModelScope.launch { + val result = scheduleRepository.getTeacher(name) + + viewModelState.update { + when (result) { + is MyResult.Success -> { + val updateDates = networkCacheRepository.getUpdateDates() + + it.copy( + teacher = result.data, + updateDates = updateDates, + lastUpdateAt = Date().time, + isLoading = false + ) + } + + is MyResult.Failure -> it.copy( + teacher = null, + isLoading = false + ) + } + } + } + } +} diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/LoadingContent.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/LoadingContent.kt index 1432f3c..f624c3d 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/LoadingContent.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/LoadingContent.kt @@ -37,6 +37,24 @@ fun LoadingContent( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoadingContent( + empty: Boolean, + emptyContent: @Composable () -> Unit = { FullScreenLoading() }, + loading: Boolean, + content: @Composable () -> Unit +) { + if (empty) { + emptyContent() + return + } + + PullToRefreshBox(isRefreshing = loading, onRefresh = {}) { + content() + } +} + @Preview(showBackground = true) @Composable fun FullScreenLoading() { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt index 936b10d..f243859 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt @@ -43,6 +43,9 @@ val Instant.dayMinutes: Int val LocalDateTime.dayMinutes: Int get() = this.hour * 60 + this.minute +fun LocalDateTime.Companion.fromEpochMilliseconds(milliseconds: Long): LocalDateTime = + Instant.fromEpochMilliseconds(milliseconds).dateTime + val Instant.dateTime: LocalDateTime get() = this.toLocalDateTime(TimeZone.currentSystemDefault()) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 775e28d..22bfd20 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -75,5 +75,9 @@ Нет подключения к интернету! Сегодня Вчера - Завтра + Завтра + Преподаватели + ФИО преподавателя + Преподаватель не выбран или вы допустили ошибку в его ФИО. + Не удалось получить список ФИО преподавателей! \ 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 3b802c4..b5143bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,5 +75,9 @@ No internet connection! Today Yesterday - Tommorow + Tomorrow + Teachers + Teacher name + Teacher not selected or you made mistake in his name. + Failed to fetch teacher names! \ No newline at end of file diff --git a/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt b/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt index f913706..2db2bb5 100644 --- a/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt +++ b/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt @@ -1,17 +1,27 @@ package ru.n08i40k.polytechnic.next +import android.content.Context import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ +@RunWith(MockitoJUnitRunner.Silent::class) class ExampleUnitTest { @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) + fun getNameAndCabinetsShort_isNotThrow() { + val mockContext = mock { + on { getString(R.string.in_gym_lc) } doReturn "с/з" + on { getString(R.string.lesson_break) } doReturn "Перемена" + } + val group = FakeScheduleRepository.exampleGroup + + for (day in group.days) { + for (lesson in day.lessons) { + lesson.getNameAndCabinetsShort(mockContext) + } + } } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3624dca..3457c0c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ kotlinxSerializationJson = "1.7.3" lifecycleRuntimeKtx = "2.8.7" activityCompose = "1.9.3" composeBom = "2024.10.01" +mockitoKotlin = "5.4.0" protobufLite = "3.0.1" volley = "1.2.1" datastore = "1.1.1" @@ -45,6 +46,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.1" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" } volley = { group = "com.android.volley", name = "volley", version.ref = "volley" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }