11 Commits

Author SHA1 Message Date
dependabot[bot]
3286df2b8a Bump com.google.dagger:hilt-android from 2.55 to 2.57.2
Bumps [com.google.dagger:hilt-android](https://github.com/google/dagger) from 2.55 to 2.57.2.
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.55...dagger-2.57.2)

---
updated-dependencies:
- dependency-name: com.google.dagger:hilt-android
  dependency-version: 2.57.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 02:10:02 +00:00
Nikita
09871faaca Merge pull request #20 from n08i40k/dependabot/gradle/com.google.firebase-firebase-messaging-24.1.1
Bump com.google.firebase:firebase-messaging from 24.1.0 to 24.1.1
2025-03-25 02:18:17 +04:00
dependabot[bot]
d6fd1ae5ff Merge pull request #22 from n08i40k/dependabot/gradle/com.google.devtools.ksp-2.1.20-1.0.31 2025-03-24 22:15:15 +00:00
dependabot[bot]
e7eda47e13 Merge pull request #21 from n08i40k/dependabot/gradle/com.google.dagger-hilt-android-compiler-2.56 2025-03-24 22:15:07 +00:00
dependabot[bot]
6d9f6c6b4e Bump com.google.devtools.ksp from 2.1.10-1.0.30 to 2.1.20-1.0.31
Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 2.1.10-1.0.30 to 2.1.20-1.0.31.
- [Release notes](https://github.com/google/ksp/releases)
- [Commits](https://github.com/google/ksp/compare/2.1.10-1.0.30...2.1.20-1.0.31)

---
updated-dependencies:
- dependency-name: com.google.devtools.ksp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 22:13:41 +00:00
dependabot[bot]
b9cf49023a Merge pull request #23 from n08i40k/dependabot/gradle/plugin.serialization-2.1.20 2025-03-24 22:12:48 +00:00
dependabot[bot]
f8dee292dd Bump plugin.serialization from 2.1.10 to 2.1.20
Bumps [plugin.serialization](https://github.com/JetBrains/kotlin) from 2.1.10 to 2.1.20.
- [Release notes](https://github.com/JetBrains/kotlin/releases)
- [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md)
- [Commits](https://github.com/JetBrains/kotlin/compare/v2.1.10...v2.1.20)

---
updated-dependencies:
- dependency-name: plugin.serialization
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:45:15 +00:00
dependabot[bot]
c6fc268c19 Bump com.google.dagger:hilt-android-compiler from 2.55 to 2.56
Bumps [com.google.dagger:hilt-android-compiler](https://github.com/google/dagger) from 2.55 to 2.56.
- [Release notes](https://github.com/google/dagger/releases)
- [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/dagger/compare/dagger-2.55...dagger-2.56)

---
updated-dependencies:
- dependency-name: com.google.dagger:hilt-android-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:42:23 +00:00
dependabot[bot]
3400015895 Bump com.google.firebase:firebase-messaging from 24.1.0 to 24.1.1
Bumps [com.google.firebase:firebase-messaging](https://github.com/firebase/firebase-android-sdk) from 24.1.0 to 24.1.1.
- [Commits](https://github.com/firebase/firebase-android-sdk/commits)

---
updated-dependencies:
- dependency-name: com.google.firebase:firebase-messaging
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:41:16 +00:00
dbc1afcf28 3.2.0
Изменённый дизайн.
2025-03-20 07:56:33 +04:00
96f84b9f54 3.1.1
Кешированные ответы от сервера теперь находятся в специальном файле, а не в настройках.

Возвращён просмотр подробностей о паре.
2025-03-20 04:40:57 +04:00
43 changed files with 836 additions and 342 deletions

6
.idea/kotlinc.xml generated
View File

@@ -7,10 +7,10 @@
<option name="jvmTarget" value="1.8" /> <option name="jvmTarget" value="1.8" />
</component> </component>
<component name="KotlinCommonCompilerArguments"> <component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.0" /> <option name="apiVersion" value="2.1" />
<option name="languageVersion" value="2.0" /> <option name="languageVersion" value="2.1" />
</component> </component>
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.10" /> <option name="version" value="2.1.10" />
</component> </component>
</project> </project>

View File

@@ -7,7 +7,7 @@ plugins {
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.1.10" kotlin("plugin.serialization") version "2.1.20"
id("kotlin-parcelize") id("kotlin-parcelize")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
@@ -46,8 +46,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next" applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 27 versionCode = 29
versionName = "3.1.0" versionName = "3.2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -136,6 +136,7 @@ dependencies {
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
// default // default
implementation(libs.androidx.runtime.tracing)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -158,7 +159,7 @@ dependencies {
protobuf { protobuf {
protoc { protoc {
artifact = "com.google.protobuf:protoc:4.29.3" artifact = "com.google.protobuf:protoc:21.0-rc-1"
} }
plugins { plugins {

View File

@@ -18,7 +18,8 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.app.AppContainer import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.proto.settings_v0
import ru.n08i40k.polytechnic.next.utils.Observable import ru.n08i40k.polytechnic.next.utils.Observable
import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker
import ru.n08i40k.polytechnic.next.worker.UpdateLinkWorker import ru.n08i40k.polytechnic.next.worker.UpdateLinkWorker
@@ -97,8 +98,35 @@ class Application : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
runBlocking { fixupSettings() }
VKID.init(this) VKID.init(this)
setupFirebase() setupFirebase()
} }
private suspend fun fixupSettings() {
val accessToken = this.settings_v0.data.map { it.accessToken }.first()
if (accessToken.isEmpty())
return
val userId = this.settings_v0.data.map { it.userId }.first()
val group = this.settings_v0.data.map { it.group }.first()
val version = this.settings_v0.data.map { it.version }.first()
val fcmToken = this.settings_v0.data.map { it.fcmToken }.first()
this.settings.updateData {
it
.toBuilder()
.setUserId(userId)
.setAccessToken(accessToken)
.setGroup(group)
.setVersion(version)
.setFcmToken(fcmToken)
.build()
}
this.settings_v0.updateData { it.toBuilder().clear().build() }
}
} }

View File

@@ -22,7 +22,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.app.NotificationChannels import ru.n08i40k.polytechnic.next.app.NotificationChannels
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.PolytechnicApp import ru.n08i40k.polytechnic.next.ui.PolytechnicApp
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
import ru.n08i40k.polytechnic.next.utils.app import ru.n08i40k.polytechnic.next.utils.app

View File

@@ -5,6 +5,7 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.limit import ru.n08i40k.polytechnic.next.utils.limit
@Parcelize @Parcelize
@@ -17,6 +18,8 @@ data class Lesson(
val group: String? = null, val group: String? = null,
val subGroups: List<SubGroup> val subGroups: List<SubGroup>
) : Parcelable { ) : Parcelable {
val duration: Int get() = time.end.dayMinutes - time.start.dayMinutes
fun getShortName(context: Context): String { fun getShortName(context: Context): String {
val name = val name =
if (type == LessonType.BREAK) if (type == LessonType.BREAK)

View File

@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.app.AppContainer import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.RequestBase import ru.n08i40k.polytechnic.next.network.RequestBase
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
open class AuthorizedRequest( open class AuthorizedRequest(
val appContainer: AppContainer, val appContainer: AppContainer,

View File

@@ -0,0 +1,29 @@
package ru.n08i40k.polytechnic.next.proto
import android.content.Context
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.google.protobuf.InvalidProtocolBufferException
import ru.n08i40k.polytechnic.next.Cache
import java.io.InputStream
import java.io.OutputStream
object CacheSerializer : Serializer<Cache> {
override val defaultValue: Cache = Cache.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Cache =
try {
Cache.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
override suspend fun writeTo(t: Cache, output: OutputStream) = t.writeTo(output)
}
val Context.cache: DataStore<Cache> by dataStore(
fileName = "cache.pb",
serializer = CacheSerializer
)

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.settings package ru.n08i40k.polytechnic.next.proto
import android.content.Context import android.content.Context
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
@@ -23,7 +23,7 @@ object SettingsSerializer : Serializer<Settings> {
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output) override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
} }
val Context.settings: DataStore<Settings> by dataStore( val Context.settings_v0: DataStore<Settings> by dataStore(
fileName = "settings.pb", fileName = "settings.pb",
serializer = SettingsSerializer serializer = SettingsSerializer
) )

View File

@@ -0,0 +1,29 @@
package ru.n08i40k.polytechnic.next.proto
import android.content.Context
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.google.protobuf.InvalidProtocolBufferException
import ru.n08i40k.polytechnic.next.SettingsV2
import java.io.InputStream
import java.io.OutputStream
object SettingsV2Serializer : Serializer<SettingsV2> {
override val defaultValue: SettingsV2 = SettingsV2.getDefaultInstance()
override suspend fun readFrom(input: InputStream): SettingsV2 =
try {
SettingsV2.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
override suspend fun writeTo(t: SettingsV2, output: OutputStream) = t.writeTo(output)
}
val Context.settings: DataStore<SettingsV2> by dataStore(
fileName = "settings-v2.pb",
serializer = SettingsV2Serializer
)

View File

@@ -1,12 +1,12 @@
package ru.n08i40k.polytechnic.next.repository.cache package ru.n08i40k.polytechnic.next.repository.cache
import ru.n08i40k.polytechnic.next.CachedResponse import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.CacheResponse
interface NetworkCacheRepository { interface NetworkCacheRepository {
suspend fun put(url: String, data: String) suspend fun put(url: String, data: String)
suspend fun get(url: String): CachedResponse? suspend fun get(url: String): CacheResponse?
suspend fun clear() suspend fun clear()
@@ -14,7 +14,7 @@ interface NetworkCacheRepository {
suspend fun setHash(hash: String) suspend fun setHash(hash: String)
suspend fun getUpdateDates(): UpdateDates suspend fun getUpdateDates(): CacheDate
suspend fun setUpdateDates(cache: Long, schedule: Long) suspend fun setUpdateDates(cache: Long, schedule: Long)
} }

View File

@@ -5,17 +5,18 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.CachedResponse import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.CacheResponse
import ru.n08i40k.polytechnic.next.app.AppContainer import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.proto.cache
import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository
import ru.n08i40k.polytechnic.next.settings.settings
import javax.inject.Inject import javax.inject.Inject
class LocalNetworkCacheRepository class LocalNetworkCacheRepository
@Inject constructor(private val appContainer: AppContainer) : NetworkCacheRepository { @Inject constructor(private val appContainer: AppContainer) : NetworkCacheRepository {
private val cacheMap: MutableMap<String, CachedResponse> = mutableMapOf() private val cacheMap: MutableMap<String, CacheResponse> = mutableMapOf()
private var updateDates: UpdateDates = UpdateDates.newBuilder().build() private var cacheDate: CacheDate = CacheDate.newBuilder().build()
private var hash: String? = null private var hash: String? = null
private val context get() = appContainer.context private val context get() = appContainer.context
@@ -26,14 +27,14 @@ class LocalNetworkCacheRepository
runBlocking { runBlocking {
cacheMap.putAll( cacheMap.putAll(
context context
.settings .cache
.data .data
.map { settings -> settings.cacheStorageMap }.first() .map { it.storageMap }.first()
) )
} }
} }
override suspend fun get(url: String): CachedResponse? { override suspend fun get(url: String): CacheResponse? {
// Если кешированного ответа нет, то возвращаем null // Если кешированного ответа нет, то возвращаем null
// Если хеши не совпадают и локальный хеш присутствует, то возвращаем null // Если хеши не совпадают и локальный хеш присутствует, то возвращаем null
@@ -49,7 +50,7 @@ class LocalNetworkCacheRepository
if (hash == null) if (hash == null)
throw IllegalStateException("Не установлен хеш!") throw IllegalStateException("Не установлен хеш!")
cacheMap[url] = CachedResponse cacheMap[url] = CacheResponse
.newBuilder() .newBuilder()
.setHash(this.hash) .setHash(this.hash)
.setData(data) .setData(data)
@@ -83,21 +84,21 @@ class LocalNetworkCacheRepository
} }
} }
override suspend fun getUpdateDates(): UpdateDates { override suspend fun getUpdateDates(): CacheDate {
return this.updateDates return this.cacheDate
} }
override suspend fun setUpdateDates(cache: Long, schedule: Long) { override suspend fun setUpdateDates(cache: Long, schedule: Long) {
updateDates = UpdateDates cacheDate = CacheDate
.newBuilder() .newBuilder()
.setCache(cache) .setCache(cache)
.setSchedule(schedule).build() .setSchedule(schedule).build()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
context.settings.updateData { context.cache.updateData {
it it
.toBuilder() .toBuilder()
.setUpdateDates(updateDates) .setDate(cacheDate)
.build() .build()
} }
} }
@@ -106,10 +107,10 @@ class LocalNetworkCacheRepository
private suspend fun save() { private suspend fun save() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
context.settings.updateData { context.cache.updateData {
it it
.toBuilder() .toBuilder()
.putAllCacheStorage(cacheMap) .putAllStorage(cacheMap)
.build() .build()
} }
} }

View File

@@ -1,11 +1,11 @@
package ru.n08i40k.polytechnic.next.repository.cache.impl package ru.n08i40k.polytechnic.next.repository.cache.impl
import ru.n08i40k.polytechnic.next.CachedResponse import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.CacheResponse
import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository
class MockNetworkCacheRepository : NetworkCacheRepository { class MockNetworkCacheRepository : NetworkCacheRepository {
override suspend fun get(url: String): CachedResponse? { override suspend fun get(url: String): CacheResponse? {
return null return null
} }
@@ -19,8 +19,8 @@ class MockNetworkCacheRepository : NetworkCacheRepository {
override suspend fun setHash(hash: String) {} override suspend fun setHash(hash: String) {}
override suspend fun getUpdateDates(): UpdateDates { override suspend fun getUpdateDates(): CacheDate {
return UpdateDates.newBuilder().build() return CacheDate.newBuilder().build()
} }
override suspend fun setUpdateDates(cache: Long, schedule: Long) {} override suspend fun setUpdateDates(cache: Long, schedule: Long) {}

View File

@@ -7,8 +7,9 @@ import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.request.fcm.FcmSetToken import ru.n08i40k.polytechnic.next.network.request.fcm.FcmSetToken
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.proto.cache
import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.repository.profile.ProfileRepository import ru.n08i40k.polytechnic.next.repository.profile.ProfileRepository
import ru.n08i40k.polytechnic.next.settings.settings
import ru.n08i40k.polytechnic.next.utils.MyResult import ru.n08i40k.polytechnic.next.utils.MyResult
import ru.n08i40k.polytechnic.next.utils.app import ru.n08i40k.polytechnic.next.utils.app
@@ -40,7 +41,14 @@ class RemoteProfileRepository(private val container: AppContainer) : ProfileRepo
override suspend fun signOut() { override suspend fun signOut() {
val context = container.context val context = container.context
container.context.settings.updateData { context.settings.updateData {
it
.toBuilder()
.clear()
.build()
}
context.cache.updateData {
it it
.toBuilder() .toBuilder()
.clear() .clear()
@@ -48,12 +56,5 @@ class RemoteProfileRepository(private val container: AppContainer) : ProfileRepo
} }
context.app.events.signOut.next(Unit) context.app.events.signOut.next(Unit)
// context.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)
// val pm = context.packageManager
// val intent = pm.getLaunchIntentForPackage(context.packageName)
// val mainIntent = Intent.makeRestartActivityTask(intent?.component)
// context.startActivity(mainIntent)
// Runtime.getRuntime().exit(0)
} }
} }

View File

@@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.Application import ru.n08i40k.polytechnic.next.Application
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.screen.MainScreen import ru.n08i40k.polytechnic.next.ui.screen.MainScreen
import ru.n08i40k.polytechnic.next.ui.screen.auth.AuthScreen import ru.n08i40k.polytechnic.next.ui.screen.auth.AuthScreen
import ru.n08i40k.polytechnic.next.utils.app import ru.n08i40k.polytechnic.next.utils.app

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.app.AppContainer import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.utils.MyResult import ru.n08i40k.polytechnic.next.utils.MyResult
@@ -25,7 +25,7 @@ sealed interface GroupUiState {
data class HasData( data class HasData(
val group: GroupOrTeacher, val group: GroupOrTeacher,
val updateDates: UpdateDates, val cacheDate: CacheDate,
val lastUpdateAt: Long, val lastUpdateAt: Long,
override val isLoading: Boolean override val isLoading: Boolean
) : GroupUiState ) : GroupUiState
@@ -33,7 +33,7 @@ sealed interface GroupUiState {
private data class GroupViewModelState( private data class GroupViewModelState(
val group: GroupOrTeacher? = null, val group: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null, val updateDates: CacheDate? = null,
val lastUpdateAt: Long = 0, val lastUpdateAt: Long = 0,
val isLoading: Boolean = false val isLoading: Boolean = false
) { ) {

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.app.AppContainer import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.utils.MyResult import ru.n08i40k.polytechnic.next.utils.MyResult
@@ -25,7 +25,7 @@ sealed interface SearchUiState {
data class HasData( data class HasData(
val teacher: GroupOrTeacher, val teacher: GroupOrTeacher,
val updateDates: UpdateDates, val cacheDate: CacheDate,
val lastUpdateAt: Long, val lastUpdateAt: Long,
override val isLoading: Boolean override val isLoading: Boolean
) : SearchUiState ) : SearchUiState
@@ -33,7 +33,7 @@ sealed interface SearchUiState {
private data class SearchViewModelState( private data class SearchViewModelState(
val teacher: GroupOrTeacher? = null, val teacher: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null, val updateDates: CacheDate? = null,
val lastUpdateAt: Long = 0, val lastUpdateAt: Long = 0,
val isLoading: Boolean = false val isLoading: Boolean = false
) { ) {

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.app.AppContainer import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.utils.MyResult import ru.n08i40k.polytechnic.next.utils.MyResult
@@ -24,7 +24,7 @@ sealed interface TeacherUiState {
data class HasData( data class HasData(
val teacher: GroupOrTeacher, val teacher: GroupOrTeacher,
val updateDates: UpdateDates, val cacheDate: CacheDate,
val lastUpdateAt: Long, val lastUpdateAt: Long,
override val isLoading: Boolean override val isLoading: Boolean
) : TeacherUiState ) : TeacherUiState
@@ -32,7 +32,7 @@ sealed interface TeacherUiState {
private data class TeacherViewModelState( private data class TeacherViewModelState(
val teacher: GroupOrTeacher? = null, val teacher: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null, val updateDates: CacheDate? = null,
val lastUpdateAt: Long = 0, val lastUpdateAt: Long = 0,
val isLoading: Boolean = false val isLoading: Boolean = false
) { ) {

View File

@@ -38,7 +38,7 @@ import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.Application import ru.n08i40k.polytechnic.next.Application
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.AppRoute import ru.n08i40k.polytechnic.next.ui.AppRoute
import ru.n08i40k.polytechnic.next.ui.icons.AppIcons import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled

View File

@@ -35,6 +35,8 @@ import androidx.navigation.compose.rememberNavController
import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Task
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.proto.cache
import ru.n08i40k.polytechnic.next.ui.AppRoute import ru.n08i40k.polytechnic.next.ui.AppRoute
import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
import ru.n08i40k.polytechnic.next.ui.helper.SnackbarBox import ru.n08i40k.polytechnic.next.ui.helper.SnackbarBox
@@ -61,8 +63,7 @@ private fun FormWrapper(
with(localDensity) { with(localDensity) {
onWidthChange(it.size.width.toDp()) onWidthChange(it.size.width.toDp())
} }
} },
/*.animateContentSize()*/,
content = content content = content
) )
} }
@@ -84,6 +85,8 @@ private fun AuthForm(parentNavController: NavController, pushSnackbar: PushSnack
} }
val finish: () -> Unit = { val finish: () -> Unit = {
runBlocking { context.cache.updateData { it.toBuilder().clear().build() } }
parentNavController.navigate(AppRoute.MAIN.route) { parentNavController.navigate(AppRoute.MAIN.route) {
popUpTo(AppRoute.AUTH.route) { inclusive = true } popUpTo(AppRoute.AUTH.route) { inclusive = true }
} }

View File

@@ -40,7 +40,7 @@ import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn
import ru.n08i40k.polytechnic.next.network.unwrapException import ru.n08i40k.polytechnic.next.network.unwrapException
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
import java.util.logging.Logger import java.util.logging.Logger

View File

@@ -8,7 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignInVK import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignInVK
import ru.n08i40k.polytechnic.next.network.unwrapException import ru.n08i40k.polytechnic.next.network.unwrapException
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.widgets.OneTapComplete import ru.n08i40k.polytechnic.next.ui.widgets.OneTapComplete
import java.util.logging.Logger import java.util.logging.Logger

View File

@@ -42,7 +42,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp
import ru.n08i40k.polytechnic.next.network.unwrapException import ru.n08i40k.polytechnic.next.network.unwrapException
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector

View File

@@ -41,7 +41,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUpVK import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUpVK
import ru.n08i40k.polytechnic.next.network.unwrapException import ru.n08i40k.polytechnic.next.network.unwrapException
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector

View File

@@ -2,9 +2,8 @@ package ru.n08i40k.polytechnic.next.ui.screen.schedule
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -24,6 +23,7 @@ import ru.n08i40k.polytechnic.next.ui.model.GroupUiState
import ru.n08i40k.polytechnic.next.ui.model.GroupViewModel import ru.n08i40k.polytechnic.next.ui.model.GroupViewModel
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager
import ru.n08i40k.polytechnic.next.ui.widgets.schedule.UpdateInfo
import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner
@Composable @Composable
@@ -62,11 +62,10 @@ fun GroupScheduleScreen(viewModel: GroupViewModel) {
) { ) {
when (uiState) { when (uiState) {
is GroupUiState.HasData -> { is GroupUiState.HasData -> {
Column { Column(Modifier.padding(20.dp), Arrangement.spacedBy(10.dp)) {
val data = uiState as GroupUiState.HasData val data = uiState as GroupUiState.HasData
UpdateInfo(data.lastUpdateAt, data.updateDates) UpdateInfo(data.lastUpdateAt, data.cacheDate)
Spacer(Modifier.height(10.dp))
SchedulePager(data.group) SchedulePager(data.group)
} }
} }

View File

@@ -2,9 +2,8 @@ package ru.n08i40k.polytechnic.next.ui.screen.schedule
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -24,6 +23,7 @@ import ru.n08i40k.polytechnic.next.ui.model.TeacherUiState
import ru.n08i40k.polytechnic.next.ui.model.TeacherViewModel import ru.n08i40k.polytechnic.next.ui.model.TeacherViewModel
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager
import ru.n08i40k.polytechnic.next.ui.widgets.schedule.UpdateInfo
import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner
@Composable @Composable
@@ -62,11 +62,10 @@ fun TeacherScheduleScreen(viewModel: TeacherViewModel) {
) { ) {
when (uiState) { when (uiState) {
is TeacherUiState.HasData -> { is TeacherUiState.HasData -> {
Column { Column(Modifier.padding(20.dp), Arrangement.spacedBy(10.dp)) {
val data = uiState as TeacherUiState.HasData val data = uiState as TeacherUiState.HasData
UpdateInfo(data.lastUpdateAt, data.updateDates) UpdateInfo(data.lastUpdateAt, data.cacheDate)
Spacer(Modifier.height(10.dp))
SchedulePager(data.teacher) SchedulePager(data.teacher)
} }
} }

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,6 +29,7 @@ import ru.n08i40k.polytechnic.next.ui.model.SearchUiState
import ru.n08i40k.polytechnic.next.ui.model.SearchViewModel import ru.n08i40k.polytechnic.next.ui.model.SearchViewModel
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager
import ru.n08i40k.polytechnic.next.ui.widgets.schedule.UpdateInfo
import ru.n08i40k.polytechnic.next.ui.widgets.selector.TeacherNameSelector import ru.n08i40k.polytechnic.next.ui.widgets.selector.TeacherNameSelector
import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner
@@ -82,11 +84,10 @@ fun TeacherSearchScreen(viewModel: SearchViewModel) {
when (uiState) { when (uiState) {
is SearchUiState.HasData -> { is SearchUiState.HasData -> {
Column { Column(Modifier.padding(20.dp), Arrangement.spacedBy(10.dp)) {
val data = uiState as SearchUiState.HasData val data = uiState as SearchUiState.HasData
UpdateInfo(data.lastUpdateAt, data.updateDates) UpdateInfo(data.lastUpdateAt, data.cacheDate)
Spacer(Modifier.height(10.dp))
SchedulePager(data.teacher) SchedulePager(data.teacher)
} }
} }

View File

@@ -10,7 +10,6 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -32,17 +31,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
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.compose.ui.unit.dp
@Composable @Composable
fun ExpandableCard( fun ExpandableCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: CardColors = CardDefaults.cardColors(), colors: CardColors = CardDefaults.cardColors(),
border: BorderStroke = BorderStroke(
Dp.Hairline,
MaterialTheme.colorScheme.inverseSurface
),
expanded: Boolean = false, expanded: Boolean = false,
onExpandedChange: () -> Unit, onExpandedChange: () -> Unit,
title: @Composable () -> Unit, title: @Composable () -> Unit,
@@ -61,8 +55,7 @@ fun ExpandableCard(
onExpandedChange() onExpandedChange()
transitionState.targetState = expanded transitionState.targetState = expanded
}, },
colors = colors, colors = colors
border = border
) { ) {
Column { Column {
ExpandableCardHeader(title, transition) ExpandableCardHeader(title, transition)
@@ -76,15 +69,10 @@ fun ExpandableCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: @Composable () -> Unit, title: @Composable () -> Unit,
colors: CardColors = CardDefaults.cardColors(), colors: CardColors = CardDefaults.cardColors(),
border: BorderStroke = BorderStroke(
Dp.Hairline,
MaterialTheme.colorScheme.inverseSurface
),
) { ) {
Card( Card(
modifier = modifier, modifier = modifier,
colors = colors, colors = colors
border = border
) { ) {
ExpandableCardHeader(title, null) ExpandableCardHeader(title, null)
} }

View File

@@ -1,31 +1,31 @@
package ru.n08i40k.polytechnic.next.ui.widgets.schedule package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -34,8 +34,9 @@ import kotlinx.coroutines.flow.flow
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.LessonType import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
import ru.n08i40k.polytechnic.next.utils.dateTime import ru.n08i40k.polytechnic.next.utils.dateTime
import ru.n08i40k.polytechnic.next.utils.now import ru.n08i40k.polytechnic.next.utils.now
@@ -76,81 +77,42 @@ private fun getCurrentLessonIdx(day: Day?): Flow<Int> {
return value return value
} }
@Preview(showBackground = true) @PreviewLightDark
@Composable
private fun DayCardPreview() {
AppTheme {
Surface(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))
) {
DayCard(Modifier, MockScheduleRepository.exampleTeacher.days[0]) {}
}
}
}
@Composable @Composable
fun DayCard( fun DayCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
day: Day = MockScheduleRepository.exampleTeacher.days[0] day: Day,
onLessonClick: (Lesson) -> Unit,
) { ) {
val offset = remember(day) { getDayOffset(day) } val offset = remember(day) { getDayOffset(day) }
val defaultCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
)
val customCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
)
val noneCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
)
val examCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
)
Card( Card(
modifier, modifier,
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = when (offset) { containerColor = MaterialTheme.colorScheme.surfaceContainerLowest
DayOffset.TODAY -> MaterialTheme.colorScheme.primaryContainer )
else -> MaterialTheme.colorScheme.secondaryContainer
}
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
) { ) {
Text(
day.name,
Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
if (day.street != null) {
Text(
day.street,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
)
}
if (offset != DayOffset.OTHER) {
Text(
stringResource(
when (offset) {
DayOffset.YESTERDAY -> R.string.yesterday
DayOffset.TODAY -> R.string.today
DayOffset.TOMORROW -> R.string.tomorrow
DayOffset.OTHER -> throw RuntimeException()
}
),
Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
)
}
val currentLessonIndex by getCurrentLessonIdx(if (offset == DayOffset.TODAY) day else null) val currentLessonIndex by getCurrentLessonIdx(if (offset == DayOffset.TODAY) day else null)
.collectAsStateWithLifecycle(0) .collectAsStateWithLifecycle(0)
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState())
.padding(10.dp),
Arrangement.spacedBy(0.5.dp) Arrangement.spacedBy(0.5.dp)
) { ) {
if (day.lessons.isEmpty()) { if (day.lessons.isEmpty()) {
@@ -159,39 +121,10 @@ fun DayCard(
} }
for (lessonIndex in day.lessons.indices) { for (lessonIndex in day.lessons.indices) {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.inversePrimary
)
val lesson = day.lessons[lessonIndex] val lesson = day.lessons[lessonIndex]
val cardColors = when (lesson.type) { Box(Modifier.clickable { onLessonClick(lesson) }) {
LessonType.DEFAULT -> defaultCardColors LessonRow(modifier, lesson, lessonIndex == currentLessonIndex)
LessonType.ADDITIONAL -> noneCardColors
LessonType.BREAK -> noneCardColors
LessonType.CONSULTATION -> customCardColors
LessonType.INDEPENDENT_WORK -> customCardColors
LessonType.EXAM -> examCardColors
LessonType.EXAM_WITH_GRADE -> examCardColors
LessonType.EXAM_DEFAULT -> examCardColors
}
// TODO: Вернуть ExtraInfo
var extraInfo by remember { mutableStateOf(false) }
Box(
Modifier
.clickable { extraInfo = true }
.background(cardColors.containerColor)
) {
val modifier =
if (lessonIndex == currentLessonIndex)
Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.error))
else
Modifier
LessonRow(modifier, lesson, cardColors)
} }
} }
} }

View File

@@ -0,0 +1,150 @@
package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.window.Dialog
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.fmtAsClock
class LessonPreviewParameterProvider : PreviewParameterProvider<Lesson> {
override val values: Sequence<Lesson>
get() {
val lessons = MockScheduleRepository.exampleGroup.days[0].lessons
return sequenceOf(
lessons[0],
lessons[2],
lessons[4],
lessons[6],
)
}
}
@Preview
@Composable
private fun ExtraInfoDialogPreview(
@PreviewParameter(LessonPreviewParameterProvider::class) lesson: Lesson
) {
ExtraInfoDialog(lesson) { }
}
@Composable
fun ExtraInfoDialog(
lesson: Lesson,
onDismiss: () -> Unit
) {
Dialog(onDismiss) {
Card {
Column(Modifier.padding(10.dp)) {
var minWidth by remember { mutableStateOf(Dp.Unspecified) }
val density = LocalDensity.current
@Composable
fun kvText(title: String, text: String) {
Row(
Modifier.alpha(if (minWidth == Dp.Unspecified) 0f else 1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
title,
Modifier
.onGloballyPositioned {
with(density) {
val dp = it.size.width.toDp()
minWidth =
if (minWidth == Dp.Unspecified)
dp
else
max(minWidth, dp)
}
}
.size(minWidth, Dp.Unspecified),
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
textAlign = TextAlign.Right,
)
Text(text)
}
}
kvText(stringResource(R.string.extra_info_lesson_name), lesson.name ?: "")
when (lesson.type) {
LessonType.BREAK -> throw IllegalArgumentException()
LessonType.DEFAULT -> null
LessonType.ADDITIONAL -> null
LessonType.CONSULTATION -> R.string.lesson_type_consultation
LessonType.INDEPENDENT_WORK -> R.string.lesson_type_independent_work
LessonType.EXAM -> R.string.lesson_type_exam
LessonType.EXAM_WITH_GRADE -> R.string.lesson_type_exam_with_grade
LessonType.EXAM_DEFAULT -> R.string.lesson_type_exam_default
}?.let {
kvText(stringResource(R.string.extra_info_type), stringResource(it))
}
if (lesson.subGroups.size == 1) {
kvText(
stringResource(R.string.extra_info_teacher),
stringResource(
R.string.extra_info_teacher_second,
lesson.subGroups[0].teacher,
lesson.subGroups[0].cabinet
)
)
} else {
for (subGroup in lesson.subGroups) {
kvText(
stringResource(R.string.extra_info_teacher),
stringResource(
R.string.extra_info_teacher_second_subgroup,
subGroup.teacher,
subGroup.cabinet,
subGroup.number
)
)
}
}
kvText(
stringResource(R.string.extra_info_duration),
stringResource(
R.string.extra_info_duration_second,
lesson.time.start.dayMinutes.fmtAsClock(),
lesson.time.end.dayMinutes.fmtAsClock(),
lesson.duration / 60,
lesson.duration % 60
)
)
}
}
}
}

View File

@@ -1,34 +1,47 @@
package ru.n08i40k.polytechnic.next.ui.widgets.schedule package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.text.BasicText import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.CardColors import androidx.compose.material3.Icon
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonType import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
import ru.n08i40k.polytechnic.next.utils.dayMinutes import ru.n08i40k.polytechnic.next.utils.dayMinutes
import ru.n08i40k.polytechnic.next.utils.fmtAsClock import ru.n08i40k.polytechnic.next.utils.fmtAsClock
@@ -42,31 +55,51 @@ private fun fmtTime(start: Int, end: Int, format: TimeFormat): ArrayList<String>
return when (format) { return when (format) {
TimeFormat.CLOCK -> arrayListOf(start.fmtAsClock(), end.fmtAsClock()) TimeFormat.CLOCK -> arrayListOf(start.fmtAsClock(), end.fmtAsClock())
TimeFormat.DURATION -> arrayListOf( TimeFormat.DURATION -> arrayListOf(
"${end - start} ${stringResource(R.string.minutes)}" "${end - start} ${stringResource(R.string.minutes_full)}"
) )
} }
} }
@Preview(showBackground = true, showSystemUi = true) @PreviewLightDark
@Composable
private fun LessonRowPreview() {
AppTheme {
Surface(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))
) {
LessonRow(Modifier, MockScheduleRepository.exampleGroup.days[0].lessons[6], true)
}
}
}
@Composable @Composable
fun LessonRow( fun LessonRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lesson: Lesson = MockScheduleRepository.exampleGroup.days[0].lessons[0], lesson: Lesson,
colors: CardColors = CardDefaults.cardColors() current: Boolean
) { ) {
val verticalPadding = when (lesson.type) { var time = fmtTime(
LessonType.BREAK -> 2.5.dp lesson.time.start.dayMinutes,
else -> 5.dp lesson.time.end.dayMinutes,
if (lesson.type == LessonType.BREAK) TimeFormat.DURATION else TimeFormat.CLOCK
)
if (lesson.type == LessonType.BREAK) {
Box(Modifier.fillMaxWidth(), Alignment.Center) {
HorizontalDivider(color = MaterialTheme.colorScheme.surfaceContainerHighest)
Text(
time[0],
Modifier
.background(MaterialTheme.colorScheme.surfaceContainerLowest)
.padding(5.dp, 0.dp),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.surfaceContainerHighest,
)
} }
val timeFormat = when (lesson.type) { return
LessonType.BREAK -> TimeFormat.DURATION
else -> TimeFormat.CLOCK
}
val contentColor = when (lesson.type) {
LessonType.BREAK -> colors.disabledContentColor
else -> colors.contentColor
} }
// магические вычисления)) // магические вычисления))
@@ -78,26 +111,10 @@ fun LessonRow(
Box(modifier) { Box(modifier) {
Row( Row(
Modifier.padding(10.dp, verticalPadding * rangeSize), Modifier.padding(10.dp, 5.dp * rangeSize),
verticalAlignment = Alignment.CenterVertically, Arrangement.spacedBy(15.dp),
Alignment.CenterVertically,
) { ) {
Text(
when (range) {
null -> " "
else -> {
if (range[0] == range[1])
" ${range[0]} "
else
"${range[0]}-${range[1]}"
}
},
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = contentColor
)
Spacer(Modifier.width(5.dp))
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
val timeWidth = textMeasurer.measure( val timeWidth = textMeasurer.measure(
text = "00:00", text = "00:00",
@@ -105,29 +122,62 @@ fun LessonRow(
) )
Column( Column(
Modifier.width(with(LocalDensity.current) { timeWidth.size.width.toDp() }), Modifier
horizontalAlignment = Alignment.CenterHorizontally .width(with(LocalDensity.current) { timeWidth.size.width.toDp() + 1.dp }),
Arrangement.spacedBy(5.dp),
Alignment.CenterHorizontally
) { ) {
var time = fmtTime( Column {
lesson.time.start.dayMinutes, Text(
lesson.time.end.dayMinutes, time[0],
timeFormat fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.W600,
style = MaterialTheme.typography.titleSmall
) )
Text(time[0], color = contentColor, fontFamily = FontFamily.Monospace, maxLines = 1)
if (lesson.type != LessonType.BREAK)
Text( Text(
time[1], time[1],
color = contentColor,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
maxLines = 1 fontWeight = FontWeight.W600,
style = MaterialTheme.typography.titleSmall
) )
} }
Spacer(Modifier.width(5.dp)) if (range != null) {
HorizontalDivider(
Modifier.width(32.dp),
1.dp,
MaterialTheme.colorScheme.inverseSurface
)
Text(
if (range[0] == range[1])
" ${range[0]} "
else
"${range[0]}-${range[1]}",
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodySmall
)
}
}
VerticalDivider(
Modifier.height(42.dp),
1.dp,
if (current)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.inverseSurface
)
Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(
lesson.name!!,
fontWeight = FontWeight.W600,
style = MaterialTheme.typography.titleMedium
)
// FIXME: Очень странный метод отсеивания, может что-нибудь на замену сделать? // FIXME: Очень странный метод отсеивания, может что-нибудь на замену сделать?
if (lesson.type.value > LessonType.BREAK.value) { if (lesson.type.value > LessonType.BREAK.value) {
Text( Text(
@@ -139,59 +189,106 @@ fun LessonRow(
LessonType.EXAM_DEFAULT -> stringResource(R.string.lesson_type_exam_default) LessonType.EXAM_DEFAULT -> stringResource(R.string.lesson_type_exam_default)
else -> throw RuntimeException("Unknown lesson type!") else -> throw RuntimeException("Unknown lesson type!")
}, },
fontWeight = FontWeight.Bold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = contentColor fontWeight = FontWeight.W600,
style = MaterialTheme.typography.titleMedium
) )
} }
Text( @Composable
lesson.name ?: stringResource(R.string.lesson_type_break), fun SmallTextWithIcon(
fontWeight = FontWeight.Medium, @DrawableRes iconId: Int,
maxLines = 1, contentDescription: String,
overflow = TextOverflow.Ellipsis, text: String
color = contentColor ) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(iconId),
contentDescription,
Modifier.size(12.dp)
) )
Text(
text,
fontWeight = FontWeight.W400,
style = MaterialTheme.typography.titleSmall
)
}
}
if (lesson.group != null) { if (lesson.group != null) {
Text( Row {
lesson.group, Spacer(Modifier.size(5.dp))
color = contentColor,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
for (subGroup in lesson.subGroups) { SmallTextWithIcon(
Text( R.drawable.ic_group,
subGroup.teacher, "Group",
color = contentColor, lesson.group
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
} }
Column(Modifier.wrapContentWidth()) { Row(
if (lesson.subGroups.size != 1) { Modifier
BasicText("") .fillMaxWidth()
.padding(5.dp),
if (lesson.group != null) Arrangement.SpaceBetween,
BasicText("") Alignment.CenterVertically
) {
lesson
.subGroups
.sortedBy { it.number }
.forEachIndexed { subGroupIdx, subGroup ->
if (subGroupIdx > 0) {
VerticalDivider(
Modifier.height(25.dp), 1.dp,
MaterialTheme.colorScheme.inverseSurface
)
} }
for (subGroup in lesson.subGroups) { // FIXME: тупая проверка
if (subGroup.teacher == "Только у другой") {
Text( Text(
subGroup.cabinet, stringResource(
color = contentColor, if (subGroup.number == 1)
maxLines = 1, R.string.only_for_second
fontFamily = FontFamily.Monospace else
R.string.only_for_first
),
fontWeight = FontWeight.W400,
style = MaterialTheme.typography.titleSmall
)
return@forEachIndexed
}
Column {
val cabinet =
if (subGroup.cabinet.toIntOrNull() == null)
subGroup.cabinet
else
"${subGroup.cabinet}"
SmallTextWithIcon(
R.drawable.ic_cabinet,
"Cabinet",
cabinet
)
SmallTextWithIcon(
R.drawable.ic_teacher,
"Teacher",
subGroup.teacher
) )
} }
} }
} }
} }
}
} }
} }

View File

@@ -1,26 +1,57 @@
package ru.n08i40k.polytechnic.next.ui.widgets.schedule package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.compose.animation.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp import androidx.compose.ui.util.lerp
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard
import ru.n08i40k.polytechnic.next.utils.dateTime import ru.n08i40k.polytechnic.next.utils.dateTime
import ru.n08i40k.polytechnic.next.utils.now import ru.n08i40k.polytechnic.next.utils.now
import java.lang.ref.WeakReference
import java.util.logging.Level import java.util.logging.Level
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -32,24 +63,120 @@ private fun isScheduleOutdated(schedule: GroupOrTeacher): Boolean {
return nowDateTime > lastLesson.time.end.dateTime return nowDateTime > lastLesson.time.end.dateTime
} }
@Preview(showSystemUi = true) @PreviewLightDark()
@Composable @Composable
fun SchedulePager(schedule: GroupOrTeacher = MockScheduleRepository.exampleTeacher) { private fun SchedulePagerPreview() {
AppTheme {
Surface(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))
) {
SchedulePager(MockScheduleRepository.exampleTeacher)
}
}
}
val weekList = listOf(
R.string.week_bar_monday,
R.string.week_bar_tuesday,
R.string.week_bar_wednesday,
R.string.week_bar_thursday,
R.string.week_bar_friday,
R.string.week_bar_saturday,
)
@Composable
fun SchedulePager(schedule: GroupOrTeacher) {
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = (schedule.currentIdx ?: (schedule.days.size - 1)).coerceAtLeast(0), initialPage = (schedule.currentIdx ?: (schedule.days.size - 1)).coerceAtLeast(0),
pageCount = { schedule.days.size } pageCount = { schedule.days.size }
) )
Column { var dialogLesson by remember { mutableStateOf<WeakReference<Lesson>?>(null) }
Column(
Modifier
.fillMaxWidth(),
Arrangement.spacedBy(20.dp)
) {
if (isScheduleOutdated(schedule)) if (isScheduleOutdated(schedule))
NotificationCard(Level.WARNING, stringResource(R.string.outdated_schedule)) NotificationCard(Level.WARNING, stringResource(R.string.outdated_schedule))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest
)
) {
Row(Modifier.fillMaxWidth(), Arrangement.spacedBy(5.dp)) {
val coroutineScope = rememberCoroutineScope()
for (i in 0..5) {
val primaryContainerColor = MaterialTheme.colorScheme.primaryContainer
val onPrimaryContainerColor = MaterialTheme.colorScheme.onPrimaryContainer
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
val containerColor = remember {
Animatable(
if (pagerState.currentPage == i) primaryContainerColor
else Color.Transparent
)
}
val contentColor = remember {
Animatable(
if (pagerState.currentPage == i) onPrimaryContainerColor
else onSurfaceColor
)
}
LaunchedEffect(pagerState, pagerState.currentPage) {
containerColor.animateTo(
if (pagerState.currentPage == i) primaryContainerColor
else Color.Transparent
)
contentColor.animateTo(
if (pagerState.currentPage == i) onPrimaryContainerColor
else onSurfaceColor
)
}
Column(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(20))
.background(containerColor.value)
.clickable { coroutineScope.launch { pagerState.animateScrollToPage(i) } },
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
Modifier
.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
stringResource(weekList[i]),
style = MaterialTheme.typography.bodyMedium,
color = contentColor.value
)
Text(
schedule.days[i].date.dateTime.date.dayOfMonth.toString(),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.W600,
color = contentColor.value
)
}
}
}
}
}
HorizontalPager( HorizontalPager(
pagerState, pagerState,
Modifier Modifier
.height(600.dp) .height(600.dp),
.padding(top = 5.dp),
PaddingValues(horizontal = 7.dp),
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { page -> ) { page ->
DayCard( DayCard(
@@ -59,7 +186,7 @@ fun SchedulePager(schedule: GroupOrTeacher = MockScheduleRepository.exampleTeach
).absoluteValue ).absoluteValue
lerp( lerp(
start = 1f, stop = 0.95f, fraction = 1f - offset.coerceIn(0f, 1f) start = 1f, stop = 0.95f, fraction = offset.coerceIn(0f, 1f)
).also { scale -> ).also { scale ->
scaleX = scale scaleX = scale
scaleY = scale scaleY = scale
@@ -69,7 +196,14 @@ fun SchedulePager(schedule: GroupOrTeacher = MockScheduleRepository.exampleTeach
) )
}, },
schedule.days[page] schedule.days[page]
) ) { dialogLesson = WeakReference(it) }
} }
} }
dialogLesson?.get()?.let { lesson ->
if (lesson.type == LessonType.BREAK)
return@let
ExtraInfoDialog(lesson) { dialogLesson = null }
}
} }

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.screen.schedule package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -19,31 +21,31 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ru.n08i40k.polytechnic.next.CacheDate
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.UpdateDates import ru.n08i40k.polytechnic.next.ui.screen.schedule.PaskhalkoDialog
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCard import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCard
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCardTitle import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCardTitle
import ru.n08i40k.polytechnic.next.utils.* import ru.n08i40k.polytechnic.next.utils.*
import java.util.Date import java.util.Date
val expanded = mutableStateOf(false)
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun UpdateInfo( fun UpdateInfo(
lastUpdateAt: Long = 0, lastUpdateAt: Long = 0,
updateDates: UpdateDates = UpdateDates.newBuilder().build() cacheDate: CacheDate = CacheDate.newBuilder().build()
) { ) {
var expanded by remember { expanded } var expanded by remember { mutableStateOf(false) }
val format = "HH:mm:ss dd.MM.yyyy" val format = "HH:mm:ss dd.MM.yyyy"
val currentDate = Date(lastUpdateAt).toString(format) val currentDate = Date(lastUpdateAt).toString(format)
val cacheUpdateDate = Date(updateDates.cache).toString(format) val cacheUpdateDate = Date(cacheDate.cache).toString(format)
val scheduleUpdateDate = Date(updateDates.schedule).toString(format) val scheduleUpdateDate = Date(cacheDate.schedule).toString(format)
ExpandableCard( ExpandableCard(
expanded = expanded, expanded = expanded,
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLowest),
onExpandedChange = { expanded = !expanded }, onExpandedChange = { expanded = !expanded },
title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) } title = { ExpandableCardTitle(stringResource(R.string.update_info_header)) }
) { ) {

View File

@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.app.appContainer import ru.n08i40k.polytechnic.next.app.appContainer
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import ru.n08i40k.polytechnic.next.utils.MyResult import ru.n08i40k.polytechnic.next.utils.MyResult
import java.time.Duration import java.time.Duration
import java.util.logging.Logger import java.util.logging.Logger

View File

@@ -8,7 +8,7 @@ import androidx.work.WorkerParameters
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import ru.n08i40k.polytechnic.next.app.appContainer import ru.n08i40k.polytechnic.next.app.appContainer
import ru.n08i40k.polytechnic.next.settings.settings import ru.n08i40k.polytechnic.next.proto.settings
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.logging.Logger import java.util.logging.Logger

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
option java_package = "ru.n08i40k.polytechnic.next";
option java_multiple_files = true;
message CacheResponse {
string hash = 1;
string data = 2;
}
message CacheDate {
int64 cache = 1;
int64 schedule = 2;
}
message Cache {
map<string, CacheResponse> storage = 4;
CacheDate date = 5;
}

View File

@@ -0,0 +1,15 @@
syntax = "proto3";
option java_package = "ru.n08i40k.polytechnic.next";
option java_multiple_files = true;
message SettingsV2 {
string user_id = 1;
string access_token = 2;
string group = 3;
string version = 5;
string suppressed_version = 6;
string fcm_token = 7;
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M120,840v-80h80v-640h400v40h160v600h80v80L680,840v-600h-80v600L120,840ZM280,200v560,-560ZM440,520q17,0 28.5,-11.5T480,480q0,-17 -11.5,-28.5T440,440q-17,0 -28.5,11.5T400,480q0,17 11.5,28.5T440,520ZM280,760h240v-560L280,200v560Z"
android:fillColor="#e8eaed"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M96,768v-92q0,-25.78 12.5,-47.39T143,594q54,-32 114.5,-49T384,528q66,0 126.5,17T625,594q22,13 34.5,34.61T672,676v92L96,768ZM744,768v-92q0,-42 -19.5,-78T672,539q39,8 75.5,21.5T817,594q22,13 34.5,34.67Q864,650.35 864,676v92L744,768ZM384,480q-60,0 -102,-42t-42,-102q0,-60 42,-102t102,-42q60,0 102,42t42,102q0,60 -42,102t-102,42ZM720,336q0,60 -42,102t-102,42q-8,0 -15,-0.5t-15,-2.5q25,-29 39.5,-64.5T600,336q0,-41 -14.5,-76.5T546,195q8,-2 15,-2.5t15,-0.5q60,0 102,42t42,102ZM168,696h432v-20q0,-6.47 -3.03,-11.76 -3.02,-5.3 -7.97,-8.24 -47,-27 -99,-41.5T384,600q-54,0 -106,14t-99,42q-4.95,2.83 -7.98,7.91 -3.02,5.09 -3.02,12L168,696ZM384.21,408Q414,408 435,386.79t21,-51Q456,306 434.79,285t-51,-21Q354,264 333,285.21t-21,51Q312,366 333.21,387t51,21ZM384,696ZM384,336Z"
android:fillColor="#e8eaed"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,840 L200,688v-240L40,360l440,-240 440,240v320h-80v-276l-80,44v240L480,840ZM480,508 L754,360 480,212 206,360 480,508ZM480,749 L680,641v-151L480,600 280,490v151l200,108ZM480,508ZM480,598ZM480,598Z"
android:fillColor="#e8eaed"/>
</vector>

View File

@@ -67,7 +67,7 @@
<string name="auth_error_disallowed_role">Пожалуйста, используйте другую роль.</string> <string name="auth_error_disallowed_role">Пожалуйста, используйте другую роль.</string>
<string name="sign_in_manual">По имени пользователя</string> <string name="sign_in_manual">По имени пользователя</string>
<string name="updater_support_end">Поддержка версий ниже %1$s прекращена!</string> <string name="updater_support_end">Поддержка версий ниже %1$s прекращена!</string>
<string name="last_server_schedule_update">Последнее обновление расписания</string> <string name="last_server_schedule_update">Расписание</string>
<string name="teacher_name">ФИО преподавателя</string> <string name="teacher_name">ФИО преподавателя</string>
<string name="updater_body">Желаете ли вы обновиться до последней версии?</string> <string name="updater_body">Желаете ли вы обновиться до последней версии?</string>
<string name="updater_new_version">Вышла новая версия приложения!</string> <string name="updater_new_version">Вышла новая версия приложения!</string>
@@ -76,8 +76,8 @@
<string name="updater_update">ОБНОВИТЬ</string> <string name="updater_update">ОБНОВИТЬ</string>
<string name="updater_suppress">ЗАГЛУШИТЬ</string> <string name="updater_suppress">ЗАГЛУШИТЬ</string>
<string name="update_info_header">Дополнительная информация</string> <string name="update_info_header">Дополнительная информация</string>
<string name="last_local_update">Последнее локальное обновление</string> <string name="last_local_update">Локально</string>
<string name="last_server_cache_update">Последнее обновление кеша</string> <string name="last_server_cache_update">Кеш</string>
<string name="download_update">Скачать обновление</string> <string name="download_update">Скачать обновление</string>
<string name="telegram_channel">Телеграм канал</string> <string name="telegram_channel">Телеграм канал</string>
<string name="schedule_update_title">Расписание обновлено!</string> <string name="schedule_update_title">Расписание обновлено!</string>
@@ -100,4 +100,20 @@
<string name="day_view_end_description">Ура, можно идти домой! Наверное :(</string> <string name="day_view_end_description">Ура, можно идти домой! Наверное :(</string>
<string name="day_view_channel_name">Текущая пара</string> <string name="day_view_channel_name">Текущая пара</string>
<string name="day_view_channel_description">Отображает текущую пару или перемену в уведомлении</string> <string name="day_view_channel_description">Отображает текущую пару или перемену в уведомлении</string>
<string name="extra_info_lesson_name">"Название: "</string>
<string name="extra_info_teacher">"Преподаватель: "</string>
<string name="extra_info_teacher_second">%1$s в %2$s каб.</string>
<string name="extra_info_teacher_second_subgroup">%1$s в %2$s каб. [подгруппа - %3$d]</string>
<string name="extra_info_duration">"Длительность: "</string>
<string name="extra_info_duration_second">С %1$s до %2$s (%3$d ч. %4$d мин.)</string>
<string name="extra_info_type">"Тип: "</string>
<string name="minutes_full">минут</string>
<string name="only_for_second">Только у второй</string>
<string name="only_for_first">Только у первой</string>
<string name="week_bar_monday">Пн</string>
<string name="week_bar_tuesday">Вт</string>
<string name="week_bar_wednesday">Ср</string>
<string name="week_bar_thursday">Чт</string>
<string name="week_bar_friday">Пт</string>
<string name="week_bar_saturday">Сб</string>
</resources> </resources>

View File

@@ -100,4 +100,20 @@
<string name="day_view_end_description">ya ne budu eto perevidit\'</string> <string name="day_view_end_description">ya ne budu eto perevidit\'</string>
<string name="day_view_channel_name">Current lesson</string> <string name="day_view_channel_name">Current lesson</string>
<string name="day_view_channel_description">View the current lesson and breaks in notification</string> <string name="day_view_channel_description">View the current lesson and breaks in notification</string>
<string name="extra_info_lesson_name">"Name: "</string>
<string name="extra_info_teacher">"Teacher: "</string>
<string name="extra_info_teacher_second">%1$s in %2$s cab.</string>
<string name="extra_info_teacher_second_subgroup">%1$s in %2$s cab. [subgroup - %3$d]</string>
<string name="extra_info_duration">"Duration: "</string>
<string name="extra_info_duration_second">From %1$s to %2$s (%3$d h. %4$d min.)</string>
<string name="extra_info_type">"Type: "</string>
<string name="minutes_full">minutes</string>
<string name="only_for_second">Only for second</string>
<string name="only_for_first">Only for first</string>
<string name="week_bar_tuesday">Tue</string>
<string name="week_bar_wednesday">Wed</string>
<string name="week_bar_thursday">Thu</string>
<string name="week_bar_friday">Fri</string>
<string name="week_bar_saturday">Sat</string>
<string name="week_bar_monday">Mon</string>
</resources> </resources>

View File

@@ -4,7 +4,7 @@ plugins {
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.1.10-1.0.30" apply false id("com.google.devtools.ksp") version "2.1.20-1.0.31" apply false
id("com.google.protobuf") version "0.9.4" apply false id("com.google.protobuf") version "0.9.4" apply false

View File

@@ -1,30 +1,33 @@
[versions] [versions]
agp = "8.8.1" agp = "8.8.2"
desugar_jdk_libs = "2.1.4" desugar_jdk_libs = "2.1.5"
kotlin = "2.1.10" kotlin = "2.1.10"
coreKtx = "1.15.0" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7" lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.0" activityCompose = "1.10.1"
composeBom = "2025.02.00" composeBom = "2025.03.00"
accompanistSwiperefresh = "0.36.0" accompanistSwiperefresh = "0.36.0"
firebaseBom = "33.9.0" firebaseBom = "33.10.0"
hiltAndroid = "2.55" hiltAndroid = "2.57.2"
hiltAndroidCompiler = "2.55" hiltAndroidCompiler = "2.56"
hiltNavigationCompose = "1.2.0" hiltNavigationCompose = "1.2.0"
kotlinxSerializationJson = "1.8.0" kotlinxSerializationJson = "1.8.0"
protobufLite = "3.0.1" protobufLite = "3.0.1"
runtimeTracing = "1.7.8"
volley = "1.2.1" volley = "1.2.1"
datastore = "1.1.2" datastore = "1.1.3"
navigationCompose = "2.8.9" navigationCompose = "2.8.9"
googleFirebaseCrashlytics = "3.0.3" googleFirebaseCrashlytics = "3.0.3"
workRuntime = "2.10.0" workRuntime = "2.10.0"
vkid = "2.3.1" #noinspection GradleDependency
vkid = "2.2.2"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "runtimeTracing" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@@ -59,9 +62,9 @@ volley = { group = "com.android.volley", name = "volley", version.ref = "volley"
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics", version = "22.2.0" } firebase-analytics = { module = "com.google.firebase:firebase-analytics", version = "22.3.0" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version = "19.4.0" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version = "19.4.1" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version = "24.1.0" } firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version = "24.1.1" }
firebase-config = { group = "com.google.firebase", name = "firebase-config", version = "22.1.0" } firebase-config = { group = "com.google.firebase", name = "firebase-config", version = "22.1.0" }
vk-vkid = {group = "com.vk.id", name = "vkid", version.ref = "vkid" } vk-vkid = {group = "com.vk.id", name = "vkid", version.ref = "vkid" }
vk-onetap-compose = {group = "com.vk.id", name = "onetap-compose", version.ref = "vkid" } vk-onetap-compose = {group = "com.vk.id", name = "onetap-compose", version.ref = "vkid" }