Переход на API v2

Переработано отображение расписания.
- Один предмет теперь может занимать несколько пар.
- В заголовке дня теперь может писаться "Сегодня", "Завтра", "Вчера".

Обновление расписания теперь происходит без отгрузки целой страницы политехникума на сервер.
Приложение теперь само находит ссылку с помощью регулярных выражений, что влечёт за собой малый прирост к скорости отправки запроса и его обработки сервером.

Пасхалко.
This commit is contained in:
2024-10-19 01:31:44 +04:00
parent 8ed9ce17e7
commit c81fd2540b
44 changed files with 486 additions and 418 deletions

View File

@@ -15,27 +15,8 @@
<option name="projectNumber" value="946974192625" /> <option name="projectNumber" value="946974192625" />
</ConnectionSetting> </ConnectionSetting>
</option> </option>
<option name="devices">
<list>
<DeviceSetting>
<option name="deviceType" value="Phone" />
<option name="displayName" value="Xiaomi (2311DRK48G)" />
<option name="manufacturer" value="Xiaomi" />
<option name="model" value="2311DRK48G" />
</DeviceSetting>
</list>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" /> <option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" /> <option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="versions">
<list>
<VersionSetting>
<option name="buildVersion" value="13" />
<option name="displayName" value="1.7.1 (13)" />
<option name="displayVersion" value="1.7.1" />
</VersionSetting>
</list>
</option>
<option name="visibilityType" value="ALL" /> <option name="visibilityType" value="ALL" />
</InsightsFilterSettings> </InsightsFilterSettings>
</value> </value>

View File

@@ -33,13 +33,14 @@ android {
applicationId = "ru.n08i40k.polytechnic.next" applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 15 versionCode = 16
versionName = "1.8.0" versionName = "2.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }
versionNameSuffix = "prod"
} }
buildTypes { buildTypes {
@@ -112,7 +113,10 @@ dependencies {
implementation(libs.accompanist.swiperefresh) implementation(libs.accompanist.swiperefresh)
// json
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)

View File

@@ -12,7 +12,6 @@ import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
class PolytechnicApplication : Application() { class PolytechnicApplication : Application() {
@Suppress("unused")
@Inject @Inject
lateinit var container: AppContainer lateinit var container: AppContainer

View File

@@ -4,6 +4,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
@@ -11,6 +15,25 @@ import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.model.LessonType import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.model.SubGroup
import ru.n08i40k.polytechnic.next.utils.now
private fun genLocalDateTime(hour: Int, minute: Int): Instant {
return LocalDateTime(2024, 1, 1, hour, minute, 0, 0).toInstant(TimeZone.currentSystemDefault())
}
private fun genBreak(start: Instant, end: Instant): Lesson {
return Lesson(
type = LessonType.BREAK,
defaultRange = null,
name = null,
time = LessonTime(
start,
end
),
subGroups = listOf()
)
}
class FakeScheduleRepository : ScheduleRepository { class FakeScheduleRepository : ScheduleRepository {
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@@ -19,132 +42,102 @@ class FakeScheduleRepository : ScheduleRepository {
name = "ИС-214/23", days = arrayListOf( name = "ИС-214/23", days = arrayListOf(
Day( Day(
name = "Понедельник", name = "Понедельник",
nonNullIndices = arrayListOf(0, 1, 2, 3, 4, 5), date = LocalDateTime.now().toInstant(TimeZone.currentSystemDefault()),
defaultIndices = arrayListOf(2, 3, 4, 5), lessons = listOf(
customIndices = arrayListOf(0, 1),
lessons = arrayListOf(
Lesson( Lesson(
type = LessonType.CUSTOM, type = LessonType.ADDITIONAL,
defaultIndex = -1, defaultRange = null,
name = "Линейка", name = "Линейка",
time = LessonTime(510, 520), time = LessonTime(
cabinets = arrayListOf(), genLocalDateTime(8, 30),
teacherNames = arrayListOf(), genLocalDateTime(8, 40),
),
subGroups = listOf()
),
genBreak(
genLocalDateTime(8, 40),
genLocalDateTime(8, 45),
), ),
Lesson( Lesson(
type = LessonType.CUSTOM, type = LessonType.ADDITIONAL,
defaultIndex = -1, defaultRange = null,
name = "Разговор о важном", name = "Разговор о важном",
time = LessonTime(525, 555), time = LessonTime(
cabinets = arrayListOf(), genLocalDateTime(8, 45),
teacherNames = arrayListOf(), genLocalDateTime(9, 15),
),
subGroups = listOf()
),
genBreak(
genLocalDateTime(9, 15),
genLocalDateTime(9, 25),
), ),
Lesson( Lesson(
type = LessonType.DEFAULT, type = LessonType.DEFAULT,
defaultIndex = 1, defaultRange = listOf(1, 1),
name = "Элементы высшей математики",
time = LessonTime(565, 645),
cabinets = arrayListOf("31", "12"),
teacherNames = arrayListOf("Цацаева Т.Н."),
),
Lesson(
type = LessonType.DEFAULT,
defaultIndex = 2,
name = "Операционные системы и среды",
time = LessonTime(655, 735),
cabinets = arrayListOf("42", "52"),
teacherNames = arrayListOf("Сергачева А.О.", "Не Сергачева А.О."),
),
Lesson(
type = LessonType.DEFAULT,
defaultIndex = 3,
name = "Физическая культура",
time = LessonTime(755, 835),
cabinets = arrayListOf("c/3"),
teacherNames = arrayListOf("Васюнин В.Г.", "Не Васюнин В.Г."),
),
Lesson(
type = LessonType.DEFAULT,
defaultIndex = 4,
name = "МДК.05.01 Проектирование и дизайн информационных систем", name = "МДК.05.01 Проектирование и дизайн информационных систем",
time = LessonTime(845, 925), time = LessonTime(
cabinets = arrayListOf("43"), genLocalDateTime(9, 25),
teacherNames = arrayListOf("Ивашова А.Н."), genLocalDateTime(10, 45),
), ),
null, subGroups = listOf(
null, SubGroup(
teacher = "Ивашова А.Н.",
number = 1,
cabinet = "43"
) )
), Day( )
name = "Вторник", ),
nonNullIndices = arrayListOf(0, 1, 2), genBreak(
defaultIndices = arrayListOf(0, 1, 2), genLocalDateTime(10, 45),
customIndices = arrayListOf(), genLocalDateTime(10, 55),
lessons = arrayListOf(
Lesson(
type = LessonType.DEFAULT,
defaultIndex = 1,
name = "Стандартизация, сертификация и техническое документоведение",
time = LessonTime(525, 605),
cabinets = arrayListOf("42"),
teacherNames = arrayListOf("Сергачева А.О."),
), ),
Lesson( Lesson(
type = LessonType.DEFAULT, type = LessonType.DEFAULT,
defaultIndex = 2, defaultRange = listOf(2, 2),
name = "Элементы высшей математики",
time = LessonTime(620, 700),
cabinets = arrayListOf("31"),
teacherNames = arrayListOf("Цацаева Т.Н."),
),
Lesson(
type = LessonType.DEFAULT,
defaultIndex = 3,
name = "Основы проектирования баз данных", name = "Основы проектирования баз данных",
time = LessonTime(720, 800), time = LessonTime(
cabinets = arrayListOf("21"), genLocalDateTime(10, 55),
teacherNames = arrayListOf("Чинарева Е.А."), genLocalDateTime(12, 15),
),
subGroups = listOf(
SubGroup(
teacher = "Чинарева Е.А.",
number = 1,
cabinet = "21"
),
SubGroup(
teacher = "Ивашова А.Н.",
number = 2,
cabinet = "44"
), ),
null,
null,
null,
null,
null,
) )
), Day( ),
name = "Среда", genBreak(
nonNullIndices = arrayListOf(0, 1, 2), genLocalDateTime(12, 15),
defaultIndices = arrayListOf(0, 1, 2), genLocalDateTime(12, 35),
customIndices = arrayListOf(), ),
lessons = arrayListOf(
Lesson( Lesson(
type = LessonType.DEFAULT, type = LessonType.DEFAULT,
defaultIndex = 1, defaultRange = listOf(3, 3),
name = "Операционные системы и среды", name = "Операционные системы и среды",
time = LessonTime(525, 605), time = LessonTime(
cabinets = arrayListOf("42"), genLocalDateTime(12, 35),
teacherNames = arrayListOf("Сергачева А.О."), genLocalDateTime(13, 55),
), ),
Lesson( subGroups = listOf(
type = LessonType.DEFAULT, SubGroup(
defaultIndex = 2, teacher = "Сергачева А.О.",
name = "Элементы высшей математики", number = 1,
time = LessonTime(620, 700), cabinet = "42"
cabinets = arrayListOf("31"),
teacherNames = arrayListOf("Цацаева Т.Н."),
), ),
Lesson( SubGroup(
type = LessonType.DEFAULT, teacher = "Воронцева Н.В.",
defaultIndex = 3, number = 2,
name = "МДК.05.01 Проектирование и дизайн информационных систем", cabinet = "41"
time = LessonTime(720, 800), ),
cabinets = arrayListOf("43"), )
teacherNames = arrayListOf("Ивашова А.Н."),
), ),
null,
null,
null,
null,
null,
) )
) )
) )

View File

@@ -13,7 +13,13 @@ class FakeProfileRepository : ProfileRepository {
companion object { companion object {
val exampleProfile = val exampleProfile =
Profile("66db32d24030a07e02d974c5", "n08i40k", "ИС-214/23", UserRole.STUDENT) Profile(
"66db32d24030a07e02d974c5",
"128735612876",
"n08i40k",
"ИС-214/23",
UserRole.STUDENT
)
} }
override suspend fun getProfile(): MyResult<Profile> { override suspend fun getProfile(): MyResult<Profile> {

View File

@@ -1,30 +1,33 @@
package ru.n08i40k.polytechnic.next.model package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.utils.getDayMinutes import ru.n08i40k.polytechnic.next.utils.dateTime
import java.util.Calendar import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.now
@Parcelize @Parcelize
@Suppress("unused", "MemberVisibilityCanBePrivate") @Suppress("unused", "MemberVisibilityCanBePrivate")
@Serializable @Serializable
class Day( class Day(
val name: String, val name: String,
val nonNullIndices: ArrayList<Int>, val date: @RawValue Instant,
val defaultIndices: ArrayList<Int>, val lessons: List<Lesson>
val customIndices: ArrayList<Int>,
val lessons: ArrayList<Lesson?>
) : Parcelable { ) : Parcelable {
fun distanceToNextByMinutes(from: Int): Pair<Int, Int>? { fun distanceToNextByLocalDateTime(from: LocalDateTime): Pair<Int, Int>? {
val toIdx = lessons val toIdx = lessons
.map { if (it?.time == null) null else it.time.start } .map { it.time.start }
.indexOfFirst { if (it == null) false else it > from } .indexOfFirst { it.dateTime > from }
if (toIdx == -1) if (toIdx == -1)
return null return null
return Pair(toIdx, lessons[toIdx]!!.time.start - from) return Pair(toIdx, lessons[toIdx].time.start.dayMinutes - from.dayMinutes)
} }
fun distanceToNextByIdx(from: Int? = null): Pair<Int, Int>? { fun distanceToNextByIdx(from: Int? = null): Pair<Int, Int>? {
@@ -35,24 +38,22 @@ class Day(
val fromTime = val fromTime =
if (from != null) if (from != null)
fromLesson!!.time.end fromLesson!!.time.end.dateTime
else else
Calendar.getInstance() LocalDateTime.now()
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
.get(Calendar.MINUTE)
return distanceToNextByMinutes(fromTime) return distanceToNextByLocalDateTime(fromTime)
} }
// current // current
val currentIdx: Int? val currentIdx: Int?
get() { get() {
val minutes = Calendar.getInstance().getDayMinutes() val now = LocalDateTime.now()
for (lessonIdx in nonNullIndices) { for (lessonIdx in lessons.indices) {
val lesson = lessons[lessonIdx]!! val lesson = lessons[lessonIdx]
if (lesson.time.start <= minutes && minutes < lesson.time.end) if (lesson.time.start.dateTime <= now && now < lesson.time.end.dateTime)
return lessonIdx return lessonIdx
} }
@@ -67,36 +68,36 @@ class Day(
val currentKV: Pair<Int, Lesson>? val currentKV: Pair<Int, Lesson>?
get() { get() {
val idx = currentIdx ?: return null val idx = currentIdx ?: return null
return Pair(idx, lessons[idx]!!) return Pair(idx, lessons[idx])
} }
// first // first
val firstIdx: Int? val firstIdx: Int?
get() = nonNullIndices.getOrNull(0) get() = if (lessons.isEmpty()) null else 0
val first: Lesson? val first: Lesson?
get() { get() {
return lessons[firstIdx ?: return null]!! return lessons[firstIdx ?: return null]
} }
val firstKV: Pair<Int, Lesson>? val firstKV: Pair<Int, Lesson>?
get() { get() {
val idx = firstIdx ?: return null val idx = firstIdx ?: return null
return Pair(idx, lessons[idx]!!) return Pair(idx, lessons[idx])
} }
// last // last
val lastIdx: Int? val lastIdx: Int?
get() = nonNullIndices.getOrNull(nonNullIndices.size - 1) get() = if (lessons.isEmpty()) null else lessons.size - 1
val last: Lesson? val last: Lesson?
get() { get() {
return lessons[lastIdx ?: return null]!! return lessons[lastIdx ?: return null]
} }
val lastKV: Pair<Int, Lesson>? val lastKV: Pair<Int, Lesson>?
get() { get() {
val idx = lastIdx ?: return null val idx = lastIdx ?: return null
return Pair(idx, lessons[idx]!!) return Pair(idx, lessons[idx])
} }
} }

View File

@@ -10,7 +10,7 @@ import java.util.Calendar
@Serializable @Serializable
data class Group( data class Group(
val name: String, val name: String,
val days: ArrayList<Day?> val days: List<Day>
) : Parcelable { ) : Parcelable {
val currentIdx: Int? val currentIdx: Int?
get() { get() {
@@ -27,7 +27,7 @@ data class Group(
return days.getOrNull(currentIdx ?: return null) return days.getOrNull(currentIdx ?: return null)
} }
val currentKV: Pair<Int, Day?>? val currentKV: Pair<Int, Day>?
get() { get() {
val idx = currentIdx ?: return null val idx = currentIdx ?: return null
return Pair(idx, days[idx]) return Pair(idx, days[idx])

View File

@@ -5,26 +5,30 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.limit import ru.n08i40k.polytechnic.next.utils.limit
@Parcelize @Parcelize
@Serializable @Serializable
data class Lesson( data class Lesson(
val type: LessonType, val type: LessonType,
val defaultIndex: Int, val defaultRange: List<Int>?,
val name: String, val name: String?,
val time: LessonTime, val time: LessonTime,
val cabinets: ArrayList<String>, val subGroups: List<SubGroup>
val teacherNames: ArrayList<String>
) : Parcelable { ) : Parcelable {
val duration: Int val duration: Int
get() { get() {
return time.end - time.start val startMinutes = time.start.dayMinutes
val endMinutes = time.end.dayMinutes
return endMinutes - startMinutes
} }
fun getNameAndCabinetsShort(context: Context): String { fun getNameAndCabinetsShort(context: Context): String {
val limitedName = name limit 15 val limitedName = name!! limit 15
val cabinets = subGroups.map { it.cabinet }
if (cabinets.isEmpty()) if (cabinets.isEmpty())
return limitedName return limitedName

View File

@@ -1,9 +1,24 @@
package ru.n08i40k.polytechnic.next.model package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Parcelize @Parcelize
@Serializable @Serializable
data class LessonTime(val start: Int, val end: Int) : Parcelable data class LessonTime(
val start: @RawValue Instant,
val end: @RawValue Instant
) : Parcelable {
companion object {
fun fromLocalDateTime(start: LocalDateTime, end: LocalDateTime): LessonTime {
val timeZone = TimeZone.currentSystemDefault()
return LessonTime(start.toInstant(timeZone), end.toInstant(timeZone))
}
}
}

View File

@@ -11,5 +11,7 @@ private class LessonTypeIntSerializer : EnumAsIntSerializer<LessonType>(
@Serializable(with = LessonTypeIntSerializer::class) @Serializable(with = LessonTypeIntSerializer::class)
enum class LessonType(val value: Int) { enum class LessonType(val value: Int) {
DEFAULT(0), CUSTOM(1) DEFAULT(0),
ADDITIONAL(1),
BREAK(2)
} }

View File

@@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Profile( data class Profile(
val id: String, val id: String,
val accessToken: String,
val username: String, val username: String,
val group: String, val group: String,
val role: UserRole val role: UserRole

View File

@@ -0,0 +1,13 @@
package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
data class SubGroup(
val number: Int,
val cabinet: String,
val teacher: String
) : Parcelable

View File

@@ -1,5 +1,5 @@
package ru.n08i40k.polytechnic.next.network package ru.n08i40k.polytechnic.next.network
object NetworkValues { object NetworkValues {
const val API_HOST = "https://polytechnic.n08i40k.ru:5050/api/v1/" const val API_HOST = "https://192.168.0.103:5050/api/"
} }

View File

@@ -16,8 +16,7 @@ import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleUpdate
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.network.tryGet import ru.n08i40k.polytechnic.next.network.tryGet
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.io.encoding.Base64 import java.util.regex.Pattern
import kotlin.io.encoding.ExperimentalEncodingApi
open class CachedRequest( open class CachedRequest(
context: Context, context: Context,
@@ -34,9 +33,12 @@ open class CachedRequest(
}, errorListener) { }, errorListener) {
private val appContainer: AppContainer = (context as PolytechnicApplication).container private val appContainer: AppContainer = (context as PolytechnicApplication).container
@OptIn(ExperimentalEncodingApi::class) companion object {
suspend fun getMainPage(): MyResult<String> { private const val REGEX: String = "<a href=\"(/\\d{4}/[\\w\\-_]+\\.xls)\">"
return withContext(Dispatchers.IO) { val pattern: Pattern = Pattern.compile(REGEX, Pattern.MULTILINE)
}
private suspend fun getXlsUrl(): MyResult<String> = withContext(Dispatchers.IO) {
val mainPageFuture = RequestFuture.newFuture<String>() val mainPageFuture = RequestFuture.newFuture<String>()
val request = StringRequest( val request = StringRequest(
Method.GET, Method.GET,
@@ -46,24 +48,28 @@ open class CachedRequest(
) )
NetworkConnection.getInstance(context).addToRequestQueue(request) NetworkConnection.getInstance(context).addToRequestQueue(request)
when (val response = tryGet(mainPageFuture)) { val response = tryGet(mainPageFuture)
is MyResult.Failure -> response if (response is MyResult.Failure)
is MyResult.Success -> { return@withContext response
val encodedMainPage = Base64.Default.encode(response.data.encodeToByteArray())
MyResult.Success(encodedMainPage) val pageData = (response as MyResult.Success).data
}
} val matcher = pattern.matcher(pageData)
} if (!matcher.find())
return@withContext MyResult.Failure(RuntimeException("Required url not found!"))
MyResult.Success("https://politehnikum-eng.ru" + matcher.group(1))
} }
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatus.ResponseDto> { private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatus.ResponseDto> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
when (val mainPage = getMainPage()) { when (val xlsUrl = getXlsUrl()) {
is MyResult.Failure -> mainPage is MyResult.Failure -> xlsUrl
is MyResult.Success -> { is MyResult.Success -> {
tryFuture { tryFuture {
ScheduleUpdate( ScheduleUpdate(
ScheduleUpdate.RequestDto(mainPage.data), ScheduleUpdate.RequestDto(xlsUrl.data),
context, context,
it, it,
it it

View File

@@ -15,7 +15,7 @@ class AuthChangePassword(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.POST, Method.POST,
"auth/change-password", "v1/auth/change-password",
{ listener.onResponse(null) }, { listener.onResponse(null) },
errorListener, errorListener,
canBeUnauthorized = true canBeUnauthorized = true

View File

@@ -5,34 +5,25 @@ import com.android.volley.Response
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
class AuthSignIn( class AuthSignIn(
private val data: RequestDto, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<ResponseDto>, listener: Response.Listener<Profile>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
) : RequestBase( ) : RequestBase(
context, context,
Method.POST, Method.POST,
"auth/sign-in", "v2/auth/sign-in",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@Serializable @Serializable
data class RequestDto(val username: String, val password: String) data class RequestDto(val username: String, val password: String)
@Serializable
data class ResponseDto(val id: String, val accessToken: String, val group: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["version"] = "2"
return headers
}
} }

View File

@@ -5,18 +5,19 @@ import com.android.volley.Response
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
class AuthSignUp( class AuthSignUp(
private val data: RequestDto, private val data: RequestDto,
context: Context, context: Context,
listener: Response.Listener<ResponseDto>, listener: Response.Listener<Profile>,
errorListener: Response.ErrorListener? errorListener: Response.ErrorListener?
) : RequestBase( ) : RequestBase(
context, context,
Method.POST, Method.POST,
"auth/sign-up", "v2/auth/sign-up",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@@ -28,9 +29,6 @@ class AuthSignUp(
val role: UserRole val role: UserRole
) )
@Serializable
data class ResponseDto(val id: String, val accessToken: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray {
return Json.encodeToString(data).toByteArray() return Json.encodeToString(data).toByteArray()
} }

View File

@@ -11,7 +11,7 @@ class FcmSetToken(
errorListener: Response.ErrorListener?, errorListener: Response.ErrorListener?,
) : AuthorizedRequest( ) : AuthorizedRequest(
context, Method.POST, context, Method.POST,
"fcm/set-token/$token", "v1/fcm/set-token/$token",
{ listener.onResponse(Unit) }, { listener.onResponse(Unit) },
errorListener, errorListener,
true true

View File

@@ -11,7 +11,7 @@ class FcmUpdateCallback(
errorListener: Response.ErrorListener?, errorListener: Response.ErrorListener?,
) : AuthorizedRequest( ) : AuthorizedRequest(
context, Method.POST, context, Method.POST,
"fcm/update-callback/$version", "v1/fcm/update-callback/$version",
{ listener.onResponse(Unit) }, { listener.onResponse(Unit) },
errorListener, errorListener,
true true

View File

@@ -15,7 +15,7 @@ class ProfileChangeGroup(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.POST, Method.POST,
"users/change-group", "v1/users/change-group",
{ listener.onResponse(null) }, { listener.onResponse(null) },
errorListener errorListener
) { ) {

View File

@@ -15,7 +15,7 @@ class ProfileChangeUsername(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.POST, Method.POST,
"users/change-username", "v1/users/change-username",
{ listener.onResponse(null) }, { listener.onResponse(null) },
errorListener errorListener
) { ) {

View File

@@ -13,7 +13,7 @@ class ProfileMe(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.GET, Method.GET,
"users/me", "v2/users/me",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) )

View File

@@ -13,8 +13,8 @@ class ScheduleGet(
errorListener: Response.ErrorListener? = null errorListener: Response.ErrorListener? = null
) : CachedRequest( ) : CachedRequest(
context, context,
Method.POST, Method.GET,
"schedule/get-group", "v2/schedule/group",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@@ -25,6 +25,6 @@ class ScheduleGet(
data class ResponseDto( data class ResponseDto(
val updatedAt: String, val updatedAt: String,
val group: Group, val group: Group,
val lastChangedDays: ArrayList<Int>, val updated: ArrayList<Int>,
) )
} }

View File

@@ -13,7 +13,7 @@ class ScheduleGetCacheStatus(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.GET, Method.GET,
"schedule/cache-status", "v2/schedule/cache-status",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@@ -24,11 +24,4 @@ class ScheduleGetCacheStatus(
val lastCacheUpdate: Long, val lastCacheUpdate: Long,
val lastScheduleUpdate: Long, val lastScheduleUpdate: Long,
) )
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["version"] = "1"
return headers
}
} }

View File

@@ -13,7 +13,7 @@ class ScheduleGetGroupNames(
) : RequestBase( ) : RequestBase(
context, context,
Method.GET, Method.GET,
"schedule/get-group-names", "v2/schedule/group-names",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {

View File

@@ -14,22 +14,13 @@ class ScheduleUpdate(
errorListener: Response.ErrorListener? = null errorListener: Response.ErrorListener? = null
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.POST, Method.PATCH,
"schedule/update-site-main-page", "v2/schedule/update-download-url",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {
@Serializable @Serializable
data class RequestDto(val mainPage: String) data class RequestDto(val url: String)
override fun getBody(): ByteArray { override fun getBody(): ByteArray = Json.encodeToString(data).toByteArray()
return Json.encodeToString(data).toByteArray()
}
override fun getHeaders(): MutableMap<String, String> {
val headers = super.getHeaders()
headers["version"] = "1"
return headers
}
} }

View File

@@ -13,7 +13,7 @@ class ScheduleReplacerClear(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.POST, Method.POST,
"schedule-replacer/clear", "v1/schedule-replacer/clear",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) { ) {

View File

@@ -13,7 +13,7 @@ class ScheduleReplacerGet(
) : AuthorizedRequest( ) : AuthorizedRequest(
context, context,
Method.GET, Method.GET,
"schedule-replacer/get", "v1/schedule-replacer/get",
{ listener.onResponse(Json.decodeFromString(it)) }, { listener.onResponse(Json.decodeFromString(it)) },
errorListener errorListener
) )

View File

@@ -14,7 +14,7 @@ class ScheduleReplacerSet(
) : AuthorizedMultipartRequest( ) : AuthorizedMultipartRequest(
context, context,
Method.POST, Method.POST,
"schedule-replacer/set", "v1/schedule-replacer/set",
{ listener.onResponse(null) }, { listener.onResponse(null) },
errorListener errorListener
) { ) {

View File

@@ -10,6 +10,7 @@ import android.os.IBinder
import android.os.Looper import android.os.Looper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat.startForegroundService import androidx.core.content.ContextCompat.startForegroundService
import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.NotificationChannels import ru.n08i40k.polytechnic.next.NotificationChannels
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
@@ -17,6 +18,7 @@ import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.fmtAsClock import ru.n08i40k.polytechnic.next.utils.fmtAsClock
import ru.n08i40k.polytechnic.next.utils.getDayMinutes import ru.n08i40k.polytechnic.next.utils.getDayMinutes
import java.util.Calendar import java.util.Calendar
@@ -64,7 +66,7 @@ class CurrentLessonViewService : Service() {
override fun run() { override fun run() {
val logger = Logger.getLogger("CLV.updateRunnable") val logger = Logger.getLogger("CLV.updateRunnable")
if (day == null || day!!.nonNullIndices.isEmpty()) { if (day == null || day!!.lessons.isEmpty()) {
logger.warning("Stopping, because day is null or empty!") logger.warning("Stopping, because day is null or empty!")
stopSelf() stopSelf()
return return
@@ -99,15 +101,16 @@ class CurrentLessonViewService : Service() {
return return
} }
val firstLessonIdx = day!!.distanceToNextByMinutes(0)?.first val firstLessonIdx =
day!!.distanceToNextByLocalDateTime(LocalDateTime(0, 0, 0, 0, 0))?.first
?: throw NullPointerException("Is this even real?") ?: throw NullPointerException("Is this even real?")
val distanceToFirst = day!!.lessons[firstLessonIdx]!!.time!!.start - currentMinutes val distanceToFirst = day!!.lessons[firstLessonIdx]!!.time!!.start.dayMinutes - currentMinutes
val currentLessonDelay = val currentLessonDelay =
if (currentLesson == null) // Если эта пара - перемена, то конец перемены через (результат getDistanceToNext) if (currentLesson == null) // Если эта пара - перемена, то конец перемены через (результат getDistanceToNext)
nextLessonEntry!!.second nextLessonEntry!!.second
else // Если эта пара - обычная пара, то конец пары через (конец этой пары - текущее кол-во минут) else // Если эта пара - обычная пара, то конец пары через (конец этой пары - текущее кол-во минут)
currentLesson.time!!.end - currentMinutes currentLesson.time!!.end.dayMinutes - currentMinutes
val currentLessonName = val currentLessonName =
currentLesson?.getNameAndCabinetsShort(this@CurrentLessonViewService) currentLesson?.getNameAndCabinetsShort(this@CurrentLessonViewService)
@@ -144,7 +147,7 @@ class CurrentLessonViewService : Service() {
getString( getString(
R.string.lesson_going_notification_description, R.string.lesson_going_notification_description,
currentLessonName, currentLessonName,
nextLessonTotal.fmtAsClock(), nextLessonTotal.dayMinutes.fmtAsClock(),
nextLessonName, nextLessonName,
) )
) )
@@ -183,7 +186,7 @@ class CurrentLessonViewService : Service() {
} }
val currentDay = group.current val currentDay = group.current
if (currentDay == null || currentDay.nonNullIndices.isEmpty()) { if (currentDay == null || currentDay.lessons.isEmpty()) {
logger.warning("Stopping, because current day is null or empty") logger.warning("Stopping, because current day is null or empty")
stopSelf() stopSelf()
return return
@@ -191,7 +194,7 @@ class CurrentLessonViewService : Service() {
val nowMinutes = Calendar.getInstance().getDayMinutes() val nowMinutes = Calendar.getInstance().getDayMinutes()
if (nowMinutes < ((5 * 60) + 30) if (nowMinutes < ((5 * 60) + 30)
|| currentDay.last!!.time.end < nowMinutes || currentDay.last!!.time.end.dayMinutes < nowMinutes
) { ) {
logger.warning("Stopping, because service started outside of acceptable time range!") logger.warning("Stopping, because service started outside of acceptable time range!")
stopSelf() stopSelf()

View File

@@ -28,7 +28,7 @@ import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
import java.time.Duration import java.time.Duration
class MyFirebaseMessagingService : FirebaseMessagingService() { class MyFirebaseMessagingService : FirebaseMessagingService() {
val scope = CoroutineScope(Job() + Dispatchers.Main) private val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
super.onNewToken(token) super.onNewToken(token)
@@ -53,7 +53,6 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
@DrawableRes iconId: Int, @DrawableRes iconId: Int,
title: String, title: String,
contentText: String, contentText: String,
priority: Int,
id: Any?, id: Any?,
intent: Intent? = null intent: Intent? = null
) { ) {
@@ -70,7 +69,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
.setSmallIcon(iconId) .setSmallIcon(iconId)
.setContentTitle(title) .setContentTitle(title)
.setContentText(contentText) .setContentText(contentText)
.setPriority(priority) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true) .setAutoCancel(true)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.build() .build()
@@ -103,7 +102,6 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
else else
R.string.schedule_update_default R.string.schedule_update_default
), ),
NotificationCompat.PRIORITY_DEFAULT,
message.data["etag"] message.data["etag"]
) )
} }
@@ -121,7 +119,6 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
R.drawable.download, R.drawable.download,
getString(R.string.app_update_title, message.data["version"]), getString(R.string.app_update_title, message.data["version"]),
getString(R.string.app_update_description), getString(R.string.app_update_description),
NotificationCompat.PRIORITY_DEFAULT,
message.data["version"], message.data["version"],
Intent(Intent.ACTION_VIEW, Uri.parse(message.data["downloadLink"])) Intent(Intent.ACTION_VIEW, Uri.parse(message.data["downloadLink"]))
) )

View File

@@ -29,7 +29,12 @@ val FilledGroup.Download: ImageVector
) { ) {
moveTo(3.0f, 12.3f) moveTo(3.0f, 12.3f)
verticalLineToRelative(7.0f) verticalLineToRelative(7.0f)
arcToRelative(2.0f, 2.0f, 0.0f, false, false, 2.0f, 2.0f) arcToRelative(2.0f, 2.0f, 0.0f,
isMoreThanHalf = false,
isPositiveArc = false,
dx1 = 2.0f,
dy1 = 2.0f
)
horizontalLineTo(19.0f) horizontalLineTo(19.0f)
arcToRelative(2.0f, 2.0f, 0.0f, false, false, 2.0f, -2.0f) arcToRelative(2.0f, 2.0f, 0.0f, false, false, 2.0f, -2.0f)
verticalLineToRelative(-7.0f) verticalLineToRelative(-7.0f)

View File

@@ -2,14 +2,11 @@ package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -20,7 +17,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -56,7 +52,7 @@ private fun getCurrentLessonIdx(day: Day?): Flow<Int> {
fun DayCard( fun DayCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
day: Day? = FakeScheduleRepository.exampleGroup.days[0], day: Day? = FakeScheduleRepository.exampleGroup.days[0],
current: Boolean = true distance: Int = 0
) { ) {
val defaultCardColors = CardDefaults.cardColors( val defaultCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer, containerColor = MaterialTheme.colorScheme.secondaryContainer,
@@ -75,7 +71,7 @@ fun DayCard(
modifier = modifier, modifier = modifier,
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = containerColor =
if (current) MaterialTheme.colorScheme.primaryContainer if (distance == 0) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.secondaryContainer
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface) border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
@@ -85,6 +81,7 @@ fun DayCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
text = stringResource(R.string.day_null) text = stringResource(R.string.day_null)
) )
return@Card return@Card
@@ -96,66 +93,55 @@ fun DayCard(
text = day.name, text = day.name,
) )
val currentLessonIdx by getCurrentLessonIdx(if (current) day else null) if (distance >= -1 && distance <= 1) {
Text(
modifier = Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
text = stringResource(when (distance) {
-1 -> R.string.yesterday
0 -> R.string.today
1 -> R.string.tommorow
else -> throw RuntimeException()
}),
)
}
val currentLessonIdx by getCurrentLessonIdx(if (distance == 0) day else null)
.collectAsStateWithLifecycle(0) .collectAsStateWithLifecycle(0)
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(0.5.dp) verticalArrangement = Arrangement.spacedBy(0.5.dp)
) { ) {
if (day.nonNullIndices.isEmpty()) { if (day.lessons.isEmpty()) {
Text("Can't get schedule!") Text("Can't get schedule!")
return@Column return@Column
} }
for (i in day.nonNullIndices.first()..day.nonNullIndices.last()) { for (lessonIdx in day.lessons.indices) {
val lesson = day.lessons[i]!! val lesson = day.lessons[lessonIdx]
val cardColors = when (lesson.type) { val cardColors = when (lesson.type) {
LessonType.DEFAULT -> defaultCardColors LessonType.DEFAULT -> defaultCardColors
LessonType.CUSTOM -> customCardColors LessonType.ADDITIONAL -> customCardColors
LessonType.BREAK -> noneCardColors
} }
val mutableExpanded = remember { mutableStateOf(false) } val mutableExpanded = remember { mutableStateOf(false) }
val lessonBoxModifier = remember { Box(
Modifier Modifier
.padding(PaddingValues(2.5.dp, 0.dp))
.clickable { mutableExpanded.value = true } .clickable { mutableExpanded.value = true }
.background(cardColors.containerColor) .background(cardColors.containerColor)
} ) {
val now = lessonIdx == currentLessonIdx
Box( if (lesson.type === LessonType.BREAK)
modifier = FreeLessonRow(lesson, lesson, cardColors, now)
if (i == currentLessonIdx) lessonBoxModifier.border( else
border = BorderStroke( LessonRow(day, lesson, cardColors, now)
3.5.dp,
Color(
cardColors.containerColor.red * 0.5F,
cardColors.containerColor.green * 0.5F,
cardColors.containerColor.blue * 0.5F,
1F
)
)
)
else lessonBoxModifier
) {
LessonRow(
day, lesson, cardColors
)
}
if (i != day.nonNullIndices.last()) {
Box(
modifier = Modifier
.padding(PaddingValues(2.5.dp, 0.dp))
.background(noneCardColors.containerColor)
) {
FreeLessonRow(
lesson,
day.lessons[day.nonNullIndices[day.nonNullIndices.indexOf(i) + 1]]!!,
noneCardColors
)
}
} }
if (mutableExpanded.value) if (mutableExpanded.value)

View File

@@ -27,10 +27,10 @@ import java.util.logging.Level
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
private fun isCurrentWeek(group: Group): Boolean { private fun isCurrentWeek(group: Group): Boolean {
if (group.days.size == 0 || group.days[0] == null) if (group.days.isEmpty())
return true return true
val dateString = group.days[0]!!.name val dateString = group.days[0].name
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale("ru")) val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale("ru"))
val datePart = dateString.split(" ").getOrNull(1) ?: return true val datePart = dateString.split(" ").getOrNull(1) ?: return true
@@ -49,10 +49,12 @@ private fun isCurrentWeek(group: Group): Boolean {
@Composable @Composable
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) { fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2) val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)
val calendarDay = currentDay val calendarDay = if (currentDay == -1) 6 else currentDay
.coerceAtLeast(0)
.coerceAtMost(group.days.size - 1) val pagerState = rememberPagerState(
val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size }) initialPage = calendarDay
.coerceAtMost(group.days.size - 1),
pageCount = { group.days.size })
Column { Column {
if (!isCurrentWeek(group)) { if (!isCurrentWeek(group)) {
@@ -65,7 +67,9 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
state = pagerState, state = pagerState,
contentPadding = PaddingValues(horizontal = 20.dp), contentPadding = PaddingValues(horizontal = 20.dp),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
modifier = Modifier.height(600.dp).padding(top = 5.dp) modifier = Modifier
.height(600.dp)
.padding(top = 5.dp)
) { page -> ) { page ->
DayCard( DayCard(
modifier = Modifier.graphicsLayer { modifier = Modifier.graphicsLayer {
@@ -82,7 +86,7 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
) )
}, },
day = group.days[page], day = group.days[page],
current = currentDay == page distance = page - currentDay
) )
} }
} }

View File

@@ -1,12 +1,13 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardColors import androidx.compose.material3.CardColors
@@ -26,11 +27,15 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.model.SubGroup
import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.fmtAsClock import ru.n08i40k.polytechnic.next.utils.fmtAsClock
private enum class LessonTimeFormat { private enum class LessonTimeFormat {
@@ -58,28 +63,36 @@ private fun fmtTime(start: Int, end: Int, format: LessonTimeFormat): ArrayList<S
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun LessonExtraInfo( fun LessonExtraInfo(
lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[0]!!, lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[0],
mutableExpanded: MutableState<Boolean> = mutableStateOf(true) mutableExpanded: MutableState<Boolean> = mutableStateOf(true)
) { ) {
Dialog(onDismissRequest = { mutableExpanded.value = false }) { Dialog(onDismissRequest = { mutableExpanded.value = false }) {
if (lesson.type === LessonType.BREAK) {
mutableExpanded.value = false
return@Dialog
}
Card { Card {
Column(Modifier.padding(10.dp)) { Column(Modifier.padding(10.dp)) {
Text(lesson.name) Text(lesson.name!!)
if (lesson.teacherNames.isNotEmpty()) { for (subGroup in lesson.subGroups) {
val teachers = buildString { val subGroups = buildString {
append(stringResource(if (lesson.teacherNames.count() > 1) R.string.lesson_teachers else R.string.lesson_teacher)) append("[")
append(subGroup.number)
append("] ")
append(subGroup.teacher)
append(" - ") append(" - ")
append(lesson.teacherNames.joinToString(", ")) append(subGroup.cabinet)
} }
Text(teachers) Text(subGroups)
} }
val duration = buildString { val duration = buildString {
append(stringResource(R.string.lesson_duration)) append(stringResource(R.string.lesson_duration))
append(" - ") append(" - ")
val duration = val duration =
lesson.time.end - lesson.time.start lesson.time.end.dayMinutes - lesson.time.start.dayMinutes
append(duration / 60) append(duration / 60)
append(stringResource(R.string.hours)) append(stringResource(R.string.hours))
@@ -88,15 +101,6 @@ fun LessonExtraInfo(
append(stringResource(R.string.minutes)) append(stringResource(R.string.minutes))
} }
Text(duration) Text(duration)
if (lesson.cabinets.isNotEmpty()) {
val cabinets = buildString {
append(stringResource(R.string.cabinets))
append(" - ")
append(lesson.cabinets.joinToString(", "))
}
Text(cabinets)
}
} }
} }
} }
@@ -105,47 +109,74 @@ fun LessonExtraInfo(
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun LessonViewRow( private fun LessonViewRow(
idx: Int = 1, range: List<Int>? = listOf(1, 3),
time: LessonTime? = LessonTime(0, 60), time: LessonTime = LessonTime.fromLocalDateTime(
LocalDateTime(2024, 1, 1, 0, 0),
LocalDateTime(2024, 1, 1, 1, 0),
),
timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO, timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
name: String = "Test", name: String = "Test",
teacherNames: ArrayList<String> = arrayListOf( subGroups: List<SubGroup> = listOf(),
"Хомченко Н.Е. (1 подggggggggggggggggggggggggggggggggggggggгруппа)",
"Хомченко Н.Е. (2 подгруппа)"
),
cabinets: ArrayList<String> = arrayListOf("14", "31"),
cardColors: CardColors = CardDefaults.cardColors(), cardColors: CardColors = CardDefaults.cardColors(),
verticalPadding: Dp = 10.dp verticalPadding: Dp = 10.dp,
now: Boolean = true,
) { ) {
val contentColor = val contentColor =
if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor else cardColors.disabledContentColor if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor
else cardColors.disabledContentColor
val teacherNamesRepl = teacherNames.map { it.replace("подгруппа", "подгр.") } val rangeSize = if (range == null) 1 else (range[1] - range[0] + 1) * 2
Box(
if (now) Modifier.border(
BorderStroke(
3.5.dp,
Color(
cardColors.containerColor.red * 0.5F,
cardColors.containerColor.green * 0.5F,
cardColors.containerColor.blue * 0.5F,
1F
)
)
) else Modifier
) {
Row( Row(
modifier = Modifier.padding(10.dp, verticalPadding), modifier = Modifier.padding(10.dp, verticalPadding * rangeSize),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
val rangeString = run {
if (range == null)
" "
else
buildString {
val same = range[0] == range[1]
append(if (same) " " else range[0])
append(if (same) range[0] else "-")
append(if (same) " " else range[1])
}
}
Text( Text(
text = if (idx == -1) "1" else idx.toString(), text = rangeString,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = if (idx == -1) Color(0) else contentColor color = contentColor
) )
Spacer(Modifier.width(7.5.dp))
if (time != null) {
val formattedTime: ArrayList<String> = fmtTime(time.start, time.end, timeFormat)
Column( Column(
modifier = Modifier.fillMaxWidth(0.25f), modifier = Modifier.fillMaxWidth(0.20f),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
val formattedTime: ArrayList<String> =
fmtTime(time.start.dayMinutes, time.end.dayMinutes, timeFormat)
Text( Text(
text = formattedTime[0], fontFamily = FontFamily.Monospace, color = contentColor text = formattedTime[0],
fontFamily = FontFamily.Monospace,
color = contentColor
) )
if (formattedTime.count() > 1) { if (formattedTime.count() > 1) {
Text( Text(
text = formattedTime[1], text = formattedTime[1],
@@ -154,16 +185,12 @@ private fun LessonViewRow(
) )
} }
} }
}
Spacer(Modifier.width(7.5.dp)) Column(verticalArrangement = Arrangement.Center) {
Column(
verticalArrangement = Arrangement.Center
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@@ -174,9 +201,9 @@ private fun LessonViewRow(
color = contentColor color = contentColor
) )
for (teacherName in teacherNamesRepl) { for (subGroup in subGroups) {
Text( Text(
text = teacherName, text = subGroup.teacher,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = contentColor color = contentColor
@@ -185,17 +212,13 @@ private fun LessonViewRow(
} }
Column(modifier = Modifier.wrapContentWidth()) { Column(modifier = Modifier.wrapContentWidth()) {
if (cabinets.size <= teacherNamesRepl.size) { if (subGroups.size != 1)
Text(text = "")
for (subGroup in subGroups) {
Text( Text(
text = "", text = subGroup.cabinet,
maxLines = 1
)
}
for (listIdx: Int in 0..<cabinets.size) {
Text(
text = cabinets[listIdx],
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, fontFamily = FontFamily.Monospace,
color = contentColor color = contentColor
) )
} }
@@ -204,42 +227,45 @@ private fun LessonViewRow(
} }
} }
}
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun FreeLessonRow( fun FreeLessonRow(
lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[0]!!, lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[0],
nextLesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[1]!!, nextLesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[1],
cardColors: CardColors = CardDefaults.cardColors() cardColors: CardColors = CardDefaults.cardColors(),
now: Boolean = true
) { ) {
LessonViewRow( LessonViewRow(
-1, lesson.defaultRange,
LessonTime(lesson.time.end, nextLesson.time.start), LessonTime(lesson.time.start, nextLesson.time.end),
LessonTimeFormat.ONLY_MINUTES_DURATION, LessonTimeFormat.ONLY_MINUTES_DURATION,
stringResource(R.string.lesson_break), stringResource(R.string.lesson_break),
arrayListOf(), lesson.subGroups,
arrayListOf(),
cardColors, cardColors,
2.5.dp 2.5.dp,
now
) )
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun LessonRow( fun LessonRow(
day: Day = FakeScheduleRepository.exampleGroup.days[0]!!, day: Day = FakeScheduleRepository.exampleGroup.days[0],
lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[0]!!, lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[0],
cardColors: CardColors = CardDefaults.cardColors() cardColors: CardColors = CardDefaults.cardColors(),
now: Boolean = true,
) { ) {
LessonViewRow( LessonViewRow(
lesson.defaultIndex, lesson.defaultRange,
lesson.time, lesson.time,
LessonTimeFormat.FROM_TO, LessonTimeFormat.FROM_TO,
lesson.name, lesson.name!!,
lesson.teacherNames, lesson.subGroups,
lesson.cabinets,
cardColors, cardColors,
5.dp 5.dp,
now
) )
} }

View File

@@ -0,0 +1,16 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import ru.n08i40k.polytechnic.next.R
@Preview(showSystemUi = true, showBackground = true)
@Composable
internal fun PaskhalkoDialog() {
Dialog(onDismissRequest = {}) {
Image(painterResource(R.drawable.paskhalko), contentDescription = "Paskhalko")
}
}

View File

@@ -1,5 +1,6 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -8,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -52,10 +54,16 @@ fun UpdateInfo(
onExpandedChange = { expanded = !expanded }, onExpandedChange = { expanded = !expanded },
title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) } title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) }
) { ) {
var paskhalkoCounter by remember { mutableIntStateOf(0) }
if (paskhalkoCounter >= 10)
PaskhalkoDialog()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp) .padding(10.dp)
.clickable { ++paskhalkoCounter }
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,

View File

@@ -323,7 +323,7 @@ fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable() () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {

View File

@@ -1,5 +1,10 @@
package ru.n08i40k.polytechnic.next.utils package ru.n08i40k.polytechnic.next.utils
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import java.util.Calendar import java.util.Calendar
infix fun <T> T?.or(data: T): T { infix fun <T> T?.or(data: T): T {
@@ -31,3 +36,17 @@ infix fun String.limit(count: Int): String {
fun Calendar.getDayMinutes(): Int = fun Calendar.getDayMinutes(): Int =
this.get(Calendar.HOUR_OF_DAY) * 60 + this.get(Calendar.MINUTE) this.get(Calendar.HOUR_OF_DAY) * 60 + this.get(Calendar.MINUTE)
val Instant.dayMinutes: Int
get() = this.toLocalDateTime(TimeZone.currentSystemDefault()).dayMinutes
val LocalDateTime.dayMinutes: Int
get() = this.hour * 60 + this.minute
val Instant.dateTime: LocalDateTime
get() = this.toLocalDateTime(TimeZone.currentSystemDefault())
fun LocalDateTime.Companion.now(): LocalDateTime {
val clock = Clock.System.now()
return clock.toLocalDateTime(TimeZone.currentSystemDefault())
}

View File

@@ -5,7 +5,6 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.service.CurrentLessonViewService
class LinkUpdateWorker(context: Context, params: WorkerParameters) : class LinkUpdateWorker(context: Context, params: WorkerParameters) :
Worker(context, params) { Worker(context, params) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -73,4 +73,7 @@
<string name="already_exists">Пользователь с таким именем уже зарегистрирован!</string> <string name="already_exists">Пользователь с таким именем уже зарегистрирован!</string>
<string name="group_does_not_exists">Группа с таким названием не существует!</string> <string name="group_does_not_exists">Группа с таким названием не существует!</string>
<string name="no_connection">Нет подключения к интернету!</string> <string name="no_connection">Нет подключения к интернету!</string>
<string name="today">Сегодня</string>
<string name="yesterday">Вчера</string>
<string name="tommorow">Завтра</string>
</resources> </resources>

View File

@@ -73,4 +73,7 @@
<string name="already_exists">A user with this name is already registered!</string> <string name="already_exists">A user with this name is already registered!</string>
<string name="group_does_not_exists">A group with this name does not exist!</string> <string name="group_does_not_exists">A group with this name does not exist!</string>
<string name="no_connection">No internet connection!</string> <string name="no_connection">No internet connection!</string>
<string name="today">Today</string>
<string name="yesterday">Yesterday</string>
<string name="tommorow">Tommorow</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
[versions] [versions]
accompanistSwiperefresh = "0.36.0" accompanistSwiperefresh = "0.36.0"
agp = "8.7.0" agp = "8.7.1"
firebaseBom = "33.4.0" firebaseBom = "33.4.0"
hiltAndroid = "2.52" hiltAndroid = "2.52"
hiltAndroidCompiler = "2.52" hiltAndroidCompiler = "2.52"
@@ -12,12 +12,12 @@ junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
kotlinxSerializationJson = "1.7.3" kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.6" lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2" activityCompose = "1.9.3"
composeBom = "2024.09.03" composeBom = "2024.10.00"
protobufLite = "3.0.1" protobufLite = "3.0.1"
volley = "1.2.1" volley = "1.2.1"
datastore = "1.1.1" datastore = "1.1.1"
navigationCompose = "2.8.2" navigationCompose = "2.8.3"
googleFirebaseCrashlytics = "3.0.2" googleFirebaseCrashlytics = "3.0.2"
workRuntime = "2.9.1" workRuntime = "2.9.1"
@@ -40,9 +40,10 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.4" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 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" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" } protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" } volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }