diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
index cb7184b..bac18c6 100644
--- a/.idea/appInsightsSettings.xml
+++ b/.idea/appInsightsSettings.xml
@@ -17,6 +17,15 @@
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 9feba08..ef5c2a0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -32,8 +32,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
- versionCode = 6
- versionName = "1.3.0"
+ versionCode = 7
+ versionName = "1.3.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt
index 4d5d776..1762a40 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt
@@ -1,6 +1,7 @@
package ru.n08i40k.polytechnic.next.data.cache
import ru.n08i40k.polytechnic.next.CachedResponse
+import ru.n08i40k.polytechnic.next.UpdateDates
interface NetworkCacheRepository {
suspend fun put(url: String, data: String)
@@ -12,4 +13,8 @@ interface NetworkCacheRepository {
suspend fun isHashPresent(): Boolean
suspend fun setHash(hash: String)
+
+ suspend fun getUpdateDates(): UpdateDates
+
+ suspend fun setUpdateDates(cache: Long, schedule: Long)
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt
index f9ea4e7..ef8bbe7 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt
@@ -1,6 +1,7 @@
package ru.n08i40k.polytechnic.next.data.cache.impl
import ru.n08i40k.polytechnic.next.CachedResponse
+import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
class FakeNetworkCacheRepository : NetworkCacheRepository {
@@ -17,4 +18,10 @@ class FakeNetworkCacheRepository : NetworkCacheRepository {
}
override suspend fun setHash(hash: String) {}
+
+ override suspend fun getUpdateDates(): UpdateDates {
+ return UpdateDates.newBuilder().build()
+ }
+
+ override suspend fun setUpdateDates(cache: Long, schedule: Long) {}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt
index 7a3b6e8..fe914ab 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.CachedResponse
+import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import javax.inject.Inject
@@ -14,6 +15,7 @@ import javax.inject.Inject
class LocalNetworkCacheRepository
@Inject constructor(private val applicationContext: Context) : NetworkCacheRepository {
private val cacheMap: MutableMap = mutableMapOf()
+ private var updateDates: UpdateDates = UpdateDates.newBuilder().build()
private var hash: String? = null
init {
@@ -75,18 +77,38 @@ class LocalNetworkCacheRepository
this.cacheMap
.mapNotNull { if (it.value.hash != this.hash) it.key else null }
.forEach { this.cacheMap.remove(it) }
+ save()
}
}
+ override suspend fun getUpdateDates(): UpdateDates {
+ return this.updateDates
+ }
+
+ override suspend fun setUpdateDates(cache: Long, schedule: Long) {
+ updateDates = UpdateDates
+ .newBuilder()
+ .setCache(cache)
+ .setSchedule(schedule).build()
+
+ withContext(Dispatchers.IO) {
+ applicationContext.settingsDataStore.updateData {
+ it
+ .toBuilder()
+ .setUpdateDates(updateDates)
+ .build()
+ }
+ }
+ save()
+ }
+
private suspend fun save() {
withContext(Dispatchers.IO) {
- runBlocking {
- applicationContext.settingsDataStore.updateData {
- it
- .toBuilder()
- .putAllCacheStorage(cacheMap)
- .build()
- }
+ applicationContext.settingsDataStore.updateData {
+ it
+ .toBuilder()
+ .putAllCacheStorage(cacheMap)
+ .build()
}
}
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt
index 46db523..9d3d96c 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt
@@ -77,6 +77,7 @@ open class RequestBase(
override fun getHeaders(): MutableMap {
val headers = mutableMapOf()
headers["Content-Type"] = "application/json; charset=utf-8"
+ headers["version"] = "1"
return headers
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt
index 403bc12..a383eff 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt
@@ -97,7 +97,10 @@ open class CachedRequest(
if (!response.cacheUpdateRequired) {
logger.info("Cache update was not required!")
- runBlocking { repository.setHash(response.cacheHash) }
+ runBlocking {
+ repository.setUpdateDates(response.lastCacheUpdate, response.lastScheduleUpdate)
+ repository.setHash(response.cacheHash)
+ }
} else {
logger.info("Cache update was required!")
val updateResult = runBlocking { updateMainPage() }
@@ -105,7 +108,13 @@ open class CachedRequest(
when (updateResult) {
is MyResult.Success -> {
logger.info("Cache update was successful!")
- runBlocking { repository.setHash(updateResult.data.cacheHash) }
+ runBlocking {
+ repository.setUpdateDates(
+ updateResult.data.lastCacheUpdate,
+ updateResult.data.lastScheduleUpdate
+ )
+ repository.setHash(updateResult.data.cacheHash)
+ }
}
is MyResult.Failure -> {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt
index d3abe18..6d8de40 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt
@@ -6,4 +6,6 @@ import kotlinx.serialization.Serializable
data class ScheduleGetCacheStatusResponse(
val cacheUpdateRequired: Boolean,
val cacheHash: String,
+ val lastCacheUpdate: Long,
+ val lastScheduleUpdate: Long,
)
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/ExpandableCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/ExpandableCard.kt
new file mode 100644
index 0000000..3e97948
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/ExpandableCard.kt
@@ -0,0 +1,134 @@
+package ru.n08i40k.polytechnic.next.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.rememberTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ExpandableCard(
+ modifier: Modifier = Modifier,
+ expanded: Boolean,
+ onExpandedChange: () -> Unit,
+ title: String,
+ content: @Composable () -> Unit
+) {
+ val transitionState = remember {
+ MutableTransitionState(expanded).apply {
+ targetState = !expanded
+ }
+ }
+
+ val transition = rememberTransition(transitionState)
+
+ Card(modifier = modifier.clickable {
+ onExpandedChange()
+ transitionState.targetState = expanded
+ }) {
+ Column {
+ ExpandableCardHeader(title, transition)
+ ExpandableCardContent(visible = expanded, content = content)
+ }
+ }
+}
+
+@Composable
+private fun ExpandableCardContent(
+ visible: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val enterTransition = remember {
+ expandVertically(
+ expandFrom = Alignment.Top,
+ animationSpec = tween(durationMillis = 250)
+ ) + fadeIn(animationSpec = tween(durationMillis = 250, delayMillis = 250))
+ }
+
+ val exitTransition = remember {
+ fadeOut(
+ animationSpec = tween(durationMillis = 250)
+ ) + shrinkVertically(
+ shrinkTowards = Alignment.Top,
+ animationSpec = tween(durationMillis = 250, delayMillis = 250)
+ )
+ }
+
+ AnimatedVisibility(
+ visible = visible,
+ enter = enterTransition,
+ exit = exitTransition
+ ) {
+ content()
+ }
+}
+
+@Composable
+private fun ExpandableCardTitle(text: String) {
+ Text(
+ text = text,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(0.dp, 10.dp),
+ style = MaterialTheme.typography.titleMedium,
+ textAlign = TextAlign.Center
+ )
+}
+
+@Composable
+private fun ExpandableCardArrow(
+ transition: Transition
+) {
+ val rotationDegree by transition.animateFloat(
+ { tween(durationMillis = 250) },
+ label = "Arrow Rotation"
+ ) {
+ if (it) 360F else 180F
+ }
+
+ Icon(
+ modifier = Modifier.rotate(rotationDegree),
+ imageVector = Icons.Filled.ArrowDropDown,
+ contentDescription = "Expandable Arrow"
+ )
+}
+
+@Composable
+private fun ExpandableCardHeader(
+ title: String = "TODO",
+ transition: Transition
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp, 0.dp),
+ contentAlignment = Alignment.CenterEnd,
+ ) {
+ ExpandableCardArrow(transition)
+ ExpandableCardTitle(title)
+ }
+}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt
index 2b79f7c..580bc15 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/ScheduleScreen.kt
@@ -1,6 +1,12 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -10,7 +16,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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 ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.LoadingContent
@@ -26,16 +35,26 @@ fun ScheduleScreen(
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
LoadingContent(
- empty = when (uiState) {
- is ScheduleUiState.NoSchedule -> uiState.isLoading
- is ScheduleUiState.HasSchedule -> false
- },
+ empty = uiState.isLoading,
loading = uiState.isLoading,
- onRefresh = onRefreshSchedule
+ onRefresh = { onRefreshSchedule() },
+ verticalArrangement = Arrangement.Top
) {
when (uiState) {
is ScheduleUiState.HasSchedule -> {
- DayPager((uiState as ScheduleUiState.HasSchedule).group)
+ Box {
+ val networkCacheRepository =
+ hiltViewModel(LocalContext.current as ComponentActivity)
+ .appContainer
+ .networkCacheRepository
+
+ UpdateInfo(networkCacheRepository)
+
+ Column {
+ Spacer(modifier = Modifier.height(200.dp))
+ DayPager((uiState as ScheduleUiState.HasSchedule).group)
+ }
+ }
}
is ScheduleUiState.NoSchedule -> {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/UpdateInfo.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/UpdateInfo.kt
new file mode 100644
index 0000000..3986a11
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/UpdateInfo.kt
@@ -0,0 +1,89 @@
+package ru.n08i40k.polytechnic.next.ui.main.schedule
+
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+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.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+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.data.cache.NetworkCacheRepository
+import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository
+import ru.n08i40k.polytechnic.next.ui.ExpandableCard
+import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+
+
+fun Date.toString(format: String, locale: Locale = Locale.getDefault()): String {
+ val formatter = SimpleDateFormat(format, locale)
+ return formatter.format(this)
+}
+
+fun getCurrentDateTime(): Date {
+ return Calendar.getInstance().time
+}
+
+val expanded = mutableStateOf(false)
+
+@Preview(showBackground = true)
+@Composable
+fun UpdateInfo(networkCacheRepository: NetworkCacheRepository = FakeNetworkCacheRepository()) {
+ var expanded by remember { expanded }
+
+ val format = "hh:mm:ss dd.MM.yyyy"
+
+ val updateDates = remember { runBlocking { networkCacheRepository.getUpdateDates() } }
+
+ val currentDate = remember { getCurrentDateTime().toString(format) }
+ val cacheUpdateDate = remember { Date(updateDates.cache).toString(format) }
+ val scheduleUpdateDate = remember { Date(updateDates.schedule).toString(format) }
+
+ ExpandableCard(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded },
+ title = stringResource(R.string.update_info_header)
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(10.dp)
+ ) {
+ Row(horizontalArrangement = Arrangement.Center) {
+ Text(text = stringResource(R.string.last_local_update) + " - ")
+ Text(text = currentDate, fontWeight = FontWeight.Bold)
+ }
+
+ Row(horizontalArrangement = Arrangement.Center) {
+ Text(text = stringResource(R.string.last_server_cache_update) + " - ")
+ Text(text = cacheUpdateDate, fontWeight = FontWeight.Bold)
+ }
+
+ Row(horizontalArrangement = Arrangement.Center) {
+ Text(text = stringResource(R.string.last_server_schedule_update) + " - ")
+ Text(text = scheduleUpdateDate, fontWeight = FontWeight.Bold)
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto
index a171ddb..3e369ce 100644
--- a/app/src/main/proto/settings.proto
+++ b/app/src/main/proto/settings.proto
@@ -8,9 +8,15 @@ message CachedResponse {
string data = 2;
}
+message UpdateDates {
+ int64 cache = 1;
+ int64 schedule = 2;
+}
+
message Settings {
string user_id = 1;
string access_token = 2;
string group = 3;
map cache_storage = 4;
+ UpdateDates update_dates = 5;
}
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 1ad036a..e72358c 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -32,4 +32,8 @@
Сменить группу
Выйти с аккаунта
Кабинеты
+ Последнее локальное обновление
+ Последнее обновление кеша
+ Последнее обновление расписания
+ Дополнительная информация
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 55ab8fd..9acfb9c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,4 +32,8 @@
Change group
Sign out
Cabinets
+ Last local update
+ Last server cache update
+ Last server schedule update
+ Additional information
\ No newline at end of file