Compare commits

..
4 Commits
Author SHA1 Message Date
Sweetbread 9e94a67a85 fix: network connectivity related issues 2026-09-17 14:11:34 +03:00
Sweetbread b7183805c2 fix: download recent news 2026-09-17 13:47:10 +03:00
Sweetbread ad64fdf4c5 feat: update GroupsNameText.kt to the new API 2026-09-17 13:07:02 +03:00
Sweetbread d66ffaae7b ref: models.kt 2026-09-17 13:01:54 +03:00
23 changed files with 270 additions and 139 deletions
+2 -1
View File
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). --> <!-- Copyright (c) 2026 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">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".app.UNNApp" android:name=".app.UNNApp"
@@ -38,7 +38,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.core.AuthRepository import ru.sweetbread.unn.core.AuthRepository
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
@@ -51,12 +50,10 @@ class LoginActivity : ComponentActivity() {
val authRepo = (application as UNNApp).container.authRepo val authRepo = (application as UNNApp).container.authRepo
if (authRepo.hasSavedCredentials()) {
runBlocking {
if (authRepo.restoreSession()) {
start<MainActivity>() start<MainActivity>()
finish() finish()
} return
} }
setContent { setContent {
@@ -1,5 +1,10 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.app package ru.sweetbread.unn.app
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -50,23 +55,33 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController 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.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.core.network.LoginResult
import ru.sweetbread.unn.core.network.portalURL import ru.sweetbread.unn.core.network.portalURL
import ru.sweetbread.unn.feature.About
import ru.sweetbread.unn.feature.news.Blogposts import ru.sweetbread.unn.feature.news.Blogposts
import ru.sweetbread.unn.feature.schedule.Schedule import ru.sweetbread.unn.feature.schedule.Schedule
import ru.sweetbread.unn.feature.About
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr import splitties.resources.appStr
import splitties.toast.toast import splitties.toast.toast
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var connectivityManager: ConnectivityManager
private var restoreSessionJob: Job? = null
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
restoreSavedSession()
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -74,6 +89,15 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val authRepo = (application as UNNApp).container.authRepo
lifecycleScope.launch {
authRepo.authenticationFailures.collect {
openLogin()
}
}
connectivityManager = getSystemService(ConnectivityManager::class.java)
restoreSavedSession()
setContent { setContent {
UNNTheme { UNNTheme {
Surface( Surface(
@@ -193,6 +217,44 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onStart() {
super.onStart()
connectivityManager.registerDefaultNetworkCallback(networkCallback)
}
override fun onStop() {
connectivityManager.unregisterNetworkCallback(networkCallback)
super.onStop()
}
private fun restoreSavedSession() {
val container = (application as UNNApp).container
if (
container.sessionManager.sessionId != null ||
restoreSessionJob?.isActive == true
) {
return
}
restoreSessionJob = lifecycleScope.launch {
if (container.authRepo.restoreSession() == LoginResult.InvalidCredentials) {
openLogin()
}
}
}
private fun openLogin() {
if (isFinishing) return
startActivity(
Intent(this, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
)
finish()
}
@Composable @Composable
fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) { fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) {
val me = (application as UNNApp).container.sessionManager.user.collectAsState().value val me = (application as UNNApp).container.sessionManager.user.collectAsState().value
@@ -8,7 +8,9 @@ import ru.sweetbread.unn.core.AuthRepository
import ru.sweetbread.unn.core.CredentialsStore import ru.sweetbread.unn.core.CredentialsStore
import ru.sweetbread.unn.core.SessionManager import ru.sweetbread.unn.core.SessionManager
import ru.sweetbread.unn.core.db.AppDatabase import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.core.network.LoginResult
import ru.sweetbread.unn.core.network.PortalApi import ru.sweetbread.unn.core.network.PortalApi
import ru.sweetbread.unn.core.network.SessionRefreshHandler
import ru.sweetbread.unn.core.network.createHttpClient import ru.sweetbread.unn.core.network.createHttpClient
import ru.sweetbread.unn.feature.news.NewsRepository import ru.sweetbread.unn.feature.news.NewsRepository
import ru.sweetbread.unn.feature.profile.UserRepository import ru.sweetbread.unn.feature.profile.UserRepository
@@ -33,8 +35,9 @@ class UNNApp : Application() {
class AppContainer(context: Context) { class AppContainer(context: Context) {
val credentialsStore = CredentialsStore(context) val credentialsStore = CredentialsStore(context)
val sessionManager = SessionManager() val sessionManager = SessionManager()
val sessionRefreshHandler = SessionRefreshHandler()
val httpClient = createHttpClient() val httpClient = createHttpClient(sessionRefreshHandler)
val portalApi = PortalApi( val portalApi = PortalApi(
client = httpClient, client = httpClient,
@@ -43,6 +46,12 @@ class AppContainer(context: Context) {
val authRepo = AuthRepository(credentialsStore, portalApi) val authRepo = AuthRepository(credentialsStore, portalApi)
init {
sessionRefreshHandler.setRefreshAction {
authRepo.restoreSession() == LoginResult.Success
}
}
val database = roomDb<AppDatabase>(name = "cache") { val database = roomDb<AppDatabase>(name = "cache") {
fallbackToDestructiveMigration(dropAllTables = true) fallbackToDestructiveMigration(dropAllTables = true)
} }
@@ -2,8 +2,10 @@
package ru.sweetbread.unn.core package ru.sweetbread.unn.core
import android.util.Log import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.io.IOException import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.sweetbread.unn.core.network.LoginResult import ru.sweetbread.unn.core.network.LoginResult
import ru.sweetbread.unn.core.network.PortalApi import ru.sweetbread.unn.core.network.PortalApi
@@ -11,6 +13,10 @@ class AuthRepository(
private val credentialsStore: CredentialsStore, private val credentialsStore: CredentialsStore,
private val api: PortalApi private val api: PortalApi
) { ) {
private val restoreMutex = Mutex()
private val _authenticationFailures = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authenticationFailures = _authenticationFailures.asSharedFlow()
suspend fun login( suspend fun login(
login: String, login: String,
password: String password: String
@@ -26,21 +32,22 @@ class AuthRepository(
return res return res
} }
suspend fun restoreSession(): Boolean { fun hasSavedCredentials(): Boolean = credentialsStore.get() != null
val credentials =
credentialsStore.get()
?: return false
return try { suspend fun restoreSession(): LoginResult? = restoreMutex.withLock {
val res = api.login( val credentials = credentialsStore.get() ?: return@withLock null
val result = api.login(
credentials.login, credentials.login,
credentials.password credentials.password
) )
res != LoginResult.InvalidCredentials if (result == LoginResult.InvalidCredentials) {
} catch (e: IOException) { credentialsStore.clear()
Log.e("AuthRepo", "restoreSession error", e) api.session.clear()
false _authenticationFailures.emit(Unit)
} }
result
} }
} }
@@ -7,6 +7,7 @@ import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import androidx.core.content.edit import androidx.core.content.edit
import java.security.GeneralSecurityException
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.KeyGenerator import javax.crypto.KeyGenerator
@@ -97,6 +98,7 @@ class CredentialsStore(
} }
fun get(): Credentials? { fun get(): Credentials? {
return try {
val encryptedString = val encryptedString =
prefs.getString("data", null) prefs.getString("data", null)
?: return null ?: return null
@@ -130,12 +132,18 @@ class CredentialsStore(
val parts = plainText.split('\u0000', limit = 2) val parts = plainText.split('\u0000', limit = 2)
if (parts.size != 2) if (parts.size != 2)
return null null
else Credentials(
return Credentials(
login = parts[0], login = parts[0],
password = parts[1] password = parts[1]
) )
} catch (_: GeneralSecurityException) {
clear()
null
} catch (_: IllegalArgumentException) {
clear()
null
}
} }
fun clear() { fun clear() {
@@ -22,6 +22,9 @@ interface NewsDao {
@Query("SELECT * FROM newsDB ORDER BY `publishedAt` DESC") @Query("SELECT * FROM newsDB ORDER BY `publishedAt` DESC")
fun paging(): PagingSource<Int, NewsDB> fun paging(): PagingSource<Int, NewsDB>
@Query("DELETE FROM newsDB")
suspend fun clear()
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(news: List<NewsDB>) suspend fun insertAll(news: List<NewsDB>)
@@ -6,8 +6,8 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.withTransaction import androidx.room.withTransaction
import ru.sweetbread.unn.api.Auditorium
import ru.sweetbread.unn.core.db.AppDatabase import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.model.Auditorium
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -5,7 +5,7 @@ package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import ru.sweetbread.unn.api.Building import ru.sweetbread.unn.model.Building
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -5,7 +5,7 @@ package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import ru.sweetbread.unn.api.Discipline import ru.sweetbread.unn.model.Discipline
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -5,7 +5,7 @@ package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import ru.sweetbread.unn.api.KindOfWork import ru.sweetbread.unn.model.KindOfWork
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -5,8 +5,8 @@ package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import ru.sweetbread.unn.api.Lecturer import ru.sweetbread.unn.model.Lecturer
import ru.sweetbread.unn.api.LecturerRank import ru.sweetbread.unn.model.LecturerRank
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -6,8 +6,8 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.withTransaction import androidx.room.withTransaction
import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.core.db.AppDatabase import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.model.ScheduleUnit
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@@ -6,7 +6,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import io.ktor.util.reflect.instanceOf import io.ktor.util.reflect.instanceOf
import ru.sweetbread.unn.api.AvatarSet import ru.sweetbread.unn.model.AvatarSet
import ru.sweetbread.unn.model.Student import ru.sweetbread.unn.model.Student
import ru.sweetbread.unn.model.Type import ru.sweetbread.unn.model.Type
import ru.sweetbread.unn.model.User import ru.sweetbread.unn.model.User
@@ -6,14 +6,37 @@ import android.util.Log
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.cookies.HttpCookies import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.plugin
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.HttpStatusCode
import io.ktor.util.AttributeKey
fun createHttpClient() = HttpClient(Android) { private val SkipSessionRefresh = AttributeKey<Unit>("SkipSessionRefresh")
private val SessionRefreshAttempted = AttributeKey<Unit>("SessionRefreshAttempted")
fun HttpRequestBuilder.skipSessionRefresh() {
attributes.put(SkipSessionRefresh, Unit)
}
class SessionRefreshHandler {
@Volatile
private var refresh: suspend () -> Boolean = { false }
fun setRefreshAction(action: suspend () -> Boolean) {
refresh = action
}
suspend fun refreshSession(): Boolean = refresh()
}
fun createHttpClient(sessionRefreshHandler: SessionRefreshHandler) = HttpClient(Android) {
install(HttpCache) install(HttpCache)
install(HttpCookies) install(HttpCookies)
install(Logging) { install(Logging) {
@@ -34,4 +57,24 @@ fun createHttpClient() = HttpClient(Android) {
request.headers.append("x-retry-count", retryCount.toString()) request.headers.append("x-retry-count", retryCount.toString())
} }
} }
}.apply {
plugin(HttpSend).intercept { request ->
val call = execute(request)
if (
call.response.status == HttpStatusCode.Forbidden &&
request.attributes.getOrNull(SkipSessionRefresh) == null &&
request.attributes.getOrNull(SessionRefreshAttempted) == null
) {
request.attributes.put(SessionRefreshAttempted, Unit)
if (sessionRefreshHandler.refreshSession()) {
execute(request)
} else {
call
}
} else {
call
}
}
} }
@@ -11,15 +11,15 @@ import io.ktor.client.statement.bodyAsText
import io.ktor.http.parameters import io.ktor.http.parameters
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import ru.sweetbread.unn.api.Auditorium
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.core.SessionManager import ru.sweetbread.unn.core.SessionManager
import ru.sweetbread.unn.model.Auditorium
import ru.sweetbread.unn.model.Building
import ru.sweetbread.unn.model.Discipline
import ru.sweetbread.unn.model.KindOfWork
import ru.sweetbread.unn.model.Lecturer
import ru.sweetbread.unn.model.LecturerRank
import ru.sweetbread.unn.model.NewsDto import ru.sweetbread.unn.model.NewsDto
import ru.sweetbread.unn.model.ScheduleUnit
import ru.sweetbread.unn.model.User import ru.sweetbread.unn.model.User
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -50,7 +50,10 @@ class Auth (
append("USER_LOGIN", login) append("USER_LOGIN", login)
append("USER_PASSWORD", password) append("USER_PASSWORD", password)
} }
) { retry { noRetry() } } ) {
retry { noRetry() }
skipSessionRefresh()
}
if (r.status.value == 302) if (r.status.value == 302)
return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1] return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
@@ -182,7 +185,9 @@ class Bitrix(
val client: HttpClient, val client: HttpClient,
) { ) {
suspend fun getCurrentUser(): User { suspend fun getCurrentUser(): User {
val r = client.get("$vuzapiURL/profile/current") val r = client.get("$vuzapiURL/profile/current") {
skipSessionRefresh()
}
return User.fromProfile(JSONObject(r.bodyAsText())) return User.fromProfile(JSONObject(r.bodyAsText()))
} }
@@ -64,9 +64,9 @@ class NewsRemoteMediator(
} }
db.withTransaction { db.withTransaction {
// if (loadType == LoadType.REFRESH) { if (loadType == LoadType.REFRESH) {
// db.newsDao().clear() db.newsDao().clear()
// } }
db.newsDao().insertAll( db.newsDao().insertAll(
news.map { it.toEntity() } news.map { it.toEntity() }
@@ -44,15 +44,15 @@ import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.api.Auditorium
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.feature.schedule.components.BoundedWeekPicker import ru.sweetbread.unn.feature.schedule.components.BoundedWeekPicker
import ru.sweetbread.unn.feature.schedule.components.GroupsNameText import ru.sweetbread.unn.feature.schedule.components.GroupsNameText
import ru.sweetbread.unn.model.Auditorium
import ru.sweetbread.unn.model.Building
import ru.sweetbread.unn.model.Discipline
import ru.sweetbread.unn.model.KindOfWork
import ru.sweetbread.unn.model.Lecturer
import ru.sweetbread.unn.model.LecturerRank
import ru.sweetbread.unn.model.ScheduleUnit
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr import splitties.resources.appStr
import java.time.LocalDate import java.time.LocalDate
@@ -5,12 +5,11 @@ package ru.sweetbread.unn.feature.schedule
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.core.SessionManager
import ru.sweetbread.unn.core.db.AppDatabase import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.core.db.entity.cacheSchedule import ru.sweetbread.unn.core.db.entity.cacheSchedule
import ru.sweetbread.unn.core.db.entity.toModel import ru.sweetbread.unn.core.db.entity.toModel
import ru.sweetbread.unn.core.network.PortalApi import ru.sweetbread.unn.core.network.PortalApi
import ru.sweetbread.unn.model.ScheduleUnit
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
@@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.sweetbread.unn.app.UNNApp import ru.sweetbread.unn.app.UNNApp
import ru.sweetbread.unn.api.ScheduleUnit import ru.sweetbread.unn.model.ScheduleUnit
import java.time.LocalDate import java.time.LocalDate
data class ScheduleUiState( data class ScheduleUiState(
@@ -27,7 +27,16 @@ fun GroupsNameText(
) { ) {
val suffix = if (text.endsWith("-В-OUP")) "-В-OUP" else null val suffix = if (text.endsWith("-В-OUP")) "-В-OUP" else null
val mainPart = if (suffix != null) text.dropLast(6) else text val mainPart = if (suffix != null) text.dropLast(6) else text
val blocks = mainPart.split('|') val blocks = if (mainPart.startsWith("Поток_(") && mainPart.endsWith(')')) {
mainPart
.removePrefix("Поток_(")
.dropLast(1)
.split(';')
.map(String::trim)
.filter(String::isNotEmpty)
} else {
mainPart.split('|')
}
FlowRow( FlowRow(
modifier = modifier, modifier = modifier,
@@ -1,19 +1,12 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.api package ru.sweetbread.unn.model
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
enum class LecturerRank(val id: Int) {
Assistant(R.string.assistant),
Lecturer(R.string.lecturer),
SLecturer(R.string.slecturer),
AProfessor(R.string.aprofessor)
}
class ScheduleUnit( class ScheduleUnit(
val oid: Int, val oid: Int,
val auditorium: Auditorium, val auditorium: Auditorium,
@@ -27,6 +20,13 @@ class ScheduleUnit(
val lastUpdated: LocalDateTime, val lastUpdated: LocalDateTime,
) )
enum class LecturerRank(val id: Int) {
Assistant(R.string.assistant),
Lecturer(R.string.lecturer),
SLecturer(R.string.slecturer),
AProfessor(R.string.aprofessor)
}
class Auditorium( class Auditorium(
val name: String, val name: String,
val oid: Int, val oid: Int,
@@ -60,18 +60,3 @@ class Lecturer(
val unnId: Int, val unnId: Int,
val uid: String val uid: String
) )
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
)
@@ -4,15 +4,12 @@ package ru.sweetbread.unn.model
import org.json.JSONObject import org.json.JSONObject
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.api.AvatarSet
import splitties.resources.appStr import splitties.resources.appStr
enum class Type(val s: String) { enum class Type(val s: String) {
student(appStr(R.string.student)), Student(appStr(R.string.student)),
// group("group"), Lecturer(appStr(R.string.lecturer)),
lecturer(appStr(R.string.lecturer)), Employee(appStr(R.string.employee))
// auditorium("auditorium"),
employee(appStr(R.string.employee))
} }
open class User( open class User(
@@ -32,8 +29,8 @@ open class User(
.getJSONObject(0) .getJSONObject(0)
.getString("type") .getString("type")
) { ) {
"student" -> Type.student "student" -> Type.Student
else -> Type.employee else -> Type.Employee
}, },
json.getString("email"), json.getString("email"),
json.getString("fullname"), json.getString("fullname"),
@@ -53,8 +50,8 @@ open class User(
return User( return User(
user.getInt("id"), user.getInt("id"),
when (json.getString("type")) { when (json.getString("type")) {
"student" -> Type.student "student" -> Type.Student
else -> Type.employee else -> Type.Employee
}, },
user.getString("email"), user.getString("email"),
user.getString("fullname"), user.getString("fullname"),
@@ -71,7 +68,7 @@ open class User(
fun getPlaceholder() = User( fun getPlaceholder() = User(
0, 0,
Type.employee, Type.Employee,
"", "",
"Удалённый пользователь", "Удалённый пользователь",
"Deleted user", "Deleted user",
@@ -108,3 +105,9 @@ class Student(
group group
) )
} }
class AvatarSet(
val original: String,
val thumbnail: String,
val small: String
)