mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
2.1.0
Окончательный, по моему мнению, фикс запуска постоянного уведомления. Добавлена возможность просмотра расписания конкретного преподавателя.
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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<Group>
|
||||
suspend fun getGroup(): MyResult<GroupOrTeacher>
|
||||
|
||||
suspend fun getTeacher(name: String): MyResult<GroupOrTeacher>
|
||||
}
|
||||
@@ -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<Group?>(exampleGroup)
|
||||
private val group = MutableStateFlow<GroupOrTeacher?>(exampleGroup)
|
||||
private val teacher = MutableStateFlow<GroupOrTeacher?>(exampleTeacher)
|
||||
|
||||
private var updateCounter: Int = 0
|
||||
|
||||
override suspend fun getGroup(): MyResult<Group> {
|
||||
override suspend fun getGroup(): MyResult<GroupOrTeacher> {
|
||||
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<GroupOrTeacher> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
delay(1500)
|
||||
if (updateCounter++ % 3 == 0) MyResult.Failure(
|
||||
IllegalStateException()
|
||||
)
|
||||
else MyResult.Success(teacher.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Group> =
|
||||
override suspend fun getGroup(): MyResult<GroupOrTeacher> =
|
||||
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<GroupOrTeacher> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Lesson>
|
||||
) : Parcelable {
|
||||
constructor(name: String, date: Instant, lessons: List<Lesson>) : this(
|
||||
@@ -29,30 +31,19 @@ class Day(
|
||||
val date: Instant
|
||||
get() = Instant.fromEpochMilliseconds(dateMillis)
|
||||
|
||||
fun distanceToNextByLocalDateTime(from: LocalDateTime): Pair<Int, Int>? {
|
||||
val toIdx = lessons
|
||||
.map { it.time.start }
|
||||
.indexOfFirst { it.dateTime > from }
|
||||
fun distanceToNext(from: LocalDateTime): Pair<Int, Int>? {
|
||||
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<Int, Int>? {
|
||||
val fromLesson = if (from != null) lessons[from] else null
|
||||
fun distanceToNext(fromIndex: Int? = null): Pair<Int, Int>? {
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.Calendar
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Group(
|
||||
data class GroupOrTeacher(
|
||||
val name: String,
|
||||
val days: List<Day>
|
||||
) : Parcelable {
|
||||
@@ -15,6 +15,7 @@ data class Lesson(
|
||||
val defaultRange: List<Int>?,
|
||||
val name: String?,
|
||||
val time: LessonTime,
|
||||
val group: String? = null,
|
||||
val subGroups: List<SubGroup>
|
||||
) : 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 }
|
||||
|
||||
@@ -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<Int>,
|
||||
)
|
||||
}
|
||||
@@ -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<ResponseDto>,
|
||||
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<Int>,
|
||||
)
|
||||
}
|
||||
@@ -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<ResponseDto>,
|
||||
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<String>,
|
||||
)
|
||||
}
|
||||
@@ -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,35 +58,64 @@ 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()
|
||||
val nextLesson = nextIndex?.let { day.lessons[nextIndex] }
|
||||
|
||||
if (currentLesson == null && nextLesson == null) {
|
||||
onLessonsEnd()
|
||||
return
|
||||
}
|
||||
|
||||
val currentMinutes = Calendar.getInstance()
|
||||
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
|
||||
.get(Calendar.MINUTE)
|
||||
handler.postDelayed(this, UPDATE_INTERVAL)
|
||||
|
||||
val currentLessonEntry = day!!.currentKV
|
||||
val currentLessonIdx: Int? = currentLessonEntry?.first
|
||||
val currentLesson: Lesson? = currentLessonEntry?.second
|
||||
val context = this@CurrentLessonViewService
|
||||
val currentMinutes = LocalDateTime.now().dayMinutes
|
||||
|
||||
val nextLessonEntry = day!!.distanceToNextByIdx(currentLessonIdx)
|
||||
val nextLesson =
|
||||
if (nextLessonEntry == null)
|
||||
null
|
||||
val distanceToFirst = day.first!!.time.start.dayMinutes - currentMinutes
|
||||
|
||||
val currentLessonName =
|
||||
currentLesson?.getNameAndCabinetsShort(context)
|
||||
?: run {
|
||||
if (distanceToFirst > 0)
|
||||
getString(R.string.lessons_not_started)
|
||||
else
|
||||
day!!.lessons[nextLessonEntry.first]
|
||||
getString(R.string.lesson_break)
|
||||
}
|
||||
|
||||
if (currentLesson == null && nextLesson == null) {
|
||||
val nextLessonName =
|
||||
nextLesson?.getNameAndCabinetsShort(context) ?: getString(R.string.lessons_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,
|
||||
(nextLessonIn - currentMinutes) / 60,
|
||||
(nextLessonIn - currentMinutes) % 60
|
||||
),
|
||||
getString(
|
||||
R.string.lesson_going_notification_description,
|
||||
currentLessonName,
|
||||
nextLessonIn.fmtAsClock(),
|
||||
nextLessonName,
|
||||
)
|
||||
)
|
||||
getNotificationManager().notify(NOTIFICATION_STATUS_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLessonsEnd() {
|
||||
val notification = NotificationCompat
|
||||
.Builder(applicationContext, NotificationChannels.LESSON_VIEW)
|
||||
.setSmallIcon(R.drawable.schedule)
|
||||
@@ -98,63 +126,6 @@ class CurrentLessonViewService : Service() {
|
||||
getNotificationManager().notify(NOTIFICATION_END_ID, notification)
|
||||
|
||||
stopSelf()
|
||||
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
|
||||
|
||||
val currentLessonDelay =
|
||||
if (currentLesson == null) // Если эта пара - перемена, то конец перемены через (результат getDistanceToNext)
|
||||
nextLessonEntry!!.second
|
||||
else // Если эта пара - обычная пара, то конец пары через (конец этой пары - текущее кол-во минут)
|
||||
currentLesson.time!!.end.dayMinutes - currentMinutes
|
||||
|
||||
val currentLessonName =
|
||||
currentLesson?.getNameAndCabinetsShort(this@CurrentLessonViewService)
|
||||
?: run {
|
||||
if (distanceToFirst > 0)
|
||||
getString(R.string.lessons_not_started)
|
||||
else
|
||||
getString(R.string.lesson_break)
|
||||
}
|
||||
|
||||
val nextLessonName =
|
||||
if (currentLesson == null) // Если текущая пара - перемена
|
||||
nextLesson!!.getNameAndCabinetsShort(this@CurrentLessonViewService)
|
||||
else if (nextLesson == null) // Если текущая пара - последняя
|
||||
getString(R.string.lessons_end)
|
||||
else // Если после текущей пары есть ещё пара(ы)
|
||||
getString(R.string.lesson_break)
|
||||
|
||||
val nextLessonTotal =
|
||||
if (currentLesson == null)
|
||||
nextLesson!!.time!!.start
|
||||
else
|
||||
currentLesson.time!!.end
|
||||
|
||||
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
|
||||
),
|
||||
getString(
|
||||
R.string.lesson_going_notification_description,
|
||||
currentLessonName,
|
||||
nextLessonTotal.dayMinutes.fmtAsClock(),
|
||||
nextLessonName,
|
||||
)
|
||||
)
|
||||
getNotificationManager().notify(NOTIFICATION_STATUS_ID, notification)
|
||||
|
||||
handler.postDelayed(this, UPDATE_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotification(
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
@@ -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<ScheduleViewModel>(LocalContext.current as ComponentActivity)
|
||||
val groupScheduleViewModel =
|
||||
hiltViewModel<GroupScheduleViewModel>(LocalContext.current as ComponentActivity)
|
||||
|
||||
// teacher view model
|
||||
val teacherScheduleViewModel =
|
||||
hiltViewModel<TeacherScheduleViewModel>(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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<ScheduleViewModel>(LocalContext.current as ComponentActivity)
|
||||
val groupScheduleViewModel =
|
||||
hiltViewModel<GroupScheduleViewModel>(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 }
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ private fun LessonViewRow(
|
||||
timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
|
||||
name: String = "Test",
|
||||
subGroups: List<SubGroup> = 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)
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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<String>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> {
|
||||
val teacherNames = remember { arrayListOf<String>() }
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -75,5 +75,9 @@
|
||||
<string name="no_connection">Нет подключения к интернету!</string>
|
||||
<string name="today">Сегодня</string>
|
||||
<string name="yesterday">Вчера</string>
|
||||
<string name="tommorow">Завтра</string>
|
||||
<string name="tomorrow">Завтра</string>
|
||||
<string name="teachers">Преподаватели</string>
|
||||
<string name="teacher_name">ФИО преподавателя</string>
|
||||
<string name="teacher_not_selected">Преподаватель не выбран или вы допустили ошибку в его ФИО.</string>
|
||||
<string name="failed_to_fetch_teacher_names">Не удалось получить список ФИО преподавателей!</string>
|
||||
</resources>
|
||||
@@ -75,5 +75,9 @@
|
||||
<string name="no_connection">No internet connection!</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="yesterday">Yesterday</string>
|
||||
<string name="tommorow">Tommorow</string>
|
||||
<string name="tomorrow">Tomorrow</string>
|
||||
<string name="teachers">Teachers</string>
|
||||
<string name="teacher_name">Teacher name</string>
|
||||
<string name="teacher_not_selected">Teacher not selected or you made mistake in his name.</string>
|
||||
<string name="failed_to_fetch_teacher_names">Failed to fetch teacher names!</string>
|
||||
</resources>
|
||||
@@ -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<Context> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user