diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 2a6b3cd..e066838 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -36,6 +36,7 @@ kotlin { implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) implementation(libs.multiplatform.settings) implementation(libs.multiplatform.settings.no.arg) implementation(libs.lifecycle.viewmodel.compose) diff --git a/composeApp/src/commonMain/kotlin/ch/parano/myice/cache/ScheduleCache.kt b/composeApp/src/commonMain/kotlin/ch/parano/myice/cache/ScheduleCache.kt new file mode 100644 index 0000000..9cbb09c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ch/parano/myice/cache/ScheduleCache.kt @@ -0,0 +1,76 @@ +package ch.parano.myice.cache + +import ch.parano.myice.models.Event +import com.russhwolf.settings.Settings +import kotlin.time.Clock +import kotlin.time.ExperimentalTime +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json + +@OptIn(ExperimentalTime::class) +class ScheduleCache(private val settings: Settings) { + + private val json = Json { ignoreUnknownKeys = true } + private val eventListSerializer = ListSerializer(Event.serializer()) + + private fun eventsKey(account: String) = "cached_events_$account" + private fun agegroupKey(account: String) = "filters_agegroup_$account" + private fun subgroupKey(account: String) = "filters_subgroup_$account" + private fun typeFilterKey(account: String) = "filters_typefilter_$account" + private fun timestampKey(account: String) = "cached_events_timestamp_$account" + + fun saveEvents(account: String, events: List) { + settings.putString(eventsKey(account), json.encodeToString(eventListSerializer, events)) + settings.putLong(timestampKey(account), Clock.System.now().toEpochMilliseconds()) + } + + fun loadEvents(account: String): List? { + val jsonString = settings.getStringOrNull(eventsKey(account)) ?: return null + return try { + json.decodeFromString(eventListSerializer, jsonString) + } catch (e: Exception) { + null + } + } + + fun getLastUpdated(account: String): Long? { + return settings.getLongOrNull(timestampKey(account)) + } + + fun saveFilters(account: String, agegroup: String?, subgroup: String?) { + if (agegroup != null) settings.putString(agegroupKey(account), agegroup) + else settings.remove(agegroupKey(account)) + if (subgroup != null) settings.putString(subgroupKey(account), subgroup) + else settings.remove(subgroupKey(account)) + } + + fun loadFilters(account: String): Pair { + return settings.getStringOrNull(agegroupKey(account)) to + settings.getStringOrNull(subgroupKey(account)) + } + + fun saveTypeFilter(account: String, typeFilter: String) { + settings.putString(typeFilterKey(account), typeFilter) + } + + fun loadTypeFilter(account: String): String? { + return settings.getStringOrNull(typeFilterKey(account)) + } + + fun clearCache(account: String) { + settings.remove(eventsKey(account)) + settings.remove(timestampKey(account)) + settings.remove(agegroupKey(account)) + settings.remove(subgroupKey(account)) + settings.remove(typeFilterKey(account)) + } + + fun clearAllCache() { + val keys = settings.keys.filter { key -> + key.startsWith("cached_events_") || + key.startsWith("filters_") || + key.startsWith("cached_events_timestamp_") + } + keys.forEach { settings.remove(it) } + } +} diff --git a/composeApp/src/commonTest/kotlin/ch/parano/myice/cache/ScheduleCacheTest.kt b/composeApp/src/commonTest/kotlin/ch/parano/myice/cache/ScheduleCacheTest.kt new file mode 100644 index 0000000..038a815 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/ch/parano/myice/cache/ScheduleCacheTest.kt @@ -0,0 +1,121 @@ +package ch.parano.myice.cache + +import ch.parano.myice.models.Event +import com.russhwolf.settings.MapSettings +import com.russhwolf.settings.Settings +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ScheduleCacheTest { + + private fun createCache(): Pair { + val settings = MapSettings() + return ScheduleCache(settings) to settings + } + + private fun sampleEvent(id: String, eventType: String = "Jeu") = Event( + idEvent = id, + agegroup = "U13 (Elite)", + name = "U13 Elite - HC Ajoie", + title = "U13 (Elite)\nGame\nArena", + opponent = "HC Ajoie", + place = "Arena", + start = "2024-11-10T14:00:00", + end = "2024-11-10T16:15:00", + color = "#e4222e", + eventType = eventType, + ) + + @Test + fun testSaveAndLoadEvents() { + val (cache, _) = createCache() + val events = listOf(sampleEvent("1"), sampleEvent("2")) + cache.saveEvents("default", events) + + val loaded = cache.loadEvents("default") + assertNotNull(loaded) + assertEquals(2, loaded.size) + assertEquals("1", loaded[0].idEvent) + assertEquals("2", loaded[1].idEvent) + } + + @Test + fun testLoadEventsReturnsNullWhenEmpty() { + val (cache, _) = createCache() + assertNull(cache.loadEvents("default")) + } + + @Test + fun testGetLastUpdated() { + val (cache, _) = createCache() + assertNull(cache.getLastUpdated("default")) + + cache.saveEvents("default", listOf(sampleEvent("1"))) + val timestamp = cache.getLastUpdated("default") + assertNotNull(timestamp) + assertTrue(timestamp > 0) + } + + @Test + fun testSaveAndLoadFilters() { + val (cache, _) = createCache() + cache.saveFilters("default", agegroup = "U13 (Elite)", subgroup = "U13 Elite") + + val (agegroup, subgroup) = cache.loadFilters("default") + assertEquals("U13 (Elite)", agegroup) + assertEquals("U13 Elite", subgroup) + } + + @Test + fun testSaveFiltersWithNulls() { + val (cache, _) = createCache() + cache.saveFilters("default", agegroup = "U13", subgroup = "Elite") + cache.saveFilters("default", agegroup = null, subgroup = null) + + val (agegroup, subgroup) = cache.loadFilters("default") + assertNull(agegroup) + assertNull(subgroup) + } + + @Test + fun testClearCache() { + val (cache, _) = createCache() + cache.saveEvents("default", listOf(sampleEvent("1"))) + cache.saveFilters("default", "U13", "Elite") + + cache.clearCache("default") + + assertNull(cache.loadEvents("default")) + val (agegroup, subgroup) = cache.loadFilters("default") + assertNull(agegroup) + assertNull(subgroup) + } + + @Test + fun testClearAllCache() { + val (cache, _) = createCache() + cache.saveEvents("default", listOf(sampleEvent("1"))) + cache.saveEvents("leonard", listOf(sampleEvent("2"))) + + cache.clearAllCache() + + assertNull(cache.loadEvents("default")) + assertNull(cache.loadEvents("leonard")) + } + + @Test + fun testTypeFilterPersistence() { + val (cache, _) = createCache() + cache.saveTypeFilter("default", "GAMES") + assertEquals("GAMES", cache.loadTypeFilter("default")) + } + + @Test + fun testTypeFilterNullWhenNotSet() { + val (cache, _) = createCache() + assertNull(cache.loadTypeFilter("default")) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8a5d2de..63b10ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ agp = "8.13.2" ktor = "3.5.1" kotlinx-serialization-json = "1.11.0" kotlinx-coroutines = "1.11.0" +kotlinx-datetime = "0.7.0" multiplatform-settings = "1.3.0" lifecycle-viewmodel-compose = "2.10.0" androidx-activity-compose = "1.13.0" @@ -19,6 +20,7 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" } multiplatform-settings-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" } multiplatform-settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatform-settings" }