Compare commits
6 Commits
7f613a2106
...
790a5fabcd
Author | SHA1 | Date | |
---|---|---|---|
790a5fabcd | |||
1286860d55 | |||
654acf1b77 | |||
0cad16ac74 | |||
0c57d0b7e7 | |||
bc7e2b4100 |
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<UNN HUB - unofficial app providing schedule, news and document access for students>
|
||||||
|
Copyright (C) 2025 Gleb Zaharov
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
70
NOTICE.md
Normal file
70
NOTICE.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# NOTICE
|
||||||
|
|
||||||
|
This project incorporates components from the following third-party libraries:
|
||||||
|
|
||||||
|
## AndroidX Libraries
|
||||||
|
- **androidx.*** (Core, Room, Navigation, Compose, etc.)
|
||||||
|
Copyright The Android Open Source Project
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
[https://developer.android.com](https://developer.android.com)
|
||||||
|
|
||||||
|
## ACRA
|
||||||
|
- **ch.acra:acra-http**
|
||||||
|
Copyright ACRA
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
[https://github.com/ACRA/acra](https://github.com/ACRA/acra)
|
||||||
|
|
||||||
|
## Coil
|
||||||
|
- **io.coil-kt:coil-compose**
|
||||||
|
Copyright Coil Contributors
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
[https://github.com/coil-kt/coil](https://github.com/coil-kt/coil)
|
||||||
|
|
||||||
|
## Compose Calendar
|
||||||
|
- **com.kizitonwose.calendar:compose**
|
||||||
|
Copyright Kizito Nwose
|
||||||
|
Licensed under MIT License
|
||||||
|
[https://github.com/kizitonwose/Calendar](https://github.com/kizitonwose/Calendar)
|
||||||
|
|
||||||
|
## Desugar JDK Libs
|
||||||
|
- **com.android.tools:desugar_jdk_libs**
|
||||||
|
Copyright Google LLC
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
|
||||||
|
## Ktor
|
||||||
|
- **io.ktor:ktor-***
|
||||||
|
Copyright JetBrains s.r.o.
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
[https://ktor.io](https://ktor.io)
|
||||||
|
|
||||||
|
## Material Components
|
||||||
|
- **com.google.android.material:material**
|
||||||
|
Copyright Google LLC
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
|
||||||
|
## Splitties
|
||||||
|
- **com.louiscad.splitties:***
|
||||||
|
Copyright Louis CAD
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
[https://github.com/LouisCAD/Splitties](https://github.com/LouisCAD/Splitties)
|
||||||
|
|
||||||
|
## Secrets Gradle Plugin
|
||||||
|
- **com.google.android.libraries.mapsplatform.secrets-gradle-plugin**
|
||||||
|
Copyright Google LLC
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
|
||||||
|
## Build Tools
|
||||||
|
- **Gradle Plugins** (Android, Kotlin)
|
||||||
|
Copyright Google LLC / JetBrains s.r.o.
|
||||||
|
Licensed under Apache License 2.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### License Notices
|
||||||
|
This product includes software developed by:
|
||||||
|
- The Android Open Source Project (AOSP)
|
||||||
|
- JetBrains s.r.o.
|
||||||
|
- Google LLC
|
||||||
|
- And other contributors listed above
|
||||||
|
|
||||||
|
Full license texts are available in the [LICENSE](LICENSE) file and respective library repositories.
|
@ -1,7 +1,4 @@
|
|||||||
/*
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
* Created by sweetbread
|
|
||||||
* Copyright (c) 2025. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn
|
package ru.sweetbread.unn
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn
|
package ru.sweetbread.unn
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.acra.config.httpSender
|
|
||||||
import org.acra.data.StringFormat
|
|
||||||
import org.acra.ktx.initAcra
|
|
||||||
import org.acra.sender.HttpSender
|
|
||||||
|
|
||||||
class UNNApp : Application() {
|
class UNNApp : Application() {
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
|
96
app/src/main/java/ru/sweetbread/unn/api/API.kt
Normal file
96
app/src/main/java/ru/sweetbread/unn/api/API.kt
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
|
package ru.sweetbread.unn.api
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import ru.sweetbread.unn.db.cacheSchedule
|
||||||
|
import ru.sweetbread.unn.db.loadSchedule
|
||||||
|
import ru.sweetbread.unn.ui.layout.LoginData
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
|
||||||
|
lateinit var PHPSESSID: String
|
||||||
|
lateinit var CSRF: String
|
||||||
|
lateinit var ME: User
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize user by [login] and [password]
|
||||||
|
*
|
||||||
|
* Also defines local vars [PHPSESSID] and [ME]
|
||||||
|
*/
|
||||||
|
suspend fun auth(
|
||||||
|
login: String = LoginData.login,
|
||||||
|
password: String = LoginData.password,
|
||||||
|
forced: Boolean = false
|
||||||
|
): Boolean {
|
||||||
|
if (!forced) {
|
||||||
|
if (::PHPSESSID.isInitialized and ::ME.isInitialized)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
val id = getToken(login, password)
|
||||||
|
if (id != null) {
|
||||||
|
PHPSESSID = id
|
||||||
|
getMyself(login)
|
||||||
|
getCSRF()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save info about current [User] in memory
|
||||||
|
*/
|
||||||
|
private suspend fun getMyself(login: String) = coroutineScope {
|
||||||
|
val idDeferred = async { getId(login) }
|
||||||
|
val userDeferred = async { getUser() }
|
||||||
|
|
||||||
|
val id = idDeferred.await()
|
||||||
|
val user = userDeferred.await()
|
||||||
|
|
||||||
|
ME = User(
|
||||||
|
unnId = id,
|
||||||
|
userId = user.userId,
|
||||||
|
type = Type.student,
|
||||||
|
email = user.email,
|
||||||
|
nameRu = user.nameRu,
|
||||||
|
nameEn = user.nameEn,
|
||||||
|
isMale = user.isMale,
|
||||||
|
birthday = user.birthday,
|
||||||
|
avatar = user.avatar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getScheduleDay(
|
||||||
|
type: Type = ME.type,
|
||||||
|
id: Int = ME.unnId!!,
|
||||||
|
date: LocalDate
|
||||||
|
): ArrayList<ScheduleUnit> {
|
||||||
|
|
||||||
|
if ((type == ME.type) and (id == ME.unnId!!)) {
|
||||||
|
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
|
||||||
|
Log.d("Schedule", schedule.joinToString())
|
||||||
|
if (schedule.isNotEmpty())
|
||||||
|
return schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
return getSchedule(type, id, date, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSchedule(
|
||||||
|
type: Type = ME.type,
|
||||||
|
id: Int = ME.unnId!!,
|
||||||
|
start: LocalDate,
|
||||||
|
finish: LocalDate
|
||||||
|
): ArrayList<ScheduleUnit> {
|
||||||
|
val schedule = downloadSchedule(type, id, start, finish)
|
||||||
|
|
||||||
|
if ((type == ME.type) and (id == ME.unnId!!))
|
||||||
|
cacheSchedule(schedule)
|
||||||
|
|
||||||
|
return schedule
|
||||||
|
}
|
@ -1,6 +1,16 @@
|
|||||||
package ru.sweetbread.unn
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
|
package ru.sweetbread.unn.api
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.android.Android
|
||||||
|
import io.ktor.client.plugins.HttpRequestRetry
|
||||||
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
|
import io.ktor.client.plugins.cache.HttpCache
|
||||||
|
import io.ktor.client.plugins.logging.LogLevel
|
||||||
|
import io.ktor.client.plugins.logging.Logger
|
||||||
|
import io.ktor.client.plugins.logging.Logging
|
||||||
import io.ktor.client.request.forms.submitForm
|
import io.ktor.client.request.forms.submitForm
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
@ -8,17 +18,11 @@ import io.ktor.client.request.parameter
|
|||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.parameters
|
import io.ktor.http.parameters
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import ru.sweetbread.unn.db.cacheSchedule
|
|
||||||
import ru.sweetbread.unn.db.cacheUser
|
import ru.sweetbread.unn.db.cacheUser
|
||||||
import ru.sweetbread.unn.db.loadSchedule
|
|
||||||
import ru.sweetbread.unn.db.loadUserByBitrixId
|
import ru.sweetbread.unn.db.loadUserByBitrixId
|
||||||
import ru.sweetbread.unn.ui.layout.LoginData
|
|
||||||
import ru.sweetbread.unn.ui.layout.client
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -27,10 +31,6 @@ import java.time.ZoneId
|
|||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
||||||
private lateinit var PHPSESSID: String
|
|
||||||
private lateinit var CSRF: String
|
|
||||||
lateinit var ME: User
|
|
||||||
|
|
||||||
const val portalURL = "https://portal.unn.ru"
|
const val portalURL = "https://portal.unn.ru"
|
||||||
const val ruzapiURL = "$portalURL/ruzapi"
|
const val ruzapiURL = "$portalURL/ruzapi"
|
||||||
const val vuzapiURL = "$portalURL/bitrix/vuz/api"
|
const val vuzapiURL = "$portalURL/bitrix/vuz/api"
|
||||||
@ -38,110 +38,29 @@ const val prtl2URL = "$portalURL/portal2/api"
|
|||||||
const val restURL = "$portalURL/rest"
|
const val restURL = "$portalURL/rest"
|
||||||
|
|
||||||
|
|
||||||
enum class Type(val s: String) {
|
val client = HttpClient(Android) {
|
||||||
Student("student"),
|
install(HttpCache)
|
||||||
Group("group"),
|
install(Logging) {
|
||||||
Lecturer("lecturer"),
|
logger = object : Logger {
|
||||||
Auditorium("auditorium"),
|
override fun log(message: String) {
|
||||||
Employee("employee")
|
Log.i("Ktor", message)
|
||||||
}
|
|
||||||
|
|
||||||
enum class LecturerRank(val id: Int) {
|
|
||||||
Assistant(R.string.assistant),
|
|
||||||
Lecturer(R.string.lecturer),
|
|
||||||
SLecturer(R.string.slecturer),
|
|
||||||
AProfessor(R.string.aprofessor)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ScheduleUnit(
|
|
||||||
val oid: Int,
|
|
||||||
val auditorium: Auditorium,
|
|
||||||
val date: LocalDate,
|
|
||||||
val discipline: Discipline,
|
|
||||||
val kindOfWork: KindOfWork,
|
|
||||||
val lecturers: ArrayList<Lecturer>,
|
|
||||||
val stream: String,
|
|
||||||
val begin: LocalTime,
|
|
||||||
val end: LocalTime
|
|
||||||
)
|
|
||||||
|
|
||||||
class Auditorium(
|
|
||||||
val name: String,
|
|
||||||
val oid: Int,
|
|
||||||
val floor: Int,
|
|
||||||
val building: Building
|
|
||||||
)
|
|
||||||
|
|
||||||
class Building(
|
|
||||||
val name: String,
|
|
||||||
val gid: Int,
|
|
||||||
val oid: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
class Discipline(
|
|
||||||
val name: String,
|
|
||||||
val oid: Int,
|
|
||||||
val type: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
class KindOfWork(
|
|
||||||
val name: String,
|
|
||||||
val oid: Int,
|
|
||||||
val uid: String,
|
|
||||||
val complexity: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
class Lecturer(
|
|
||||||
val name: String,
|
|
||||||
val rank: LecturerRank,
|
|
||||||
val email: String,
|
|
||||||
val unnId: Int,
|
|
||||||
val uid: String
|
|
||||||
)
|
|
||||||
|
|
||||||
class User(
|
|
||||||
val unnId: Int?,
|
|
||||||
val bitrixId: Int,
|
|
||||||
val userId: Int,
|
|
||||||
val type: Type,
|
|
||||||
val email: String,
|
|
||||||
val nameRu: String,
|
|
||||||
val nameEn: String,
|
|
||||||
val isMale: Boolean,
|
|
||||||
val birthday: LocalDate,
|
|
||||||
val avatar: AvatarSet
|
|
||||||
)
|
|
||||||
|
|
||||||
class Post(
|
|
||||||
val id: Int,
|
|
||||||
val authorId: Int,
|
|
||||||
val enableComments: Boolean,
|
|
||||||
val numComments: Int,
|
|
||||||
val date: LocalDateTime,
|
|
||||||
val content: String
|
|
||||||
)
|
|
||||||
|
|
||||||
class AvatarSet(
|
|
||||||
val original: String,
|
|
||||||
val thumbnail: String,
|
|
||||||
val small: String
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authorize user by [login] and [password]
|
|
||||||
*
|
|
||||||
* Also defines local vars [PHPSESSID] and [ME]
|
|
||||||
*/
|
|
||||||
suspend fun auth(
|
|
||||||
login: String = LoginData.login,
|
|
||||||
password: String = LoginData.password,
|
|
||||||
forced: Boolean = false
|
|
||||||
): Boolean {
|
|
||||||
if (!forced) {
|
|
||||||
if (::PHPSESSID.isInitialized and ::ME.isInitialized)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
level = LogLevel.ALL
|
||||||
|
}
|
||||||
|
install(HttpTimeout) {
|
||||||
|
connectTimeoutMillis = 5000
|
||||||
|
}
|
||||||
|
install(HttpRequestRetry) {
|
||||||
|
retryOnException(maxRetries = 3, retryOnTimeout = true)
|
||||||
|
exponentialDelay()
|
||||||
|
modifyRequest { request ->
|
||||||
|
request.headers.append("x-retry-count", retryCount.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getToken(login: String, password: String): String? {
|
||||||
val r = client.submitForm("$portalURL/auth/?login=yes",
|
val r = client.submitForm("$portalURL/auth/?login=yes",
|
||||||
formParameters = parameters {
|
formParameters = parameters {
|
||||||
append("AUTH_FORM", "Y")
|
append("AUTH_FORM", "Y")
|
||||||
@ -151,51 +70,43 @@ suspend fun auth(
|
|||||||
append("USER_PASSWORD", password)
|
append("USER_PASSWORD", password)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (r.status.value == 302) {
|
|
||||||
PHPSESSID =
|
if (r.status.value == 302)
|
||||||
"""PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
|
return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
|
||||||
getMyself(login)
|
return null
|
||||||
getCSRF()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun getId(login: String): Int {
|
||||||
* Save info about current [User] in memory
|
return JSONObject(client.get("$ruzapiURL/studentinfo/") {
|
||||||
*/
|
|
||||||
private suspend fun getMyself(login: String) {
|
|
||||||
// WARNING: trailing / is important, 'cuz API devs are eating shit
|
|
||||||
val studentinfo = JSONObject(client.get("$ruzapiURL/studentinfo/") {
|
|
||||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||||
parameter("uns", login.drop(1))
|
parameter("uns", login.drop(1))
|
||||||
}.bodyAsText())
|
}.bodyAsText()).getString("id").toInt()
|
||||||
|
}
|
||||||
|
|
||||||
val user = JSONObject(
|
suspend fun getUser(userId: Int? = null): User {
|
||||||
client.get("$vuzapiURL/user") {
|
// WARNING: trailing / is important, 'cuz API devs are eating shit
|
||||||
|
// TODO: make up another way to get unnId: this is not useful for lectures
|
||||||
|
val json = JSONObject(
|
||||||
|
client.get("$vuzapiURL/user/${userId ?: ""}") {
|
||||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||||
}.bodyAsText()
|
}.bodyAsText()
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d("studentInfo", studentinfo.toString(2))
|
return User(
|
||||||
|
unnId = null,
|
||||||
ME = User(
|
userId = json.getInt("id"),
|
||||||
unnId = studentinfo.getString("id").toInt(),
|
type = if (json.getJSONArray("profiles").getJSONObject(0)
|
||||||
bitrixId = user.getInt("bitrix_id"),
|
.getString("type") == "employee"
|
||||||
userId = user.getInt("id"),
|
) Type.employee else Type.student,
|
||||||
type = when (studentinfo.getString("type")) {
|
email = json.getString("email"),
|
||||||
"lecturer" -> Type.Lecturer // ig,,,
|
nameRu = json.getString("fullname"),
|
||||||
else -> Type.Student
|
nameEn = json.getString("fullname_en"),
|
||||||
},
|
isMale = json.getString("sex") == "M",
|
||||||
email = user.getString("email"),
|
|
||||||
nameRu = user.getString("fullname"),
|
|
||||||
nameEn = user.getString("fullname_en"),
|
|
||||||
isMale = user.getString("sex") == "M",
|
|
||||||
birthday = Instant
|
birthday = Instant
|
||||||
.parse(user.getString("birthdate"))
|
.parse(json.getString("birthdate"))
|
||||||
.atZone(ZoneId.of("Europe/Moscow"))
|
.atZone(ZoneId.of("Europe/Moscow"))
|
||||||
.toLocalDate(),
|
.toLocalDate(),
|
||||||
avatar = user.getJSONObject("photo").let {
|
avatar = json.getJSONObject("photo").let {
|
||||||
AvatarSet(
|
AvatarSet(
|
||||||
it.getString("orig"),
|
it.getString("orig"),
|
||||||
it.getString("thumbnail"),
|
it.getString("thumbnail"),
|
||||||
@ -205,23 +116,7 @@ private suspend fun getMyself(login: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getScheduleDay(
|
suspend fun downloadSchedule(
|
||||||
type: Type = ME.type,
|
|
||||||
id: Int = ME.unnId!!,
|
|
||||||
date: LocalDate
|
|
||||||
): ArrayList<ScheduleUnit> {
|
|
||||||
|
|
||||||
if ((type == ME.type) and (id == ME.unnId!!)) {
|
|
||||||
val schedule = withContext(Dispatchers.IO) { loadSchedule(date) }
|
|
||||||
Log.d("Schedule", schedule.joinToString())
|
|
||||||
if (schedule.isNotEmpty())
|
|
||||||
return schedule
|
|
||||||
}
|
|
||||||
|
|
||||||
return getSchedule(type, id, date, date)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSchedule(
|
|
||||||
type: Type = ME.type,
|
type: Type = ME.type,
|
||||||
id: Int = ME.unnId!!,
|
id: Int = ME.unnId!!,
|
||||||
start: LocalDate,
|
start: LocalDate,
|
||||||
@ -229,13 +124,13 @@ suspend fun getSchedule(
|
|||||||
): ArrayList<ScheduleUnit> {
|
): ArrayList<ScheduleUnit> {
|
||||||
val unnDatePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
val unnDatePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||||
|
|
||||||
val r = client.get("$ruzapiURL/schedule/${type.s}/$id") {
|
val r = client.get("$ruzapiURL/schedule/${type.name}/$id") {
|
||||||
parameter("start", start.format(unnDatePattern))
|
parameter("start", start.format(unnDatePattern))
|
||||||
parameter("finish", finish.format(unnDatePattern))
|
parameter("finish", finish.format(unnDatePattern))
|
||||||
parameter("lng", "1")
|
parameter("lng", "1")
|
||||||
}
|
}
|
||||||
val json = JSONArray(r.bodyAsText())
|
|
||||||
|
|
||||||
|
val json = JSONArray(r.bodyAsText())
|
||||||
val out = arrayListOf<ScheduleUnit>()
|
val out = arrayListOf<ScheduleUnit>()
|
||||||
for (i in 0 until json.length()) {
|
for (i in 0 until json.length()) {
|
||||||
val unit = json.getJSONObject(i)
|
val unit = json.getJSONObject(i)
|
||||||
@ -299,9 +194,6 @@ suspend fun getSchedule(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type == ME.type) and (id == ME.unnId!!)) {
|
|
||||||
cacheSchedule(out)
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,6 +205,23 @@ suspend fun getCSRF() {
|
|||||||
CSRF = JSONObject(r.bodyAsText()).getString("sessid")
|
CSRF = JSONObject(r.bodyAsText()).getString("sessid")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getUserByBitrixId(id: Int): User {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
loadUserByBitrixId(id)
|
||||||
|
}?.let { return it }
|
||||||
|
|
||||||
|
val userId = JSONObject(client.get("$vuzapiURL/user/bx/$id") {
|
||||||
|
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||||
|
}.bodyAsText()).getInt("id")
|
||||||
|
|
||||||
|
getUser(userId).let { user ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
cacheUser(user)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getBlogposts(): ArrayList<Post> {
|
suspend fun getBlogposts(): ArrayList<Post> {
|
||||||
val r = client.get("$prtl2URL/news.php") {
|
val r = client.get("$prtl2URL/news.php") {
|
||||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
header("Cookie", "PHPSESSID=$PHPSESSID")
|
||||||
@ -339,54 +248,3 @@ suspend fun getBlogposts(): ArrayList<Post> {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUserByBitrixId(id: Int): User {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
loadUserByBitrixId(id)
|
|
||||||
}?.let { return it }
|
|
||||||
|
|
||||||
val userId = JSONObject(client.get("$vuzapiURL/user/bx/$id") {
|
|
||||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
|
||||||
}.bodyAsText()).getInt("id")
|
|
||||||
|
|
||||||
getUser(userId).let { user ->
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
cacheUser(user)
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUser(id: Int): User {
|
|
||||||
val json = JSONObject(
|
|
||||||
client.get("$vuzapiURL/user/$id") {
|
|
||||||
header("Cookie", "PHPSESSID=$PHPSESSID")
|
|
||||||
}.bodyAsText()
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.d("type", json.getJSONArray("profiles").getJSONObject(0).getString("type"))
|
|
||||||
|
|
||||||
return User(
|
|
||||||
unnId = null,
|
|
||||||
bitrixId = json.getInt("bitrix_id"),
|
|
||||||
userId = json.getInt("id"),
|
|
||||||
type = if (json.getJSONArray("profiles").getJSONObject(0)
|
|
||||||
.getString("type") == "employee"
|
|
||||||
) Type.Employee else Type.Student,
|
|
||||||
email = json.getString("email"),
|
|
||||||
nameRu = json.getString("fullname"),
|
|
||||||
nameEn = json.getString("fullname_en"),
|
|
||||||
isMale = json.getString("sex") == "M",
|
|
||||||
birthday = Instant
|
|
||||||
.parse(json.getString("birthdate"))
|
|
||||||
.atZone(ZoneId.of("Europe/Moscow"))
|
|
||||||
.toLocalDate(),
|
|
||||||
avatar = json.getJSONObject("photo").let {
|
|
||||||
AvatarSet(
|
|
||||||
it.getString("orig"),
|
|
||||||
it.getString("thumbnail"),
|
|
||||||
it.getString("small"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
97
app/src/main/java/ru/sweetbread/unn/api/models.kt
Normal file
97
app/src/main/java/ru/sweetbread/unn/api/models.kt
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
|
package ru.sweetbread.unn.api
|
||||||
|
|
||||||
|
import ru.sweetbread.unn.R
|
||||||
|
import splitties.resources.appStr
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
enum class Type(val s: String) {
|
||||||
|
student(appStr(R.string.student)),
|
||||||
|
group("group"),
|
||||||
|
lecturer(appStr(R.string.lecturer)),
|
||||||
|
auditorium("auditorium"),
|
||||||
|
employee(appStr(R.string.employee))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class LecturerRank(val id: Int) {
|
||||||
|
Assistant(R.string.assistant),
|
||||||
|
Lecturer(R.string.lecturer),
|
||||||
|
SLecturer(R.string.slecturer),
|
||||||
|
AProfessor(R.string.aprofessor)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScheduleUnit(
|
||||||
|
val oid: Int,
|
||||||
|
val auditorium: Auditorium,
|
||||||
|
val date: LocalDate,
|
||||||
|
val discipline: Discipline,
|
||||||
|
val kindOfWork: KindOfWork,
|
||||||
|
val lecturers: ArrayList<Lecturer>,
|
||||||
|
val stream: String,
|
||||||
|
val begin: LocalTime,
|
||||||
|
val end: LocalTime
|
||||||
|
)
|
||||||
|
|
||||||
|
class Auditorium(
|
||||||
|
val name: String,
|
||||||
|
val oid: Int,
|
||||||
|
val floor: Int,
|
||||||
|
val building: Building
|
||||||
|
)
|
||||||
|
|
||||||
|
class Building(
|
||||||
|
val name: String,
|
||||||
|
val gid: Int,
|
||||||
|
val oid: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
class Discipline(
|
||||||
|
val name: String,
|
||||||
|
val oid: Int,
|
||||||
|
val type: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
class KindOfWork(
|
||||||
|
val name: String,
|
||||||
|
val oid: Int,
|
||||||
|
val uid: String,
|
||||||
|
val complexity: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
class Lecturer(
|
||||||
|
val name: String,
|
||||||
|
val rank: LecturerRank,
|
||||||
|
val email: String,
|
||||||
|
val unnId: Int,
|
||||||
|
val uid: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class User(
|
||||||
|
val unnId: Int?,
|
||||||
|
val userId: Int,
|
||||||
|
val type: Type,
|
||||||
|
val email: String,
|
||||||
|
val nameRu: String,
|
||||||
|
val nameEn: String,
|
||||||
|
val isMale: Boolean,
|
||||||
|
val birthday: LocalDate,
|
||||||
|
val avatar: AvatarSet
|
||||||
|
)
|
||||||
|
|
||||||
|
class Post(
|
||||||
|
val id: Int,
|
||||||
|
val authorId: Int,
|
||||||
|
val enableComments: Boolean,
|
||||||
|
val numComments: Int,
|
||||||
|
val date: LocalDateTime,
|
||||||
|
val content: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class AvatarSet(
|
||||||
|
val original: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val small: String
|
||||||
|
)
|
@ -1,7 +1,14 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn.db
|
package ru.sweetbread.unn.db
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import splitties.arch.room.roomDb
|
||||||
|
|
||||||
|
val cacheDb = roomDb<AppDatabase>(name = "cache") {
|
||||||
|
fallbackToDestructiveMigration(dropAllTables = true)
|
||||||
|
}
|
||||||
|
|
||||||
@Database(entities = [
|
@Database(entities = [
|
||||||
UserDB::class,
|
UserDB::class,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn.db
|
package ru.sweetbread.unn.db
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -9,14 +11,13 @@ import androidx.room.Insert
|
|||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import ru.sweetbread.unn.Auditorium
|
import ru.sweetbread.unn.api.Auditorium
|
||||||
import ru.sweetbread.unn.Building
|
import ru.sweetbread.unn.api.Building
|
||||||
import ru.sweetbread.unn.Discipline
|
import ru.sweetbread.unn.api.Discipline
|
||||||
import ru.sweetbread.unn.KindOfWork
|
import ru.sweetbread.unn.api.KindOfWork
|
||||||
import ru.sweetbread.unn.Lecturer
|
import ru.sweetbread.unn.api.Lecturer
|
||||||
import ru.sweetbread.unn.LecturerRank
|
import ru.sweetbread.unn.api.LecturerRank
|
||||||
import ru.sweetbread.unn.ScheduleUnit
|
import ru.sweetbread.unn.api.ScheduleUnit
|
||||||
import ru.sweetbread.unn.ui.layout.db
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
@ -43,7 +44,7 @@ interface BuildingDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cacheBuilding(building: Building) {
|
fun cacheBuilding(building: Building) {
|
||||||
db.buildingDao().insert(
|
cacheDb.buildingDao().insert(
|
||||||
BuildingDB(
|
BuildingDB(
|
||||||
building.oid,
|
building.oid,
|
||||||
building.name,
|
building.name,
|
||||||
@ -54,13 +55,13 @@ fun cacheBuilding(building: Building) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadBuilding(oid: Int): Building? {
|
fun loadBuilding(oid: Int): Building? {
|
||||||
return db.buildingDao().get(oid)?.let {
|
return cacheDb.buildingDao().get(oid)?.let {
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
it.expiredAt,
|
it.expiredAt,
|
||||||
DateTimeFormatter.ISO_DATE_TIME
|
DateTimeFormatter.ISO_DATE_TIME
|
||||||
) > LocalDateTime.now()
|
) > LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
db.buildingDao().delete(it)
|
cacheDb.buildingDao().delete(it)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
Building(
|
Building(
|
||||||
@ -94,7 +95,7 @@ interface AuditoriumDao {
|
|||||||
|
|
||||||
fun cacheAuditorium(auditorium: Auditorium) {
|
fun cacheAuditorium(auditorium: Auditorium) {
|
||||||
cacheBuilding(auditorium.building)
|
cacheBuilding(auditorium.building)
|
||||||
db.auditoriumDao().insert(
|
cacheDb.auditoriumDao().insert(
|
||||||
AuditoriumDB(
|
AuditoriumDB(
|
||||||
auditorium.oid,
|
auditorium.oid,
|
||||||
auditorium.name,
|
auditorium.name,
|
||||||
@ -106,13 +107,13 @@ fun cacheAuditorium(auditorium: Auditorium) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadAuditorium(oid: Int): Auditorium? {
|
fun loadAuditorium(oid: Int): Auditorium? {
|
||||||
return db.auditoriumDao().get(oid)?.let {
|
return cacheDb.auditoriumDao().get(oid)?.let {
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
it.expiredAt,
|
it.expiredAt,
|
||||||
DateTimeFormatter.ISO_DATE_TIME
|
DateTimeFormatter.ISO_DATE_TIME
|
||||||
) > LocalDateTime.now()
|
) > LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
db.auditoriumDao().delete(it)
|
cacheDb.auditoriumDao().delete(it)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val building = loadBuilding(it.buildingOid) ?: return null
|
val building = loadBuilding(it.buildingOid) ?: return null
|
||||||
@ -146,7 +147,7 @@ interface DisciplineDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cacheDiscipline(discipline: Discipline) {
|
fun cacheDiscipline(discipline: Discipline) {
|
||||||
db.disciplineDao().insert(
|
cacheDb.disciplineDao().insert(
|
||||||
DisciplineDB(
|
DisciplineDB(
|
||||||
discipline.oid,
|
discipline.oid,
|
||||||
discipline.name,
|
discipline.name,
|
||||||
@ -157,13 +158,13 @@ fun cacheDiscipline(discipline: Discipline) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadDiscipline(oid: Int): Discipline? {
|
fun loadDiscipline(oid: Int): Discipline? {
|
||||||
return db.disciplineDao().get(oid)?.let {
|
return cacheDb.disciplineDao().get(oid)?.let {
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
it.expiredAt,
|
it.expiredAt,
|
||||||
DateTimeFormatter.ISO_DATE_TIME
|
DateTimeFormatter.ISO_DATE_TIME
|
||||||
) > LocalDateTime.now()
|
) > LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
db.disciplineDao().delete(it)
|
cacheDb.disciplineDao().delete(it)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +198,7 @@ interface KindOfWorkDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cacheKindOfWork(kindOfWork: KindOfWork) {
|
fun cacheKindOfWork(kindOfWork: KindOfWork) {
|
||||||
db.kindOfWorkDao().insert(
|
cacheDb.kindOfWorkDao().insert(
|
||||||
KindOfWorkDB(
|
KindOfWorkDB(
|
||||||
kindOfWork.oid,
|
kindOfWork.oid,
|
||||||
kindOfWork.name,
|
kindOfWork.name,
|
||||||
@ -209,13 +210,13 @@ fun cacheKindOfWork(kindOfWork: KindOfWork) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadKindOfWork(oid: Int): KindOfWork? {
|
fun loadKindOfWork(oid: Int): KindOfWork? {
|
||||||
return db.kindOfWorkDao().get(oid)?.let {
|
return cacheDb.kindOfWorkDao().get(oid)?.let {
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
it.expiredAt,
|
it.expiredAt,
|
||||||
DateTimeFormatter.ISO_DATE_TIME
|
DateTimeFormatter.ISO_DATE_TIME
|
||||||
) > LocalDateTime.now()
|
) > LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
db.kindOfWorkDao().delete(it)
|
cacheDb.kindOfWorkDao().delete(it)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +252,7 @@ interface LecturerDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cacheLecturer(lecturer: Lecturer) {
|
fun cacheLecturer(lecturer: Lecturer) {
|
||||||
db.lecturerDao().insert(
|
cacheDb.lecturerDao().insert(
|
||||||
LecturerDB(
|
LecturerDB(
|
||||||
lecturer.unnId,
|
lecturer.unnId,
|
||||||
lecturer.name,
|
lecturer.name,
|
||||||
@ -264,13 +265,13 @@ fun cacheLecturer(lecturer: Lecturer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadLecturer(unnId: Int): Lecturer? {
|
fun loadLecturer(unnId: Int): Lecturer? {
|
||||||
return db.lecturerDao().get(unnId)?.let {
|
return cacheDb.lecturerDao().get(unnId)?.let {
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
it.expiredAt,
|
it.expiredAt,
|
||||||
DateTimeFormatter.ISO_DATE_TIME
|
DateTimeFormatter.ISO_DATE_TIME
|
||||||
) > LocalDateTime.now()
|
) > LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
db.lecturerDao().delete(it)
|
cacheDb.lecturerDao().delete(it)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,7 +321,7 @@ fun cacheSchedule(item: ScheduleUnit) {
|
|||||||
cacheKindOfWork(item.kindOfWork)
|
cacheKindOfWork(item.kindOfWork)
|
||||||
cacheLecturer(item.lecturers[0])
|
cacheLecturer(item.lecturers[0])
|
||||||
|
|
||||||
db.scheduleDao().insert(
|
cacheDb.scheduleDao().insert(
|
||||||
ScheduleUnitDB(
|
ScheduleUnitDB(
|
||||||
item.oid,
|
item.oid,
|
||||||
item.date.format(DateTimeFormatter.ISO_DATE),
|
item.date.format(DateTimeFormatter.ISO_DATE),
|
||||||
@ -347,7 +348,7 @@ fun cacheSchedule(items: ArrayList<ScheduleUnit>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadSchedule(oid: Int): ScheduleUnit? {
|
fun loadSchedule(oid: Int): ScheduleUnit? {
|
||||||
db.scheduleDao().getSchedule(oid)?.let {
|
cacheDb.scheduleDao().getSchedule(oid)?.let {
|
||||||
Log.d("load", it.oid.toString())
|
Log.d("load", it.oid.toString())
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
it.expiredAt,
|
it.expiredAt,
|
||||||
@ -355,7 +356,7 @@ fun loadSchedule(oid: Int): ScheduleUnit? {
|
|||||||
) < LocalDateTime.now()
|
) < LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
Log.d("delete", it.oid.toString())
|
Log.d("delete", it.oid.toString())
|
||||||
db.scheduleDao().delete(it)
|
cacheDb.scheduleDao().delete(it)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,10 +376,10 @@ fun loadSchedule(oid: Int): ScheduleUnit? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
|
fun loadSchedule(date: LocalDate): ArrayList<ScheduleUnit> {
|
||||||
db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||||
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
|
.map { Log.d("meow", "${it.oid}: ${loadSchedule(it.oid)}") }
|
||||||
return ArrayList(
|
return ArrayList(
|
||||||
db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
cacheDb.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
|
||||||
.mapNotNull { loadSchedule(it.oid) }
|
.mapNotNull { loadSchedule(it.oid) }
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn.db
|
package ru.sweetbread.unn.db
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -8,10 +10,9 @@ import androidx.room.Entity
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import ru.sweetbread.unn.AvatarSet
|
import ru.sweetbread.unn.api.AvatarSet
|
||||||
import ru.sweetbread.unn.Type
|
import ru.sweetbread.unn.api.Type
|
||||||
import ru.sweetbread.unn.User
|
import ru.sweetbread.unn.api.User
|
||||||
import ru.sweetbread.unn.ui.layout.db
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@ -20,7 +21,6 @@ import java.time.format.DateTimeFormatter
|
|||||||
data class UserDB(
|
data class UserDB(
|
||||||
@PrimaryKey val userId: Int,
|
@PrimaryKey val userId: Int,
|
||||||
@ColumnInfo val unnId: Int?,
|
@ColumnInfo val unnId: Int?,
|
||||||
@ColumnInfo val bitrixId: Int,
|
|
||||||
@ColumnInfo val type: Type,
|
@ColumnInfo val type: Type,
|
||||||
@ColumnInfo val email: String,
|
@ColumnInfo val email: String,
|
||||||
@ColumnInfo val nameRu: String,
|
@ColumnInfo val nameRu: String,
|
||||||
@ -35,8 +35,8 @@ data class UserDB(
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface UserDao {
|
interface UserDao {
|
||||||
@Query("SELECT * FROM userDB WHERE bitrixId = :bitrixId LIMIT 1")
|
@Query("SELECT * FROM userDB WHERE userId = :userId LIMIT 1")
|
||||||
fun getUserByBitrix(bitrixId: Int): UserDB?
|
fun getUserById(userId: Int): UserDB?
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
fun insert(user: UserDB)
|
fun insert(user: UserDB)
|
||||||
@ -48,11 +48,10 @@ interface UserDao {
|
|||||||
|
|
||||||
fun cacheUser(user: User) {
|
fun cacheUser(user: User) {
|
||||||
try {
|
try {
|
||||||
db.userDao().insert(
|
cacheDb.userDao().insert(
|
||||||
UserDB(
|
UserDB(
|
||||||
user.userId,
|
user.userId,
|
||||||
user.unnId,
|
user.unnId,
|
||||||
user.bitrixId,
|
|
||||||
user.type,
|
user.type,
|
||||||
user.email,
|
user.email,
|
||||||
user.nameRu,
|
user.nameRu,
|
||||||
@ -70,7 +69,7 @@ fun cacheUser(user: User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadUserByBitrixId(bitrixId: Int): User? {
|
fun loadUserByBitrixId(bitrixId: Int): User? {
|
||||||
val user = db.userDao().getUserByBitrix(bitrixId)
|
val user = cacheDb.userDao().getUserById(bitrixId)
|
||||||
Log.d("UserDB", user?.nameEn ?: "None")
|
Log.d("UserDB", user?.nameEn ?: "None")
|
||||||
if (user == null) return null
|
if (user == null) return null
|
||||||
if (LocalDateTime.parse(
|
if (LocalDateTime.parse(
|
||||||
@ -78,12 +77,11 @@ fun loadUserByBitrixId(bitrixId: Int): User? {
|
|||||||
DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
DateTimeFormatter.ISO_LOCAL_DATE_TIME
|
||||||
) < LocalDateTime.now()
|
) < LocalDateTime.now()
|
||||||
) {
|
) {
|
||||||
db.userDao().delete(user)
|
cacheDb.userDao().delete(user)
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
return User(
|
return User(
|
||||||
user.unnId,
|
user.unnId,
|
||||||
user.bitrixId,
|
|
||||||
user.userId,
|
user.userId,
|
||||||
user.type,
|
user.type,
|
||||||
user.email,
|
user.email,
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
/*
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
* Created by sweetbread
|
|
||||||
* Copyright (c) 2025. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.composes
|
package ru.sweetbread.unn.ui.composes
|
||||||
|
|
||||||
@ -55,14 +52,14 @@ import coil.request.ImageRequest
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import ru.sweetbread.unn.AvatarSet
|
|
||||||
import ru.sweetbread.unn.Post
|
|
||||||
import ru.sweetbread.unn.R
|
import ru.sweetbread.unn.R
|
||||||
import ru.sweetbread.unn.Type
|
import ru.sweetbread.unn.api.AvatarSet
|
||||||
import ru.sweetbread.unn.User
|
import ru.sweetbread.unn.api.Post
|
||||||
import ru.sweetbread.unn.getBlogposts
|
import ru.sweetbread.unn.api.Type
|
||||||
import ru.sweetbread.unn.getUserByBitrixId
|
import ru.sweetbread.unn.api.User
|
||||||
import ru.sweetbread.unn.portalURL
|
import ru.sweetbread.unn.api.getBlogposts
|
||||||
|
import ru.sweetbread.unn.api.getUserByBitrixId
|
||||||
|
import ru.sweetbread.unn.api.portalURL
|
||||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -73,8 +70,7 @@ import java.time.format.FormatStyle
|
|||||||
val defUser = User(
|
val defUser = User(
|
||||||
null,
|
null,
|
||||||
123,
|
123,
|
||||||
123,
|
Type.student,
|
||||||
Type.Student,
|
|
||||||
"cool.email@domain.com",
|
"cool.email@domain.com",
|
||||||
"Джон Сигма Омегович",
|
"Джон Сигма Омегович",
|
||||||
"Jon Sigma Omega",
|
"Jon Sigma Omega",
|
||||||
@ -241,7 +237,9 @@ fun UserItemPreview() {
|
|||||||
Modifier
|
Modifier
|
||||||
.width(300.dp)
|
.width(300.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer), defUser, Type.Student.s
|
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
|
defUser,
|
||||||
|
Type.student.s
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
/*
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
* Created by sweetbread
|
|
||||||
* Copyright (c) 2025. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.composes
|
package ru.sweetbread.unn.ui.composes
|
||||||
|
|
||||||
@ -18,10 +15,10 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@ -42,6 +39,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
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.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@ -49,22 +47,23 @@ 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.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import com.kizitonwose.calendar.compose.WeekCalendar
|
|
||||||
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
|
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
|
||||||
|
import com.kizitonwose.calendar.core.WeekDay
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.sweetbread.unn.Auditorium
|
|
||||||
import ru.sweetbread.unn.Building
|
|
||||||
import ru.sweetbread.unn.Discipline
|
|
||||||
import ru.sweetbread.unn.KindOfWork
|
|
||||||
import ru.sweetbread.unn.Lecturer
|
|
||||||
import ru.sweetbread.unn.LecturerRank
|
|
||||||
import ru.sweetbread.unn.R
|
import ru.sweetbread.unn.R
|
||||||
import ru.sweetbread.unn.ScheduleUnit
|
import ru.sweetbread.unn.api.Auditorium
|
||||||
import ru.sweetbread.unn.getScheduleDay
|
import ru.sweetbread.unn.api.Building
|
||||||
|
import ru.sweetbread.unn.api.Discipline
|
||||||
|
import ru.sweetbread.unn.api.KindOfWork
|
||||||
|
import ru.sweetbread.unn.api.Lecturer
|
||||||
|
import ru.sweetbread.unn.api.LecturerRank
|
||||||
|
import ru.sweetbread.unn.api.ScheduleUnit
|
||||||
|
import ru.sweetbread.unn.api.getScheduleDay
|
||||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||||
import splitties.resources.appStr
|
import splitties.resources.appStr
|
||||||
|
import splitties.resources.appStrArray
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -75,36 +74,76 @@ import java.util.Calendar
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Schedule() {
|
fun Schedule() {
|
||||||
val state = rememberWeekCalendarState(
|
var selectedDate by remember { mutableStateOf(LocalDate.now()) }
|
||||||
firstDayOfWeek = DayOfWeek.MONDAY // TODO: set start and end weeks to September and July of current year
|
val calendarState = rememberWeekCalendarState(
|
||||||
|
startDate = LocalDate.now(),
|
||||||
|
firstVisibleWeekDate = LocalDate.now(),
|
||||||
|
firstDayOfWeek = DayOfWeek.MONDAY
|
||||||
)
|
)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
var curDate by remember { mutableStateOf(LocalDate.now()) }
|
Row(
|
||||||
WeekCalendar(
|
|
||||||
state = state,
|
|
||||||
dayContent = {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 16.dp)
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f) // This is important for square sizing!
|
.padding(horizontal = 8.dp),
|
||||||
.offset(2.dp)
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
.background(if (it.date == curDate) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.surfaceContainer)
|
) {
|
||||||
|
calendarState.firstVisibleWeek.days
|
||||||
|
.filter { it.date.dayOfWeek != DayOfWeek.SUNDAY }
|
||||||
|
.forEach { day ->
|
||||||
|
DayItem (
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.padding(2.dp),
|
||||||
|
day = day,
|
||||||
|
isSelected = day.date == selectedDate,
|
||||||
|
onClick = { selectedDate = day.date }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleDay(date = selectedDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DayItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
day: WeekDay,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val isToday = day.date == LocalDate.now()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.background(
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.inversePrimary
|
||||||
|
else MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
shape = if (isToday) CircleShape else RectangleShape
|
||||||
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
onClick = { curDate = it.date },
|
onClick = onClick,
|
||||||
enabled = curDate != it.date
|
enabled = !isSelected
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column (
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = it.date.dayOfMonth.toString(),
|
text = day.date.dayOfMonth.toString(),
|
||||||
fontWeight = if (it.date == LocalDate.now()) FontWeight.Bold else null
|
fontWeight = if (isToday) FontWeight.Bold else null,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = appStrArray(R.array.short_weekdays)[day.date.dayOfWeek.value-1],
|
||||||
|
fontWeight = if (isToday) FontWeight.Bold else null,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
ScheduleDay(date = curDate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -175,9 +214,9 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
val backgroundColor by animateColorAsState(
|
val backgroundColor by animateColorAsState(
|
||||||
targetValue = if (rel == 1f)
|
targetValue = if (ratio == 1f)
|
||||||
MaterialTheme.colorScheme.surfaceContainer
|
MaterialTheme.colorScheme.surfaceContainer
|
||||||
else if (rel == -1f)
|
else if (ratio == -1f)
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.primaryContainer,
|
MaterialTheme.colorScheme.primaryContainer,
|
||||||
@ -283,13 +322,13 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
|
|||||||
verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(begin.toString(), fontWeight = FontWeight.Bold)
|
Text(begin.toString(), fontWeight = FontWeight.Bold)
|
||||||
AnimatedVisibility (
|
AnimatedVisibility (
|
||||||
(0f <= rel) and (rel < 1f),
|
(0f <= ratio) and (ratio < 1f),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 2.dp)
|
.padding(horizontal = 2.dp)
|
||||||
) {
|
) {
|
||||||
DividerWithMarker(
|
DividerWithMarker(
|
||||||
positionPercentage = rel,
|
positionPercentage = ratio,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = MaterialTheme.colorScheme.outline,
|
||||||
thickness = 3.dp,
|
thickness = 3.dp,
|
||||||
markerSize = 8.dp,
|
markerSize = 8.dp,
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
/*
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
* Created by sweetbread
|
|
||||||
* Copyright (c) 2025. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.layout
|
package ru.sweetbread.unn.ui.layout
|
||||||
|
|
||||||
@ -44,7 +41,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import ru.sweetbread.unn.R
|
import ru.sweetbread.unn.R
|
||||||
import ru.sweetbread.unn.auth
|
import ru.sweetbread.unn.api.auth
|
||||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||||
import splitties.activities.start
|
import splitties.activities.start
|
||||||
import splitties.preferences.Preferences
|
import splitties.preferences.Preferences
|
||||||
|
@ -1,77 +1,74 @@
|
|||||||
/*
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
* Created by sweetbread
|
|
||||||
* Copyright (c) 2025. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.layout
|
package ru.sweetbread.unn.ui.layout
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AccountBox
|
import androidx.compose.material.icons.filled.AccountBox
|
||||||
import androidx.compose.material.icons.filled.DateRange
|
import androidx.compose.material.icons.filled.DateRange
|
||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.DrawerValue
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalNavigationDrawer
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberDrawerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import io.ktor.client.HttpClient
|
import coil.compose.AsyncImage
|
||||||
import io.ktor.client.engine.android.Android
|
import kotlinx.coroutines.launch
|
||||||
import io.ktor.client.plugins.HttpRequestRetry
|
import ru.sweetbread.unn.R
|
||||||
import io.ktor.client.plugins.HttpTimeout
|
import ru.sweetbread.unn.api.ME
|
||||||
import io.ktor.client.plugins.cache.HttpCache
|
import ru.sweetbread.unn.api.portalURL
|
||||||
import io.ktor.client.plugins.logging.LogLevel
|
|
||||||
import io.ktor.client.plugins.logging.Logger
|
|
||||||
import io.ktor.client.plugins.logging.Logging
|
|
||||||
import ru.sweetbread.unn.db.AppDatabase
|
|
||||||
import ru.sweetbread.unn.ui.composes.Blogposts
|
import ru.sweetbread.unn.ui.composes.Blogposts
|
||||||
import ru.sweetbread.unn.ui.composes.Schedule
|
import ru.sweetbread.unn.ui.composes.Schedule
|
||||||
import ru.sweetbread.unn.ui.theme.UNNTheme
|
import ru.sweetbread.unn.ui.theme.UNNTheme
|
||||||
import splitties.arch.room.roomDb
|
import splitties.resources.appStr
|
||||||
import splitties.toast.toast
|
import splitties.toast.toast
|
||||||
|
|
||||||
val client = HttpClient(Android) {
|
|
||||||
install(HttpCache)
|
|
||||||
install(Logging) {
|
|
||||||
logger = object : Logger {
|
|
||||||
override fun log(message: String) {
|
|
||||||
Log.i("Ktor", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
level = LogLevel.ALL
|
|
||||||
}
|
|
||||||
install(HttpTimeout) {
|
|
||||||
connectTimeoutMillis = 5000
|
|
||||||
}
|
|
||||||
install(HttpRequestRetry) {
|
|
||||||
retryOnException(maxRetries = 3, retryOnTimeout = true)
|
|
||||||
exponentialDelay()
|
|
||||||
modifyRequest { request ->
|
|
||||||
request.headers.append("x-retry-count", retryCount.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val db = roomDb<AppDatabase>(name = "database")
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -81,15 +78,60 @@ class MainActivity : ComponentActivity() {
|
|||||||
UNNTheme {
|
UNNTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
var route by remember { mutableStateOf("portal/blogposts") }
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
ModalNavigationDrawer(
|
||||||
|
drawerState = drawerState,
|
||||||
|
drawerContent = {
|
||||||
|
DrawerContent(
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(.75f)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.systemBarsPadding(),
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentRoute = navBackStackEntry?.destination?.route
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
when {
|
||||||
|
currentRoute?.startsWith("portal/") == true ->
|
||||||
|
appStr(R.string.news)
|
||||||
|
|
||||||
|
currentRoute?.startsWith("journal/") == true ->
|
||||||
|
appStr(R.string.schedule)
|
||||||
|
|
||||||
|
else -> appStr(R.string.app_name)
|
||||||
|
},
|
||||||
|
Modifier.padding()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch { drawerState.open() }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Menu, "Меню")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
route = "portal/blogposts"
|
navController.navigate("portal/blogposts") {
|
||||||
navController.navigate(route)
|
launchSingleTop = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -97,13 +139,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
contentDescription = "Home"
|
contentDescription = "Home"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selected = route.startsWith("portal/")
|
selected = currentRoute?.startsWith("portal/") == true
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
route = "journal/schedule"
|
navController.navigate("journal/schedule") {
|
||||||
navController.navigate(route)
|
launchSingleTop = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
@ -111,7 +154,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
contentDescription = "Schedule"
|
contentDescription = "Schedule"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selected = route.startsWith("journal/")
|
selected = currentRoute?.startsWith("journal/") == true
|
||||||
)
|
)
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
@ -126,7 +169,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {innerPadding ->
|
) { innerPadding ->
|
||||||
Box(Modifier.padding(innerPadding)) {
|
Box(Modifier.padding(innerPadding)) {
|
||||||
NavHost(navController, startDestination = "portal/blogposts") {
|
NavHost(navController, startDestination = "portal/blogposts") {
|
||||||
composable("portal/blogposts") {
|
composable("portal/blogposts") {
|
||||||
@ -142,4 +185,83 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) {
|
||||||
|
Column(modifier = modifier.padding(0.dp)) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(16.dp))
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier.size(64.dp).clip(CircleShape),
|
||||||
|
model = portalURL + ME.avatar.thumbnail,
|
||||||
|
contentDescription = ME.nameEn
|
||||||
|
)
|
||||||
|
|
||||||
|
Column (Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
ME.nameRu.split(" ").dropLast(1).joinToString(" "),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(ME.type.s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
{},
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
shape = RectangleShape
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
appStr(R.string.record_book),
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
{},
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
shape = RectangleShape
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
appStr(R.string.documents),
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
{},
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
shape = RectangleShape
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
appStr(R.string.materials),
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
TextButton(
|
||||||
|
{},
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
shape = RectangleShape
|
||||||
|
) {
|
||||||
|
Text(appStr(R.string.about_app))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.theme
|
package ru.sweetbread.unn.ui.theme
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.theme
|
package ru.sweetbread.unn.ui.theme
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
package ru.sweetbread.unn.ui.theme
|
package ru.sweetbread.unn.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="activity_horizontal_margin">48dp</dimen>
|
<dimen name="activity_horizontal_margin">48dp</dimen>
|
||||||
</resources>
|
</resources>
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name_reg">ННГУ</string>
|
<string name="app_name_reg">ННГУ</string>
|
||||||
<string name="app_name_dev">ННГУ Альфа</string>
|
<string name="app_name_dev">ННГУ Альфа</string>
|
||||||
@ -14,4 +16,20 @@
|
|||||||
<string name="building">Здание</string>
|
<string name="building">Здание</string>
|
||||||
<string name="floor">Этаж</string>
|
<string name="floor">Этаж</string>
|
||||||
<string name="noData">Нет данных</string>
|
<string name="noData">Нет данных</string>
|
||||||
|
<string name="news">Новости</string>
|
||||||
|
<string name="schedule">Расписание</string>
|
||||||
|
<string name="record_book">Зачётная книга</string>
|
||||||
|
<string name="about_app">О приложении</string>
|
||||||
|
<string name="documents">Документы</string>
|
||||||
|
<string name="materials">Материалы</string>
|
||||||
|
<string-array name="short_weekdays">
|
||||||
|
<item>Пн</item>
|
||||||
|
<item>Вт</item>
|
||||||
|
<item>Ср</item>
|
||||||
|
<item>Чт</item>
|
||||||
|
<item>Пт</item>
|
||||||
|
<item>Сб</item>
|
||||||
|
</string-array>
|
||||||
|
<string name="student">Студент</string>
|
||||||
|
<string name="employee">Сотрудник</string>
|
||||||
</resources>
|
</resources>
|
@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="activity_horizontal_margin">200dp</dimen>
|
<dimen name="activity_horizontal_margin">200dp</dimen>
|
||||||
</resources>
|
</resources>
|
@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="activity_horizontal_margin">48dp</dimen>
|
<dimen name="activity_horizontal_margin">48dp</dimen>
|
||||||
</resources>
|
</resources>
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
<color name="purple_500">#FF6200EE</color>
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#1565AA</color>
|
<color name="ic_launcher_background">#1565AA</color>
|
||||||
</resources>
|
</resources>
|
@ -1,8 +1,9 @@
|
|||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name_reg">UNN</string>
|
<string name="app_name_reg">UNN</string>
|
||||||
<string name="app_name_dev">UNN Dev</string>
|
<string name="app_name_dev">UNN Dev</string>
|
||||||
<string name="app_name_beta">UNN Beta</string>
|
<string name="app_name_beta">UNN Beta</string>
|
||||||
<!-- <string name="title_activity_login">LoginActivity</string>-->
|
|
||||||
<string name="prompt_email" translatable="false">Email</string>
|
<string name="prompt_email" translatable="false">Email</string>
|
||||||
<string name="prompt_login">Login</string>
|
<string name="prompt_login">Login</string>
|
||||||
<string name="prompt_password">Password</string>
|
<string name="prompt_password">Password</string>
|
||||||
@ -16,5 +17,20 @@
|
|||||||
<string name="floor">Floor</string>
|
<string name="floor">Floor</string>
|
||||||
<string name="lorem" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel iaculis elit. Aliquam varius urna ut nisl rhoncus ullamcorper. Maecenas et nisl at dui mollis maximus nec in libero. Ut eu nulla id felis hendrerit lobortis. Maecenas vel facilisis lectus. Morbi eleifend massa a ante consequat, eu aliquam elit euismod. Aenean quis erat tincidunt, egestas ligula id, convallis tortor. Vivamus volutpat condimentum nisl sed eleifend. Aenean dapibus dolor ut orci lobortis, placerat lobortis tortor pretium. Nam eros lectus, convallis sed ultricies sit amet, lacinia sed sem. In mi odio, porta non malesuada et, cursus a metus. Morbi quis odio sed quam commodo gravida id sit amet dolor. Donec ac iaculis massa. Nulla mauris sapien, auctor consequat est in, tempus accumsan ipsum. Donec semper volutpat nisi. Quisque dignissim tellus ipsum, sed malesuada libero aliquam sed. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam eleifend pharetra orci eu scelerisque. In hac habitasse platea dictumst. Sed non neque vitae metus porttitor vestibulum ut eget felis. Aliquam venenatis a magna eu mattis. Proin rutrum, sapien id viverra finibus, nisi quam aliquam eros, et dignissim lectus sem sit amet purus. Donec et semper enim, sed pretium lacus. Nullam venenatis ullamcorper maximus. Mauris pellentesque velit non sem sollicitudin molestie. Duis hendrerit consequat enim eget euismod.</string>
|
<string name="lorem" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel iaculis elit. Aliquam varius urna ut nisl rhoncus ullamcorper. Maecenas et nisl at dui mollis maximus nec in libero. Ut eu nulla id felis hendrerit lobortis. Maecenas vel facilisis lectus. Morbi eleifend massa a ante consequat, eu aliquam elit euismod. Aenean quis erat tincidunt, egestas ligula id, convallis tortor. Vivamus volutpat condimentum nisl sed eleifend. Aenean dapibus dolor ut orci lobortis, placerat lobortis tortor pretium. Nam eros lectus, convallis sed ultricies sit amet, lacinia sed sem. In mi odio, porta non malesuada et, cursus a metus. Morbi quis odio sed quam commodo gravida id sit amet dolor. Donec ac iaculis massa. Nulla mauris sapien, auctor consequat est in, tempus accumsan ipsum. Donec semper volutpat nisi. Quisque dignissim tellus ipsum, sed malesuada libero aliquam sed. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam eleifend pharetra orci eu scelerisque. In hac habitasse platea dictumst. Sed non neque vitae metus porttitor vestibulum ut eget felis. Aliquam venenatis a magna eu mattis. Proin rutrum, sapien id viverra finibus, nisi quam aliquam eros, et dignissim lectus sem sit amet purus. Donec et semper enim, sed pretium lacus. Nullam venenatis ullamcorper maximus. Mauris pellentesque velit non sem sollicitudin molestie. Duis hendrerit consequat enim eget euismod.</string>
|
||||||
<string name="noData">No Data</string>
|
<string name="noData">No Data</string>
|
||||||
<!-- <string name="login_failed">"Login failed"</string>-->
|
<string name="news">News</string>
|
||||||
|
<string name="schedule">Schedule</string>
|
||||||
|
<string name="student">Student</string>
|
||||||
|
<string name="employee">Employee</string>
|
||||||
|
<string name="record_book">Record book</string>
|
||||||
|
<string name="about_app">About app</string>
|
||||||
|
<string name="documents">Documents</string>
|
||||||
|
<string name="materials">Materials</string>
|
||||||
|
<string-array name="short_weekdays">
|
||||||
|
<item>Mn</item>
|
||||||
|
<item>Tu</item>
|
||||||
|
<item>We</item>
|
||||||
|
<item>Th</item>
|
||||||
|
<item>Fr</item>
|
||||||
|
<item>Sa</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.UNN" parent="android:Theme.Material.NoActionBar" />
|
<style name="Theme.UNN" parent="android:Theme.Material.NoActionBar" />
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Sample data extraction rules file; uncomment and customize as necessary.
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
/*
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
* Created by sweetbread
|
|
||||||
* Copyright (c) 2025. All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
# Project-wide Gradle settings.
|
# Project-wide Gradle settings.
|
||||||
# IDE (e.g. Android Studio) users:
|
# IDE (e.g. Android Studio) users:
|
||||||
# Gradle settings configured through the IDE *will override*
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,3 +1,5 @@
|
|||||||
|
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
#Sat Mar 16 18:30:45 MSK 2024
|
#Sat Mar 16 18:30:45 MSK 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
16
gradlew
vendored
16
gradlew
vendored
@ -1,20 +1,6 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
#
|
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
# Copyright 2015 the original author or authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
##
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
ACRA_URL=https://bugs.coders-squad.com/report
|
ACRA_URL=https://bugs.coders-squad.com/report
|
||||||
ACRA_LOGIN=pMJkqPlNLX4kQ3kK
|
ACRA_LOGIN=pMJkqPlNLX4kQ3kK
|
||||||
ACRA_PASS=HYY72oV4ybmpCggC
|
ACRA_PASS=HYY72oV4ybmpCggC
|
@ -1,3 +1,5 @@
|
|||||||
|
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
|
||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
repositories {
|
repositories {
|
||||||
google {
|
google {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user