Compare commits

4 Commits
Author SHA1 Message Date
Sweetbread b7647d7267 ref: blogposts 2026-09-11 01:56:10 +03:00
Sweetbread 5b76f55cb7 deps: update 2026-09-09 15:20:02 +03:00
Sweetbread d5194c2e6f feat: offline-first 2026-09-07 19:39:45 +03:00
Sweetbread 008ec3030b ref: rewrite code architecture for Schedule 2026-09-06 20:42:32 +03:00
41 changed files with 806 additions and 721 deletions
+4 -5
View File
@@ -1,14 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). --> <!-- 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">
<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=".UNNApp"
android:allowBackup="true" android:allowBackup="true"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@@ -46,11 +45,11 @@
android:value="0.1" /> android:value="0.1" />
<activity <activity
android:name=".app.MainActivity" android:name=".ui.layout.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.UNN" /> android:theme="@style/Theme.UNN" />
<activity <activity
android:name=".app.LoginActivity" android:name=".ui.layout.LoginActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.UNN"> android:theme="@style/Theme.UNN">
<intent-filter> <intent-filter>
@@ -1,17 +1,15 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.app package ru.sweetbread.unn
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import ru.sweetbread.unn.core.AuthRepository 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.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.db.AppDatabase
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
import ru.sweetbread.unn.feature.schedule.ScheduleRepository import ru.sweetbread.unn.feature.schedule.ScheduleRepository
@@ -35,9 +33,8 @@ 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(sessionRefreshHandler) val httpClient = createHttpClient()
val portalApi = PortalApi( val portalApi = PortalApi(
client = httpClient, client = httpClient,
@@ -46,12 +43,6 @@ 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)
} }
@@ -69,5 +60,6 @@ class AppContainer(context: Context) {
val scheduleRepository = ScheduleRepository( val scheduleRepository = ScheduleRepository(
api = portalApi, api = portalApi,
db = database, db = database,
session = sessionManager
) )
} }
@@ -1,12 +1,19 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.model package ru.sweetbread.unn.api
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,
@@ -20,13 +27,6 @@ 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,3 +60,18 @@ 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
)
@@ -2,10 +2,8 @@
package ru.sweetbread.unn.core package ru.sweetbread.unn.core
import kotlinx.coroutines.flow.MutableSharedFlow import android.util.Log
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.io.IOException
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
@@ -13,10 +11,6 @@ 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
@@ -32,22 +26,21 @@ class AuthRepository(
return res return res
} }
fun hasSavedCredentials(): Boolean = credentialsStore.get() != null suspend fun restoreSession(): Boolean {
val credentials =
credentialsStore.get()
?: return false
suspend fun restoreSession(): LoginResult? = restoreMutex.withLock { return try {
val credentials = credentialsStore.get() ?: return@withLock null val res = api.login(
val result = api.login(
credentials.login, credentials.login,
credentials.password credentials.password
) )
if (result == LoginResult.InvalidCredentials) { res != LoginResult.InvalidCredentials
credentialsStore.clear() } catch (e: IOException) {
api.session.clear() Log.e("AuthRepo", "restoreSession error", e)
_authenticationFailures.emit(Unit) false
} }
result
} }
} }
@@ -7,7 +7,6 @@ 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
@@ -98,7 +97,6 @@ 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
@@ -132,18 +130,12 @@ class CredentialsStore(
val parts = plainText.split('\u0000', limit = 2) val parts = plainText.split('\u0000', limit = 2)
if (parts.size != 2) if (parts.size != 2)
null return 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() {
@@ -1,6 +1,6 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao package ru.sweetbread.unn.core.db
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.room.Dao import androidx.room.Dao
@@ -22,9 +22,6 @@ 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>)
@@ -1,22 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.AuditoriumDB
@Dao
interface AuditoriumDao {
@Query("SELECT * FROM auditoriumdb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): AuditoriumDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(auditoriums: List<AuditoriumDB>)
@Delete
fun delete(auditorium: AuditoriumDB)
}
@@ -1,22 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.BuildingDB
@Dao
interface BuildingDao {
@Query("SELECT * FROM buildingdb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): BuildingDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(buildings: List<BuildingDB>)
@Delete
fun delete(building: BuildingDB)
}
@@ -1,22 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.DisciplineDB
@Dao
interface DisciplineDao {
@Query("SELECT * FROM disciplinedb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): DisciplineDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(disciplines: List<DisciplineDB>)
@Delete
fun delete(discipline: DisciplineDB)
}
@@ -1,22 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.KindOfWorkDB
@Dao
interface KindOfWorkDao {
@Query("SELECT * FROM kindofworkdb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): KindOfWorkDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(kindOfWork: List<KindOfWorkDB>)
@Delete
fun delete(kindOfWork: KindOfWorkDB)
}
@@ -1,22 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.LecturerDB
@Dao
interface LecturerDao {
@Query("SELECT * FROM lecturerdb WHERE unnId = :unnId LIMIT 1")
suspend fun get(unnId: Int): LecturerDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(lecturers: List<LecturerDB>)
@Delete
fun delete(lecturer: LecturerDB)
}
@@ -1,38 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import ru.sweetbread.unn.core.db.entity.ScheduleUnitDB
@Dao
interface ScheduleItemDao {
@Query("SELECT * FROM scheduleUnitDB WHERE oid = :oid LIMIT 1")
suspend fun getSchedule(oid: Int): ScheduleUnitDB?
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
suspend fun getSchedule(date: String): List<ScheduleUnitDB>
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun getScheduleFlow(date: String): Flow<List<ScheduleUnitDB>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(units: List<ScheduleUnitDB>)
@Delete
suspend fun delete(unit: ScheduleUnitDB)
@Query("DELETE FROM scheduleUnitDB WHERE date = :date")
suspend fun deleteByDate(date: String)
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun observe(date: String): Flow<List<ScheduleUnitDB>>
@Query("SELECT * FROM scheduleUnitDB")
suspend fun getAll(): List<ScheduleUnitDB>
}
@@ -1,21 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.UserDB
@Dao
interface UserDao {
@Query("SELECT * FROM userDB WHERE userId = :userId LIMIT 1")
suspend fun get(userId: Int): UserDB?
@Insert
suspend fun insert(user: UserDB)
@Delete
fun delete(user: UserDB)
}
@@ -1,51 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.withTransaction
import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.model.Auditorium
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Entity
data class AuditoriumDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val floor: Int?,
@ColumnInfo val buildingOid: Int,
@ColumnInfo val updatedAt: String
)
suspend fun AuditoriumDB.toModel(db: AppDatabase): Auditorium? {
return Auditorium(
name,
oid,
floor ?: 0,
db.buildingDao().get(buildingOid)?.toModel() ?: return null
)
}
fun Auditorium.toEntity() = AuditoriumDB(
oid,
name,
floor,
building.oid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
suspend fun cacheAuditorium(db: AppDatabase, auditoriums: List<Auditorium>) {
db.withTransaction {
db.buildingDao().insert(
auditoriums.map { it.building }.map { it.toEntity() }
)
db.auditoriumDao().insert(
auditoriums.map { it.toEntity() }
)
}
}
@@ -1,21 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import ru.sweetbread.unn.model.Building
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Entity
data class BuildingDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val gid: Int,
@ColumnInfo val updatedAt: String
)
fun BuildingDB.toModel() = Building(name, gid, oid)
fun Building.toEntity() = BuildingDB(oid, name, gid, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
@@ -1,22 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import ru.sweetbread.unn.model.Discipline
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Entity
data class DisciplineDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val type: Int,
@ColumnInfo val updatedAt: String
)
fun DisciplineDB.toModel() = Discipline(name, oid, type)
fun Discipline.toEntity() = DisciplineDB(oid, name, type, LocalDateTime.now().format(
DateTimeFormatter.ISO_DATE_TIME))
@@ -1,28 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import ru.sweetbread.unn.model.KindOfWork
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Entity
data class KindOfWorkDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val uid: String,
@ColumnInfo val complexity: Int,
@ColumnInfo val updatedAt: String
)
fun KindOfWorkDB.toModel() = KindOfWork(name, oid, uid, complexity)
fun KindOfWork.toEntity() = KindOfWorkDB(
oid,
name,
uid,
complexity,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
@@ -1,31 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import ru.sweetbread.unn.model.Lecturer
import ru.sweetbread.unn.model.LecturerRank
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Entity
data class LecturerDB(
@PrimaryKey val unnId: Int,
@ColumnInfo val name: String,
@ColumnInfo val rank: LecturerRank,
@ColumnInfo val email: String,
@ColumnInfo val uid: String,
@ColumnInfo val updatedAt: String
)
fun LecturerDB.toModel() = Lecturer(name, rank, email, unnId, uid)
fun Lecturer.toEntity() = LecturerDB(
unnId,
name,
rank,
email,
uid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
@@ -18,7 +18,6 @@ data class NewsDB(
@ColumnInfo val authorId: Int @ColumnInfo val authorId: Int
) )
suspend fun NewsDB.toModel(userRepo: UserRepository): News = News( suspend fun NewsDB.toModel(userRepo: UserRepository): News = News(
id, id,
LocalDateTime.parse(publishedAt, DateTimeFormatter.ISO_DATE_TIME), LocalDateTime.parse(publishedAt, DateTimeFormatter.ISO_DATE_TIME),
@@ -1,70 +0,0 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.withTransaction
import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.model.ScheduleUnit
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
@Entity
data class ScheduleUnitDB(
@PrimaryKey val oid: Int,
@ColumnInfo val date: String,
@ColumnInfo val stream: String,
@ColumnInfo val begin: String,
@ColumnInfo val end: String,
@ColumnInfo val auditoriumOid: Int,
@ColumnInfo val disciplineOid: Int,
@ColumnInfo val kindOfWorkOid: Int,
@ColumnInfo val lecturersId: String,
@ColumnInfo val updatedAt: String
)
suspend fun ScheduleUnitDB.toModel(db: AppDatabase): ScheduleUnit? {
return ScheduleUnit(
oid,
db.auditoriumDao().get(auditoriumOid)?.toModel(db) ?: return null,
LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
db.disciplineDao().get(disciplineOid)?.toModel() ?: return null,
db.kindOfWorkDao().get(kindOfWorkOid)?.toModel() ?: return null,
lecturersId.split(",").map { db.lecturerDao().get(it.toInt())?.toModel() ?: return null },
stream,
LocalTime.parse(begin, DateTimeFormatter.ISO_TIME),
LocalTime.parse(end, DateTimeFormatter.ISO_TIME),
LocalDateTime.parse(updatedAt, DateTimeFormatter.ISO_DATE_TIME),
)
}
fun ScheduleUnit.toEntity() = ScheduleUnitDB(
oid,
date.format(DateTimeFormatter.ISO_DATE),
stream,
begin.format(DateTimeFormatter.ISO_TIME),
end.format(DateTimeFormatter.ISO_TIME),
auditorium.oid,
discipline.oid,
kindOfWork.oid,
lecturers.map { it.unnId }.joinToString(","),
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
suspend fun cacheSchedule(db: AppDatabase, units: List<ScheduleUnit>) {
db.withTransaction {
cacheAuditorium(db, units.map { it.auditorium })
db.disciplineDao().insert(units.map { it.discipline.toEntity() })
db.kindOfWorkDao().insert(units.map { it.kindOfWork.toEntity() })
db.lecturerDao().insert(units.flatMap { it.lecturers }.map { it.toEntity() })
db.scheduleDao().insert(units.map { it.toEntity() })
}
}
@@ -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.model.AvatarSet import ru.sweetbread.unn.api.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
@@ -29,7 +29,6 @@ data class UserDB(
@ColumnInfo val updatedAt: String @ColumnInfo val updatedAt: String
) )
fun User.toEntity(): UserDB = UserDB( fun User.toEntity(): UserDB = UserDB(
userId, userId,
if (this.instanceOf(Student::class)) (this as Student).uns else null, if (this.instanceOf(Student::class)) (this as Student).uns else null,
@@ -46,7 +45,7 @@ fun User.toEntity(): UserDB = UserDB(
) )
fun UserDB.toModel(): User { fun UserDB.toModel(): User {
val user = User( var user = User(
userId, userId,
type, type,
email, email,
@@ -11,9 +11,9 @@ class PortalApi (
val client: HttpClient, val client: HttpClient,
val session: SessionManager val session: SessionManager
) { ) {
public val auth = Auth(client) public val auth = Auth(client, session)
public val ruzapi = Ruzapi(client, session) public val ruzapi = Ruzapi(client, session)
public val bitrix = Bitrix(client) public val bitrix = Bitrix(client, session)
/** /**
* Authorize user by [login] and [password] * Authorize user by [login] and [password]
@@ -40,6 +40,60 @@ class PortalApi (
LoginResult.NetworkError LoginResult.NetworkError
} }
} }
// /**
// * Save info about current [User] in memory
// */
// private suspend fun getMyself(login: String): User = coroutineScope {
// val idDeferred = async { getId(login) }
// val userDeferred = async { getUser() }
//
// val id = idDeferred.await()
// val user = userDeferred.await()
//
// return@coroutineScope User(
// bx_id = null,
// ra_id = null,
// type = Type.student,
// email = user.email,
// nameRu = user.nameRu,
// nameEn = user.nameEn,
// isMale = user.isMale,
// birthday = user.birthday,
// avatar = user.avatar
// )
// }
// suspend fun getScheduleDay(
// date: LocalDate,
// type: Type = session.user.value!!.type,
// id: Int = ME.unnId!!
// ): List<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
// }
} }
sealed interface LoginResult { sealed interface LoginResult {
@@ -6,37 +6,14 @@ 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
private val SkipSessionRefresh = AttributeKey<Unit>("SkipSessionRefresh") fun createHttpClient() = HttpClient(Android) {
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) {
@@ -57,24 +34,4 @@ fun createHttpClient(sessionRefreshHandler: SessionRefreshHandler) = HttpClient(
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
@@ -37,6 +37,7 @@ const val restURL = "$portalURL/rest"
class Auth ( class Auth (
val client: HttpClient, val client: HttpClient,
val session: SessionManager
) { ) {
/** /**
* Get token (phpsessid header) from portal * Get token (phpsessid header) from portal
@@ -50,10 +51,7 @@ 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]
@@ -183,11 +181,10 @@ class Ruzapi(
class Bitrix( class Bitrix(
val client: HttpClient, val client: HttpClient,
val session: SessionManager
) { ) {
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()))
} }
@@ -204,3 +201,176 @@ class Bitrix(
return NewsDto.fromJsonArray(JSONArray(r.bodyAsText())) return NewsDto.fromJsonArray(JSONArray(r.bodyAsText()))
} }
} }
//suspend fun getId(login: String): Int {
// return JSONObject(client.get("$ruzapiURL/studentinfo/") {
// header("Cookie", "PHPSESSID=${PHPSESSID}")
// parameter("uns", login.drop(1))
// }.bodyAsText()).getString("id").toInt()
//}
//
//suspend fun getUser(userId: Int? = null): 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")
// }.bodyAsText()
// )
//
// return User(
// unnId = null,
// 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"),
// )
// }
// )
//}
//
//suspend fun getProfile
//
///**
// * Fetch schedule from Uni API
// *
// * @return [ArrayList] of all [ru.sweetbread.unn.api.ScheduleUnit]s between [start] and [finish]
// */
//suspend fun downloadSchedule(
// type: Type = ME.type,
// id: Int = ME.unnId!!,
// start: LocalDate,
// finish: LocalDate
//): ArrayList<ScheduleUnit> {
// val unnDatePattern = DateTimeFormatter.ofPattern("yyyy-MM-dd")
//
// val r = client.get("$ruzapiURL/schedule/${type.name}/$id") {
// parameter("start", start.format(unnDatePattern))
// parameter("finish", finish.format(unnDatePattern))
// parameter("lng", "1")
// }
//
// val json = JSONArray(r.bodyAsText())
// val out = arrayListOf<ScheduleUnit>()
// for (i in 0 until json.length()) {
// val unit = json.getJSONObject(i)
// val lecturesJson = unit.getJSONArray("listOfLecturers")
// val lecturers = arrayListOf<Lecturer>()
//
// for (j in 0 until lecturesJson.length()) {
// val lecturer = lecturesJson.getJSONObject(j)
// lecturers.add(
// Lecturer(
// name = lecturer.getString("lecturer"),
// email = lecturer.getString("lecturerEmail"),
// unnId = lecturer.getInt("lecturerOid"),
// uid = lecturer.getString("lecturerUID"),
// rank = when (lecturer.getString("lecturer_rank")) {
// "АССИСТ" -> LecturerRank.Assistant
// "СТПРЕП" -> LecturerRank.SLecturer
// "ДОЦЕНТ" -> LecturerRank.AProfessor
// else -> LecturerRank.Lecturer
// }
// )
// )
// }
//
// out.add(
// ScheduleUnit(
// oid = unit.getInt("lessonOid"),
// auditorium = Auditorium(
// name = unit.getString("auditorium"),
// oid = unit.getInt("auditoriumOid"),
// floor = unit.getInt("auditoriumfloor"),
// building = Building(
// name = unit.getString("building"),
// gid = unit.getInt("buildingGid"),
// oid = unit.getInt("buildingOid")
// )
// ),
// date = LocalDate.parse(unit.getString("date"), unnDatePattern),
// discipline = Discipline(
// name = unit.getString("discipline"),
// oid = unit.getInt("disciplineOid"),
// type = unit.getInt("disciplinetypeload")
// ),
// kindOfWork = KindOfWork(
// name = unit.getString("kindOfWork"),
// complexity = unit.getInt("kindOfWorkComplexity"),
// oid = unit.getInt("kindOfWorkOid"),
// uid = unit.getString("kindOfWorkUid")
// ),
// lecturers = lecturers,
// stream = unit.getString("stream"),
// begin = LocalTime.parse(
// unit.getString("beginLesson"),
// DateTimeFormatter.ofPattern("HH:mm")
// ),
// end = LocalTime.parse(
// unit.getString("endLesson"),
// DateTimeFormatter.ofPattern("HH:mm")
// )
// )
// )
// }
//
// 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 getBlogposts(): ArrayList<Post> {
// val r = client.get("$prtl2URL/news.php") {
// header("Cookie", "PHPSESSID=${PHPSESSID}")
// header("x-bitrix-sessid-token", CSRF)
// }
// val result = JSONArray(r.bodyAsText())
//
// val out = arrayListOf<Post>()
// for (i in 0 until result.length()) {
// val el = result.getJSONObject(i)
// out.add(
// Post(
// id = el.getString("id").toInt(),
// authorId = el.getJSONObject("author").getInt("id").toInt(),
// enableComments = true, // FIXME: Delete the field or get correct value
// numComments = el.getString("commentsnum").toInt(),
// date = LocalDateTime.parse(
// el.getString("time"),
// DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
// ),
// content = el.getString("fulltext")
// )
// )
// }
// return out
//}
@@ -1,24 +1,11 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.core.db package ru.sweetbread.unn.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import ru.sweetbread.unn.core.db.dao.AuditoriumDao import ru.sweetbread.unn.core.db.NewsDao
import ru.sweetbread.unn.core.db.dao.BuildingDao
import ru.sweetbread.unn.core.db.dao.DisciplineDao
import ru.sweetbread.unn.core.db.dao.KindOfWorkDao
import ru.sweetbread.unn.core.db.dao.LecturerDao
import ru.sweetbread.unn.core.db.dao.NewsDao
import ru.sweetbread.unn.core.db.dao.ScheduleItemDao
import ru.sweetbread.unn.core.db.dao.UserDao
import ru.sweetbread.unn.core.db.entity.AuditoriumDB
import ru.sweetbread.unn.core.db.entity.BuildingDB
import ru.sweetbread.unn.core.db.entity.DisciplineDB
import ru.sweetbread.unn.core.db.entity.KindOfWorkDB
import ru.sweetbread.unn.core.db.entity.LecturerDB
import ru.sweetbread.unn.core.db.entity.NewsDB import ru.sweetbread.unn.core.db.entity.NewsDB
import ru.sweetbread.unn.core.db.entity.ScheduleUnitDB
import ru.sweetbread.unn.core.db.entity.UserDB import ru.sweetbread.unn.core.db.entity.UserDB
@Database(entities = [ @Database(entities = [
@@ -0,0 +1,269 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow
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 java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
@Entity
data class BuildingDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val gid: Int,
@ColumnInfo val updatedAt: String
)
@Dao
interface BuildingDao {
@Query("SELECT * FROM buildingdb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): BuildingDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(buildings: List<BuildingDB>)
@Delete
fun delete(building: BuildingDB)
}
fun BuildingDB.toModel() = Building(name, gid, oid)
fun Building.toEntity() = BuildingDB(oid, name, gid, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
@Entity
data class AuditoriumDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val floor: Int?,
@ColumnInfo val buildingOid: Int,
@ColumnInfo val updatedAt: String
)
@Dao
interface AuditoriumDao {
@Query("SELECT * FROM auditoriumdb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): AuditoriumDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(auditoriums: List<AuditoriumDB>)
@Delete
fun delete(auditorium: AuditoriumDB)
}
suspend fun AuditoriumDB.toModel(db: AppDatabase): Auditorium? {
return Auditorium(
name,
oid,
floor ?: 0,
db.buildingDao().get(buildingOid)?.toModel() ?: return null
)
}
fun Auditorium.toEntity() = AuditoriumDB(
oid,
name,
floor,
building.oid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
suspend fun cacheAuditorium(db: AppDatabase, auditoriums: List<Auditorium>) {
db.withTransaction {
db.buildingDao().insert(
auditoriums.map { it.building }.map { it.toEntity() }
)
db.auditoriumDao().insert(
auditoriums.map { it.toEntity() }
)
}
}
@Entity
data class DisciplineDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val type: Int,
@ColumnInfo val updatedAt: String
)
@Dao
interface DisciplineDao {
@Query("SELECT * FROM disciplinedb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): DisciplineDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(disciplines: List<DisciplineDB>)
@Delete
fun delete(discipline: DisciplineDB)
}
fun DisciplineDB.toModel() = Discipline(name, oid, type)
fun Discipline.toEntity() = DisciplineDB(oid, name, type, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
@Entity
data class KindOfWorkDB(
@PrimaryKey val oid: Int,
@ColumnInfo val name: String,
@ColumnInfo val uid: String,
@ColumnInfo val complexity: Int,
@ColumnInfo val updatedAt: String
)
@Dao
interface KindOfWorkDao {
@Query("SELECT * FROM kindofworkdb WHERE oid = :oid LIMIT 1")
suspend fun get(oid: Int): KindOfWorkDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(kindOfWork: List<KindOfWorkDB>)
@Delete
fun delete(kindOfWork: KindOfWorkDB)
}
fun KindOfWorkDB.toModel() = KindOfWork(name, oid, uid, complexity)
fun KindOfWork.toEntity() = KindOfWorkDB(
oid,
name,
uid,
complexity,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
@Entity
data class LecturerDB(
@PrimaryKey val unnId: Int,
@ColumnInfo val name: String,
@ColumnInfo val rank: LecturerRank,
@ColumnInfo val email: String,
@ColumnInfo val uid: String,
@ColumnInfo val updatedAt: String
)
@Dao
interface LecturerDao {
@Query("SELECT * FROM lecturerdb WHERE unnId = :unnId LIMIT 1")
suspend fun get(unnId: Int): LecturerDB?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(lecturers: List<LecturerDB>)
@Delete
fun delete(lecturer: LecturerDB)
}
fun LecturerDB.toModel() = Lecturer(name, rank, email, unnId, uid)
fun Lecturer.toEntity() = LecturerDB(
unnId,
name,
rank,
email,
uid,
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
@Entity
data class ScheduleUnitDB(
@PrimaryKey val oid: Int,
@ColumnInfo val date: String,
@ColumnInfo val stream: String,
@ColumnInfo val begin: String,
@ColumnInfo val end: String,
@ColumnInfo val auditoriumOid: Int,
@ColumnInfo val disciplineOid: Int,
@ColumnInfo val kindOfWorkOid: Int,
@ColumnInfo val lecturersId: String,
@ColumnInfo val updatedAt: String
)
@Dao
interface ScheduleItemDao {
@Query("SELECT * FROM scheduleUnitDB WHERE oid = :oid LIMIT 1")
suspend fun getSchedule(oid: Int): ScheduleUnitDB?
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
suspend fun getSchedule(date: String): List<ScheduleUnitDB>
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun getScheduleFlow(date: String): Flow<List<ScheduleUnitDB>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(units: List<ScheduleUnitDB>)
@Delete
suspend fun delete(unit: ScheduleUnitDB)
@Query("DELETE FROM scheduleUnitDB WHERE date = :date")
suspend fun deleteByDate(date: String)
@Query("SELECT * FROM scheduleUnitDB WHERE date = :date ORDER BY `begin`")
fun observe(date: String): Flow<List<ScheduleUnitDB>>
@Query("SELECT * FROM scheduleUnitDB")
suspend fun getAll(): List<ScheduleUnitDB>
}
suspend fun ScheduleUnitDB.toModel(db: AppDatabase): ScheduleUnit? {
return ScheduleUnit(
oid,
db.auditoriumDao().get(auditoriumOid)?.toModel(db) ?: return null,
LocalDate.parse(date, DateTimeFormatter.ISO_DATE),
db.disciplineDao().get(disciplineOid)?.toModel() ?: return null,
db.kindOfWorkDao().get(kindOfWorkOid)?.toModel() ?: return null,
lecturersId.split(",").map { db.lecturerDao().get(it.toInt())?.toModel() ?: return null },
stream,
LocalTime.parse(begin, DateTimeFormatter.ISO_TIME),
LocalTime.parse(end, DateTimeFormatter.ISO_TIME),
LocalDateTime.parse(updatedAt, DateTimeFormatter.ISO_DATE_TIME),
)
}
fun ScheduleUnit.toEntity() = ScheduleUnitDB(
oid,
date.format(DateTimeFormatter.ISO_DATE),
stream,
begin.format(DateTimeFormatter.ISO_TIME),
end.format(DateTimeFormatter.ISO_TIME),
auditorium.oid,
discipline.oid,
kindOfWork.oid,
lecturers.map { it.unnId }.joinToString(","),
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
)
suspend fun cacheSchedule(db: AppDatabase, units: List<ScheduleUnit>) {
db.withTransaction {
cacheAuditorium(db, units.map { it.auditorium })
db.disciplineDao().insert(units.map { it.discipline.toEntity() })
db.kindOfWorkDao().insert(units.map { it.kindOfWork.toEntity() })
db.lecturerDao().insert(units.flatMap { it.lecturers }.map { it.toEntity() })
db.scheduleDao().insert(units.map { it.toEntity() })
}
}
//fun loadSchedule(db: AppDatabase, date: LocalDate): List<ScheduleUnit> =
// db.scheduleDao().getSchedule(date.format(DateTimeFormatter.ISO_DATE))
// .mapNotNull { it.toModel(db) }
@@ -0,0 +1,74 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.db
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import ru.sweetbread.unn.core.db.entity.UserDB
@Dao
interface UserDao {
@Query("SELECT * FROM userDB WHERE userId = :userId LIMIT 1")
suspend fun get(userId: Int): UserDB?
@Insert
suspend fun insert(user: UserDB)
@Delete
fun delete(user: UserDB)
}
//fun cacheUser(user: User) {
// try {
// cacheDb.userDao().insert(
// UserDB(
// user.userId,
// user.unnId,
// user.type,
// user.email,
// user.nameRu,
// user.nameEn,
// user.isMale,
// user.birthday.format(DateTimeFormatter.ISO_LOCAL_DATE),
// user.avatar.original,
// user.avatar.thumbnail,
// user.avatar.small,
// LocalDateTime.now().plusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// )
// )
// } catch (_: android.database.sqlite.SQLiteConstraintException) {
// }
//}
//
//fun loadUserByBitrixId(bitrixId: Int): User? {
// val user = cacheDb.userDao().getUserById(bitrixId)
// Log.d("UserDB", user?.nameEn ?: "None")
// if (user == null) return null
// if (LocalDateTime.parse(
// user.expiredAt,
// DateTimeFormatter.ISO_LOCAL_DATE_TIME
// ) < LocalDateTime.now()
// ) {
// cacheDb.userDao().delete(user)
// return null
// } else {
// return User(
// user.unnId,
// user.userId,
// user.type,
// user.email,
// user.nameRu,
// user.nameEn,
// user.isMale,
// LocalDate.parse(user.birthday, DateTimeFormatter.ISO_LOCAL_DATE),
// AvatarSet(
// user.origAvatar,
// user.thumbAvatar,
// user.smallAvatar
// )
// )
// }
//}
@@ -48,6 +48,23 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
//val defUser = User(
// null,
// 123,
// Type.student,
// "cool.email@domain.com",
// "Джон Сигма Омегович",
// "Jon Sigma Omega",
// true,
// LocalDate.now(),
// AvatarSet(
// "https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg",
// "https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg",
// "https://upload.wikimedia.org/wikipedia/ru/thumb/9/94/%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg/500px-%D0%93%D0%B8%D0%B3%D0%B0%D1%87%D0%B0%D0%B4.jpg"
// )
//)
//
//
@Composable @Composable
fun Blogposts(viewModel: NewsViewModel = viewModel(factory = NewsViewModel.Factory)) { fun Blogposts(viewModel: NewsViewModel = viewModel(factory = NewsViewModel.Factory)) {
val news = viewModel.news.collectAsLazyPagingItems() val news = viewModel.news.collectAsLazyPagingItems()
@@ -151,6 +168,47 @@ fun PostItem(modifier: Modifier = Modifier, post: News, extended: Boolean = fals
)) ))
} }
} }
//
//
//@Preview
//@Composable
//fun UserItemPreview() {
// UNNTheme {
// Surface {
// UserItem(
// Modifier
// .width(300.dp)
// .clip(RoundedCornerShape(8.dp))
// .background(MaterialTheme.colorScheme.primaryContainer),
// defUser,
// Type.student.s
// )
// }
// }
//}
//
//@Preview
//@Composable
//fun PostItemPreview() {
// val post = Post(
// id = 154923,
// authorId = 165945,
// enableComments = true,
// numComments = 0,
// date = LocalDateTime.of(2024, 3, 20, 18, 55, 20),
// content = stringResource(id = R.string.lorem)
// )
//
// UNNTheme {
// Surface {
// PostItem(
// Modifier
// .clip(RoundedCornerShape(8.dp))
// .background(MaterialTheme.colorScheme.primaryContainer), post
// )
// }
// }
//}
class CoilImageGetter( class CoilImageGetter(
@@ -11,10 +11,10 @@ import androidx.paging.PagingState
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.io.IOException import kotlinx.io.IOException
import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.core.db.entity.NewsDB import ru.sweetbread.unn.core.db.entity.NewsDB
import ru.sweetbread.unn.core.db.entity.toEntity import ru.sweetbread.unn.core.db.entity.toEntity
import ru.sweetbread.unn.core.network.PortalApi import ru.sweetbread.unn.core.network.PortalApi
import ru.sweetbread.unn.db.AppDatabase
import ru.sweetbread.unn.feature.profile.UserRepository import ru.sweetbread.unn.feature.profile.UserRepository
import ru.sweetbread.unn.model.News import ru.sweetbread.unn.model.News
@@ -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() }
@@ -10,9 +10,9 @@ import androidx.paging.map
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
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.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.db.AppDatabase
import ru.sweetbread.unn.feature.profile.UserRepository import ru.sweetbread.unn.feature.profile.UserRepository
import ru.sweetbread.unn.model.News import ru.sweetbread.unn.model.News
@@ -8,7 +8,7 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.paging.cachedIn import androidx.paging.cachedIn
import ru.sweetbread.unn.app.UNNApp import ru.sweetbread.unn.UNNApp
class NewsViewModel( class NewsViewModel(
repository: NewsRepository repository: NewsRepository
@@ -3,10 +3,10 @@
package ru.sweetbread.unn.feature.profile package ru.sweetbread.unn.feature.profile
import android.util.Log import android.util.Log
import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.core.db.entity.toEntity import ru.sweetbread.unn.core.db.entity.toEntity
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.db.AppDatabase
import ru.sweetbread.unn.model.User import ru.sweetbread.unn.model.User
class UserRepository ( class UserRepository (
@@ -44,20 +44,16 @@ 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.app.UNNApp import ru.sweetbread.unn.api.Auditorium
import ru.sweetbread.unn.feature.schedule.components.BoundedWeekPicker import ru.sweetbread.unn.api.Building
import ru.sweetbread.unn.feature.schedule.components.GroupsNameText import ru.sweetbread.unn.api.Discipline
import ru.sweetbread.unn.model.Auditorium import ru.sweetbread.unn.api.KindOfWork
import ru.sweetbread.unn.model.Building import ru.sweetbread.unn.api.Lecturer
import ru.sweetbread.unn.model.Discipline import ru.sweetbread.unn.api.LecturerRank
import ru.sweetbread.unn.model.KindOfWork import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.model.Lecturer import ru.sweetbread.unn.ui.composes.BoundedWeekPicker
import ru.sweetbread.unn.model.LecturerRank import ru.sweetbread.unn.ui.composes.GroupsNameText
import ru.sweetbread.unn.model.ScheduleUnit
import ru.sweetbread.unn.model.Student
import ru.sweetbread.unn.model.Type
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.init.appCtx
import splitties.resources.appStr import splitties.resources.appStr
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -117,9 +113,6 @@ fun ScheduleDay(modifier: Modifier = Modifier, viewModel: ScheduleViewModel) {
@Composable @Composable
fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) { fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Boolean = false) {
val me = (appCtx as UNNApp).container.sessionManager.user.collectAsState().value
val currentGroup = if (me?.type == Type.Student) (me as Student).group else null
fun getRatio(): Float { fun getRatio(): Float {
val begin = LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond() val begin = LocalDateTime.of(unit.date, unit.begin).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
val end = LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond() val end = LocalDateTime.of(unit.date, unit.end).atZone(ZoneId.of("Europe/Moscow")).toEpochSecond()
@@ -209,9 +202,7 @@ fun ScheduleItem(modifier: Modifier = Modifier, unit: ScheduleUnit, expanded: Bo
AnimatedVisibility (expanded) { AnimatedVisibility (expanded) {
GroupsNameText( GroupsNameText(
unit.stream, unit.stream,
highlightColor = MaterialTheme.colorScheme.inversePrimary, highlightColor = MaterialTheme.colorScheme.inversePrimary
currentGroupName = currentGroup,
currentGroupColor = MaterialTheme.colorScheme.inverseSurface
) )
} }
} }
@@ -5,11 +5,12 @@ 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.core.db.AppDatabase import ru.sweetbread.unn.api.ScheduleUnit
import ru.sweetbread.unn.core.db.entity.cacheSchedule import ru.sweetbread.unn.core.SessionManager
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 ru.sweetbread.unn.db.AppDatabase
import ru.sweetbread.unn.db.cacheSchedule
import ru.sweetbread.unn.db.toModel
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
@@ -17,6 +18,7 @@ import java.time.format.DateTimeFormatter
class ScheduleRepository ( class ScheduleRepository (
val api: PortalApi, val api: PortalApi,
val db: AppDatabase, val db: AppDatabase,
val session: SessionManager
) { ) {
fun observeSchedule(date: LocalDate): Flow<List<ScheduleUnit>> = fun observeSchedule(date: LocalDate): Flow<List<ScheduleUnit>> =
db.scheduleDao() db.scheduleDao()
@@ -14,8 +14,8 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest 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.UNNApp
import ru.sweetbread.unn.model.ScheduleUnit import ru.sweetbread.unn.api.ScheduleUnit
import java.time.LocalDate import java.time.LocalDate
data class ScheduleUiState( data class ScheduleUiState(
@@ -4,12 +4,15 @@ 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)),
Lecturer(appStr(R.string.lecturer)), // group("group"),
Employee(appStr(R.string.employee)) lecturer(appStr(R.string.lecturer)),
// auditorium("auditorium"),
employee(appStr(R.string.employee))
} }
open class User( open class User(
@@ -29,8 +32,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"),
@@ -46,17 +49,17 @@ open class User(
companion object { companion object {
fun fromProfile(json: JSONObject): User { fun fromProfile(json: JSONObject): User {
val userField = json.getJSONObject("user") val user = json.getJSONObject("user")
val user = User( return User(
userField.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
}, },
userField.getString("email"), user.getString("email"),
userField.getString("fullname"), user.getString("fullname"),
userField.getString("fullname_en"), user.getString("fullname_en"),
userField.getJSONObject("photo").let { user.getJSONObject("photo").let {
AvatarSet( AvatarSet(
it.getString("orig"), it.getString("orig"),
it.getString("thumbnail"), it.getString("thumbnail"),
@@ -64,16 +67,11 @@ open class User(
) )
} }
) )
if (user.type == Type.Student)
user = Student()
return user
} }
fun getPlaceholder() = User( fun getPlaceholder() = User(
0, 0,
Type.Employee, Type.employee,
"", "",
"Удалённый пользователь", "Удалённый пользователь",
"Deleted user", "Deleted user",
@@ -110,9 +108,3 @@ class Student(
group group
) )
} }
class AvatarSet(
val original: String,
val thumbnail: String,
val small: String
)
@@ -1,6 +1,6 @@
// Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.feature package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -1,6 +1,6 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.feature.schedule.components package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -20,25 +20,14 @@ import androidx.compose.ui.unit.dp
fun GroupsNameText( fun GroupsNameText(
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
highlightColor: Color = MaterialTheme.colorScheme.tertiaryContainer, highlightColor: Color = MaterialTheme.colorScheme.secondaryContainer,
highlightPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp), highlightPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp),
highlightShape: Shape = MaterialTheme.shapes.small, highlightShape: Shape = MaterialTheme.shapes.small,
currentGroupName: String? = null,
currentGroupColor: Color = MaterialTheme.colorScheme.secondaryContainer,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
) { ) {
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 = if (mainPart.startsWith("Поток_(") && mainPart.endsWith(')')) { val blocks = mainPart.split('|')
mainPart
.removePrefix("Поток_(")
.dropLast(1)
.split(';')
.map(String::trim)
.filter(String::isNotEmpty)
} else {
mainPart.split('|')
}
FlowRow( FlowRow(
modifier = modifier, modifier = modifier,
@@ -49,10 +38,7 @@ fun GroupsNameText(
Box( Box(
modifier = Modifier modifier = Modifier
.padding(end = 2.dp) .padding(end = 2.dp)
.background( .background(highlightColor, highlightShape)
if (currentGroupName == block) currentGroupColor
else highlightColor, highlightShape
)
.padding(highlightPadding) .padding(highlightPadding)
) { ) {
Text(text = block) Text(text = block)
@@ -1,5 +1,5 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.feature.schedule.components package ru.sweetbread.unn.ui.composes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -1,6 +1,6 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.app package ru.sweetbread.unn.ui.layout
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@@ -38,7 +38,9 @@ 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.UNNApp
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
import splitties.activities.start import splitties.activities.start
@@ -50,10 +52,12 @@ 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 {
@@ -159,3 +163,11 @@ fun LoginPanel(
} }
} }
} }
//@Preview(showBackground = true)
//@Composable
//fun LoginPreview() {
// UNNTheme {
// LoginPanel(ok = { _, _ -> }, error = {})
// }
//}
@@ -1,10 +1,7 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). // Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.app package ru.sweetbread.unn.ui.layout
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
@@ -55,33 +52,24 @@ 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.UNNApp
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.ui.composes.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?) {
@@ -89,21 +77,9 @@ 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(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navController = rememberNavController() val navController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -200,6 +176,7 @@ class MainActivity : ComponentActivity() {
Box(Modifier.padding(innerPadding)) { Box(Modifier.padding(innerPadding)) {
NavHost(navController, startDestination = "journal/schedule") { NavHost(navController, startDestination = "journal/schedule") {
composable("portal/blogposts") { composable("portal/blogposts") {
// Text("Not implemented")
Blogposts() Blogposts()
} }
composable("journal/schedule") { composable("journal/schedule") {
@@ -217,44 +194,6 @@ 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