Фикс краша при получении расписания с включённым VPN.

Допилено обновление расписания без перезагрузки всего экрана.
This commit is contained in:
2024-10-02 00:22:51 +04:00
parent f2724d275b
commit 4db3a7d1c2
20 changed files with 312 additions and 147 deletions

View File

@@ -20,9 +20,9 @@
<option name="versions"> <option name="versions">
<list> <list>
<VersionSetting> <VersionSetting>
<option name="buildVersion" value="3" /> <option name="buildVersion" value="7" />
<option name="displayName" value="1.2 (3)" /> <option name="displayName" value="1.3.1 (7)" />
<option name="displayVersion" value="1.2" /> <option name="displayVersion" value="1.3.1" />
</VersionSetting> </VersionSetting>
</list> </list>
</option> </option>

124
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

2
.idea/misc.xml generated
View File

@@ -7,7 +7,7 @@
</list> </list>
</component> </component>
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@@ -32,8 +32,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next" applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 7 versionCode = 8
versionName = "1.3.1" versionName = "1.3.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -1,7 +1,5 @@
package ru.n08i40k.polytechnic.next.data package ru.n08i40k.polytechnic.next.data
import java.lang.Exception
sealed interface MyResult<out R> { sealed interface MyResult<out R> {
data class Success<out T>(val data: T) : MyResult<T> data class Success<out T>(val data: T) : MyResult<T>
data class Failure(val exception: Exception) : MyResult<Nothing> data class Failure(val exception: Exception) : MyResult<Nothing>

View File

@@ -32,12 +32,12 @@ class LocalNetworkCacheRepository
} }
override suspend fun get(url: String): CachedResponse? { override suspend fun get(url: String): CachedResponse? {
if (this.hash == null) // Если кешированого ответа нет, то возвращаем null
return null // Если хеши не совпадают и локальный хеш присутствует, то возвращаем null
val response = cacheMap[url] val response = cacheMap[url] ?: return null
if (response?.hash != this.hash) if (response.hash != this.hash && this.hash != null)
return null return null
return response return response

View File

@@ -1,7 +1,7 @@
package ru.n08i40k.polytechnic.next.data.schedule package ru.n08i40k.polytechnic.next.data.schedule
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group
interface ScheduleRepository { interface ScheduleRepository {
suspend fun getGroup(): MyResult<Group> suspend fun getGroup(): MyResult<Group>

View File

@@ -4,13 +4,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Lesson import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.model.LessonType import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.data.MyResult
class FakeScheduleRepository : ScheduleRepository { class FakeScheduleRepository : ScheduleRepository {
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")

View File

@@ -1,7 +1,6 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl package ru.n08i40k.polytechnic.next.data.schedule.impl
import android.content.Context import android.content.Context
import com.android.volley.toolbox.RequestFuture
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -12,7 +11,7 @@ import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequest import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetResponse import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.settings.settingsDataStore
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository { class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
@@ -27,18 +26,18 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
if (groupName.isEmpty()) if (groupName.isEmpty())
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!")) return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
val future = RequestFuture.newFuture<ScheduleGetResponse>() val response = tryFuture {
ScheduleGetRequest( ScheduleGetRequest(
ScheduleGetRequestData(groupName), ScheduleGetRequestData(groupName),
context, context,
future, it,
future it
).send() )
}
try { when (response) {
MyResult.Success(future.get().group) is MyResult.Failure -> response
} catch (exception: Exception) { is MyResult.Success -> MyResult.Success(response.data.group)
MyResult.Failure(exception)
} }
} }
} }

View File

@@ -7,7 +7,6 @@ import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.model.UserRole
import java.lang.Exception
class FakeProfileRepository : ProfileRepository { class FakeProfileRepository : ProfileRepository {
private var counter = 0 private var counter = 0

View File

@@ -1,28 +1,23 @@
package ru.n08i40k.polytechnic.next.data.users.impl package ru.n08i40k.polytechnic.next.data.users.impl
import android.content.Context import android.content.Context
import com.android.volley.toolbox.RequestFuture
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
import ru.n08i40k.polytechnic.next.network.tryFuture
class RemoteProfileRepository(private val context: Context) : ProfileRepository { class RemoteProfileRepository(private val context: Context) : ProfileRepository {
override suspend fun getProfile(): MyResult<Profile> { override suspend fun getProfile(): MyResult<Profile> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val responseFuture = RequestFuture.newFuture<Profile>() tryFuture {
UsersMeRequest( UsersMeRequest(
context, context,
responseFuture, it,
responseFuture it
).send() )
try {
MyResult.Success(responseFuture.get())
} catch (exception: Exception) {
MyResult.Failure(exception)
} }
} }
} }

View File

@@ -5,10 +5,15 @@ import android.content.Context
import com.android.volley.Request import com.android.volley.Request
import com.android.volley.RequestQueue import com.android.volley.RequestQueue
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.HurlStack import com.android.volley.toolbox.HurlStack
import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
import ru.n08i40k.polytechnic.next.data.MyResult
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeoutException
import java.util.logging.Logger import java.util.logging.Logger
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory import javax.net.ssl.SSLSocketFactory
@@ -82,3 +87,23 @@ open class RequestBase(
return headers return headers
} }
} }
fun <ResultT, RequestT : RequestBase> tryFuture(
buildRequest: (RequestFuture<ResultT>) -> RequestT
): MyResult<ResultT> {
val future = RequestFuture.newFuture<ResultT>()
buildRequest(future).send()
return tryGet(future)
}
fun <T> tryGet(future: RequestFuture<T>): MyResult<T> {
return try {
MyResult.Success(future.get())
} catch (exception: VolleyError) {
MyResult.Failure(exception)
} catch (exception: ExecutionException) {
MyResult.Failure(exception.cause as VolleyError)
} catch (exception: TimeoutException) {
MyResult.Failure(exception)
}
}

View File

@@ -2,7 +2,6 @@ package ru.n08i40k.polytechnic.next.network.data
import android.content.Context import android.content.Context
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.RequestFuture import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.StringRequest
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -16,6 +15,8 @@ import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusR
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResponse import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResponse
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData
import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.network.tryGet
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@@ -47,61 +48,58 @@ open class CachedRequest(
) )
NetworkConnection.getInstance(context).addToRequestQueue(request) NetworkConnection.getInstance(context).addToRequestQueue(request)
try { when (val response = tryGet(mainPageFuture)) {
val encodedMainPage = is MyResult.Failure -> response
Base64.Default.encode(mainPageFuture.get().encodeToByteArray()) is MyResult.Success -> {
MyResult.Success(encodedMainPage) val encodedMainPage = Base64.Default.encode(response.data.encodeToByteArray())
} catch (exception: Exception) { MyResult.Success(encodedMainPage)
MyResult.Failure(exception) }
} }
} }
} }
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResponse> { private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResponse> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val mainPage = getMainPage() when (val mainPage = getMainPage()) {
is MyResult.Failure -> mainPage
if (mainPage is MyResult.Failure) is MyResult.Success -> {
return@withContext mainPage tryFuture {
ScheduleUpdateRequest(
val updateFuture = RequestFuture.newFuture<ScheduleGetCacheStatusResponse>() ScheduleUpdateRequestData(mainPage.data),
ScheduleUpdateRequest( context,
ScheduleUpdateRequestData((mainPage as MyResult.Success<String>).data), it,
context, it
updateFuture, )
updateFuture }
).send() }
try {
MyResult.Success(updateFuture.get())
} catch (exception: Exception) {
MyResult.Failure(exception)
} }
} }
} }
override fun send() { override fun send() {
val logger = Logger.getLogger("CachedRequest") val logger = Logger.getLogger("CachedRequest")
val repository = appContainer.networkCacheRepository val repository = appContainer.networkCacheRepository
val future = RequestFuture.newFuture<ScheduleGetCacheStatusResponse>()
logger.info("Getting cache status...") logger.info("Getting cache status...")
ScheduleGetCacheStatusRequest(context, future, future).send()
try { val cacheStatusResult = tryFuture {
val response = future.get() ScheduleGetCacheStatusRequest(context, it, it)
}
if (cacheStatusResult is MyResult.Success) {
val cacheStatus = cacheStatusResult.data
logger.info("Cache status received successfully!") logger.info("Cache status received successfully!")
if (!response.cacheUpdateRequired) { runBlocking {
logger.info("Cache update was not required!") repository.setUpdateDates(
runBlocking { cacheStatus.lastCacheUpdate,
repository.setUpdateDates(response.lastCacheUpdate, response.lastScheduleUpdate) cacheStatus.lastScheduleUpdate
repository.setHash(response.cacheHash) )
} repository.setHash(cacheStatus.cacheHash)
} else { }
if (cacheStatus.cacheUpdateRequired) {
logger.info("Cache update was required!") logger.info("Cache update was required!")
val updateResult = runBlocking { updateMainPage() } val updateResult = runBlocking { updateMainPage() }
@@ -119,16 +117,11 @@ open class CachedRequest(
is MyResult.Failure -> { is MyResult.Failure -> {
logger.warning("Failed to update cache!") logger.warning("Failed to update cache!")
super.getErrorListener()
?.onErrorResponse(updateResult.exception.cause as VolleyError)
return
} }
} }
} }
} catch (exception: Exception) { } else {
logger.warning("Failed to get cache status!") logger.warning("Failed to get cache status!")
super.getErrorListener()?.onErrorResponse(exception.cause as VolleyError)
return
} }
val cachedResponse = runBlocking { repository.get(url) } val cachedResponse = runBlocking { repository.get(url) }

View File

@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -27,6 +28,7 @@ import androidx.compose.runtime.remember
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.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.RectangleShape
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
@@ -46,10 +48,13 @@ fun ExpandableCard(
val transition = rememberTransition(transitionState) val transition = rememberTransition(transitionState)
Card(modifier = modifier.clickable { Card(
onExpandedChange() modifier = modifier.clickable {
transitionState.targetState = expanded onExpandedChange()
}) { transitionState.targetState = expanded
},
shape = RectangleShape
) {
Column { Column {
ExpandableCardHeader(title, transition) ExpandableCardHeader(title, transition)
ExpandableCardContent(visible = expanded, content = content) ExpandableCardContent(visible = expanded, content = content)
@@ -83,6 +88,7 @@ private fun ExpandableCardContent(
enter = enterTransition, enter = enterTransition,
exit = exitTransition exit = exitTransition
) { ) {
HorizontalDivider()
content() content()
} }
} }

View File

@@ -1,6 +1,7 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
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.runtime.Composable import androidx.compose.runtime.Composable
@@ -28,7 +29,8 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
contentPadding = PaddingValues(horizontal = 20.dp), contentPadding = PaddingValues(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.Top,
modifier = Modifier.height(600.dp)
) { page -> ) { page ->
DayCard( DayCard(
modifier = Modifier.graphicsLayer { modifier = Modifier.graphicsLayer {
@@ -37,8 +39,8 @@ fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
lerp( lerp(
start = 0.95f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f) start = 0.95f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
).also { scale -> ).also { scale ->
scaleX = scale scaleX = 1F - scale + 0.95F
scaleY = scale scaleY = 1F - scale + 0.95F
} }
alpha = lerp( alpha = lerp(
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f) start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)

View File

@@ -1,12 +1,8 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
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
@@ -16,10 +12,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.LoadingContent import ru.n08i40k.polytechnic.next.ui.LoadingContent
@@ -35,25 +28,20 @@ fun ScheduleScreen(
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle() val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
LoadingContent( LoadingContent(
empty = uiState.isLoading, empty = when (uiState) {
is ScheduleUiState.NoSchedule -> uiState.isLoading
is ScheduleUiState.HasSchedule -> false
},
loading = uiState.isLoading, loading = uiState.isLoading,
onRefresh = { onRefreshSchedule() }, onRefresh = { onRefreshSchedule() },
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { ) {
when (uiState) { when (uiState) {
is ScheduleUiState.HasSchedule -> { is ScheduleUiState.HasSchedule -> {
Box { Column {
val networkCacheRepository = val hasSchedule = uiState as ScheduleUiState.HasSchedule
hiltViewModel<MainViewModel>(LocalContext.current as ComponentActivity) UpdateInfo(hasSchedule.lastUpdateAt, hasSchedule.updateDates)
.appContainer DayPager(hasSchedule.group)
.networkCacheRepository
UpdateInfo(networkCacheRepository)
Column {
Spacer(modifier = Modifier.height(200.dp))
DayPager((uiState as ScheduleUiState.HasSchedule).group)
}
} }
} }

View File

@@ -1,36 +1,26 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule package ru.n08i40k.polytechnic.next.ui.main.schedule
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
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.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
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.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 androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.R import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository
import ru.n08i40k.polytechnic.next.ui.ExpandableCard import ru.n08i40k.polytechnic.next.ui.ExpandableCard
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -40,24 +30,21 @@ fun Date.toString(format: String, locale: Locale = Locale.getDefault()): String
return formatter.format(this) return formatter.format(this)
} }
fun getCurrentDateTime(): Date {
return Calendar.getInstance().time
}
val expanded = mutableStateOf(false) val expanded = mutableStateOf(false)
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun UpdateInfo(networkCacheRepository: NetworkCacheRepository = FakeNetworkCacheRepository()) { fun UpdateInfo(
lastUpdateAt: Long = 0,
updateDates: UpdateDates = UpdateDates.newBuilder().build()
) {
var expanded by remember { expanded } var expanded by remember { expanded }
val format = "hh:mm:ss dd.MM.yyyy" val format = "HH:mm:ss dd.MM.yyyy"
val updateDates = remember { runBlocking { networkCacheRepository.getUpdateDates() } } val currentDate = Date(lastUpdateAt).toString(format)
val cacheUpdateDate = Date(updateDates.cache).toString(format)
val currentDate = remember { getCurrentDateTime().toString(format) } val scheduleUpdateDate = Date(updateDates.schedule).toString(format)
val cacheUpdateDate = remember { Date(updateDates.cache).toString(format) }
val scheduleUpdateDate = remember { Date(updateDates.schedule).toString(format) }
ExpandableCard( ExpandableCard(
expanded = expanded, expanded = expanded,
@@ -69,19 +56,40 @@ fun UpdateInfo(networkCacheRepository: NetworkCacheRepository = FakeNetworkCache
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp) .padding(10.dp)
) { ) {
Row(horizontalArrangement = Arrangement.Center) { Row(
Text(text = stringResource(R.string.last_local_update) + " - ") horizontalArrangement = Arrangement.SpaceBetween,
Text(text = currentDate, fontWeight = FontWeight.Bold) modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.last_local_update))
Text(
text = currentDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
} }
Row(horizontalArrangement = Arrangement.Center) { Row(
Text(text = stringResource(R.string.last_server_cache_update) + " - ") horizontalArrangement = Arrangement.SpaceBetween,
Text(text = cacheUpdateDate, fontWeight = FontWeight.Bold) modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.last_server_cache_update))
Text(
text = cacheUpdateDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
} }
Row(horizontalArrangement = Arrangement.Center) { Row(
Text(text = stringResource(R.string.last_server_schedule_update) + " - ") horizontalArrangement = Arrangement.SpaceBetween,
Text(text = scheduleUpdateDate, fontWeight = FontWeight.Bold) modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.last_server_schedule_update))
Text(
text = scheduleUpdateDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
} }
} }

View File

@@ -9,9 +9,12 @@ 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.data.AppContainer import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.Group import ru.n08i40k.polytechnic.next.model.Group
import java.util.Date
import java.util.logging.Logger
import javax.inject.Inject import javax.inject.Inject
sealed interface ScheduleUiState { sealed interface ScheduleUiState {
@@ -23,18 +26,22 @@ sealed interface ScheduleUiState {
data class HasSchedule( data class HasSchedule(
val group: Group, val group: Group,
val updateDates: UpdateDates,
val lastUpdateAt: Long,
override val isLoading: Boolean override val isLoading: Boolean
) : ScheduleUiState ) : ScheduleUiState
} }
private data class ScheduleViewModelState( private data class ScheduleViewModelState(
val group: Group? = null, val group: Group? = null,
val updateDates: UpdateDates? = null,
val lastUpdateAt: Long = 0,
val isLoading: Boolean = false val isLoading: Boolean = false
) { ) {
fun toUiState(): ScheduleUiState = if (group == null) { fun toUiState(): ScheduleUiState = if (group == null) {
ScheduleUiState.NoSchedule(isLoading) ScheduleUiState.NoSchedule(isLoading)
} else { } else {
ScheduleUiState.HasSchedule(group, isLoading) ScheduleUiState.HasSchedule(group, updateDates!!, lastUpdateAt, isLoading)
} }
} }
@@ -43,6 +50,7 @@ class ScheduleViewModel @Inject constructor(
appContainer: AppContainer appContainer: AppContainer
) : ViewModel() { ) : ViewModel() {
private val scheduleRepository = appContainer.scheduleRepository private val scheduleRepository = appContainer.scheduleRepository
private val networkCacheRepository = appContainer.networkCacheRepository
private val viewModelState = MutableStateFlow(ScheduleViewModelState(isLoading = true)) private val viewModelState = MutableStateFlow(ScheduleViewModelState(isLoading = true))
val uiState = viewModelState val uiState = viewModelState
@@ -61,8 +69,23 @@ class ScheduleViewModel @Inject constructor(
viewModelState.update { viewModelState.update {
when (result) { when (result) {
is MyResult.Success -> it.copy(group = result.data, isLoading = false) is MyResult.Success -> {
is MyResult.Failure -> it.copy(group = null, isLoading = false) val updateDates = networkCacheRepository.getUpdateDates()
Logger.getLogger("ScheduleViewModel").info("Updating...")
it.copy(
group = result.data,
updateDates = updateDates,
lastUpdateAt = Date().time,
isLoading = false
)
}
is MyResult.Failure -> it.copy(
group = null,
isLoading = false
)
} }
} }
} }