diff --git a/.run/vvm.desktop.run.run.xml b/.run/wwm.desktop.run.run.xml
similarity index 86%
rename from .run/vvm.desktop.run.run.xml
rename to .run/wwm.desktop.run.run.xml
index 3178ba5..a1a40e4 100644
--- a/.run/vvm.desktop.run.run.xml
+++ b/.run/wwm.desktop.run.run.xml
@@ -1,6 +1,11 @@
+
@@ -22,4 +27,4 @@
false
-
+
\ No newline at end of file
diff --git a/wwm/core/data/build.gradle.kts b/wwm/core/data/build.gradle.kts
index c14dbf7..b53693c 100644
--- a/wwm/core/data/build.gradle.kts
+++ b/wwm/core/data/build.gradle.kts
@@ -9,6 +9,9 @@ kotlin {
implementation(project(":wwm-core-foundation"))
implementation(project(":wwm-core-resources"))
}
+ dependencies {
+ implementation(kotlinx.serialization.json)
+ }
}
}
}
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/AchievementsRepository.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/AchievementsRepository.kt
index 47f5cd7..7c46793 100644
--- a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/AchievementsRepository.kt
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/AchievementsRepository.kt
@@ -13,4 +13,6 @@ interface AchievementsRepository {
suspend fun reloadAchievementCategories()
suspend fun reloadAchievementGroups()
suspend fun reloadMultiText(locale: WwLocale)
+ suspend fun markAchievement(achievementId: Int)
+ suspend fun unmarkAchievement(achievementId: Int)
}
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmAchievements.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmAchievements.kt
index 83a71eb..d4ffe71 100644
--- a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmAchievements.kt
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmAchievements.kt
@@ -15,6 +15,7 @@ data class WwmAchievements(
val achievementGroups: WwAchievementGroups = emptyList(),
val multiText: WwMultiText = emptyList(),
val locale: WwLocale = WwLocale.ZH_HANS,
+ val markedAchievementIds: WwmMarkedAchievementIds = emptySet(),
) {
companion object {
val Empty: WwmAchievements = WwmAchievements()
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmDataComponent.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmDataComponent.kt
index a012133..bd6bd5c 100644
--- a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmDataComponent.kt
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmDataComponent.kt
@@ -4,8 +4,12 @@
package dev.omico.wwm.data
import dev.omico.wwm.data.internal.AchievementsRepositoryImpl
+import dev.omico.wwm.data.internal.MarkedAchievementsDataStore
interface WwmDataComponent {
val achievementsRepository: AchievementsRepository
- fun provideAchievementsRepository(): AchievementsRepository = AchievementsRepositoryImpl()
+ fun provideAchievementsRepository(): AchievementsRepository =
+ AchievementsRepositoryImpl(
+ markedAchievementsDataStore = MarkedAchievementsDataStore(),
+ )
}
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmMarkedAchievementIds.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmMarkedAchievementIds.kt
new file mode 100644
index 0000000..34ccaf6
--- /dev/null
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/WwmMarkedAchievementIds.kt
@@ -0,0 +1,6 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.data
+
+typealias WwmMarkedAchievementIds = Set
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/AchievementsRepositoryImpl.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/AchievementsRepositoryImpl.kt
index b71c95e..6b07d85 100644
--- a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/AchievementsRepositoryImpl.kt
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/AchievementsRepositoryImpl.kt
@@ -5,6 +5,7 @@ package dev.omico.wwm.data.internal
import dev.omico.wwm.data.AchievementsRepository
import dev.omico.wwm.data.WwmAchievements
+import dev.omico.wwm.data.WwmMarkedAchievementIds
import dev.omico.wwm.resources.WwmResources
import dev.omico.wwm.resources.model.game.WwAchievementCategories
import dev.omico.wwm.resources.model.game.WwAchievementGroups
@@ -14,14 +15,20 @@ import dev.omico.wwm.resources.model.game.WwMultiText
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
-internal class AchievementsRepositoryImpl : AchievementsRepository {
+internal class AchievementsRepositoryImpl(
+ private val markedAchievementsDataStore: MarkedAchievementsDataStore,
+) : AchievementsRepository {
private val wwAchievements: MutableStateFlow = MutableStateFlow(emptyList())
private val wwAchievementCategories: MutableStateFlow = MutableStateFlow(emptyList())
private val wwAchievementGroups: MutableStateFlow = MutableStateFlow(emptyList())
private val wwMultiText: MutableStateFlow = MutableStateFlow(emptyList())
private val wwLocale: MutableStateFlow = MutableStateFlow(WwLocale.ZH_HANS)
+ private val markedAchievementIds: MutableStateFlow = MutableStateFlow(emptySet())
+ @Suppress("UNCHECKED_CAST")
override val achievements: Flow =
combine(
wwAchievements,
@@ -29,8 +36,17 @@ internal class AchievementsRepositoryImpl : AchievementsRepository {
wwAchievementGroups,
wwMultiText,
wwLocale,
- ::WwmAchievements,
- )
+ markedAchievementIds,
+ ) { arguments ->
+ WwmAchievements(
+ achievements = arguments[0] as WwAchievements,
+ achievementCategories = arguments[1] as WwAchievementCategories,
+ achievementGroups = arguments[2] as WwAchievementGroups,
+ multiText = arguments[3] as WwMultiText,
+ locale = arguments[4] as WwLocale,
+ markedAchievementIds = arguments[5] as WwmMarkedAchievementIds,
+ )
+ }
override suspend fun load() {
reloadAchievements()
@@ -38,7 +54,10 @@ internal class AchievementsRepositoryImpl : AchievementsRepository {
reloadAchievementGroups()
}
- override suspend fun reloadAchievements(): Unit = wwAchievements.emit(WwmResources.loadAchievements())
+ override suspend fun reloadAchievements() {
+ wwAchievements.emit(WwmResources.loadAchievements())
+ markedAchievementIds.emit(markedAchievementsDataStore.load())
+ }
override suspend fun reloadAchievementCategories(): Unit =
wwAchievementCategories.emit(WwmResources.loadAchievementCategories())
@@ -50,4 +69,18 @@ internal class AchievementsRepositoryImpl : AchievementsRepository {
wwMultiText.emit(WwmResources.loadMultiText(locale))
wwLocale.emit(locale)
}
+
+ override suspend fun markAchievement(achievementId: Int): Unit =
+ saveMarkedAchievementIds(markedAchievementIds.value + achievementId)
+
+ override suspend fun unmarkAchievement(achievementId: Int): Unit =
+ saveMarkedAchievementIds(markedAchievementIds.value - achievementId)
+
+ private val markedAchievementIdsMutex: Mutex = Mutex()
+
+ private suspend fun saveMarkedAchievementIds(ids: WwmMarkedAchievementIds): Unit =
+ markedAchievementIdsMutex.withLock {
+ markedAchievementsDataStore.save(ids)
+ markedAchievementIds.emit(ids)
+ }
}
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/Json.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/Json.kt
new file mode 100644
index 0000000..688454f
--- /dev/null
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/Json.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.data.internal
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+@OptIn(ExperimentalSerializationApi::class)
+internal val json: Json =
+ Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+
+internal inline fun T.toJson(): String = json.encodeToString(this)
+
+internal inline fun String.fromJson(): T = json.decodeFromString(this)
diff --git a/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.kt b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.kt
new file mode 100644
index 0000000..84cd104
--- /dev/null
+++ b/wwm/core/data/src/commonMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.data.internal
+
+import dev.omico.wwm.data.WwmMarkedAchievementIds
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") // TODO KT-61573
+internal expect class MarkedAchievementsDataStore() {
+ suspend fun load(): WwmMarkedAchievementIds
+ suspend fun save(ids: WwmMarkedAchievementIds)
+}
diff --git a/wwm/core/data/src/desktopMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.desktop.kt b/wwm/core/data/src/desktopMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.desktop.kt
new file mode 100644
index 0000000..99cb0ad
--- /dev/null
+++ b/wwm/core/data/src/desktopMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.desktop.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.data.internal
+
+import dev.omico.wwm.data.WwmMarkedAchievementIds
+import dev.omico.wwm.foundation.wwmMarkedAchievementsFile
+import java.nio.file.Path
+import kotlin.io.path.exists
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") // TODO KT-61573
+internal actual class MarkedAchievementsDataStore {
+ actual suspend fun load(): WwmMarkedAchievementIds =
+ wwmMarkedAchievementsFile.takeIf(Path::exists)?.readText()?.fromJson() ?: emptySet()
+
+ actual suspend fun save(ids: WwmMarkedAchievementIds) {
+ wwmMarkedAchievementsFile.writeText(ids.toJson())
+ }
+}
diff --git a/wwm/core/data/src/wasmJsMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.wasmJs.kt b/wwm/core/data/src/wasmJsMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.wasmJs.kt
new file mode 100644
index 0000000..310ad70
--- /dev/null
+++ b/wwm/core/data/src/wasmJsMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.wasmJs.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.data.internal
+
+import dev.omico.wwm.data.WwmMarkedAchievementIds
+import kotlinx.browser.localStorage
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") // TODO KT-61573
+internal actual class MarkedAchievementsDataStore {
+ actual suspend fun load(): WwmMarkedAchievementIds {
+ val achievements = localStorage.getItem(ACHIEVEMENTS_KEY) ?: return emptySet()
+ return achievements.split(",").map(String::toInt).toSet()
+ }
+
+ actual suspend fun save(ids: WwmMarkedAchievementIds): Unit =
+ localStorage.setItem(ACHIEVEMENTS_KEY, ids.joinToString(","))
+}
+
+private const val ACHIEVEMENTS_KEY = "achievements"
diff --git a/wwm/core/data/src/webMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.web.kt b/wwm/core/data/src/webMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.web.kt
new file mode 100644
index 0000000..310ad70
--- /dev/null
+++ b/wwm/core/data/src/webMain/kotlin/dev/omico/wwm/data/internal/MarkedAchievementsDataStore.web.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.data.internal
+
+import dev.omico.wwm.data.WwmMarkedAchievementIds
+import kotlinx.browser.localStorage
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") // TODO KT-61573
+internal actual class MarkedAchievementsDataStore {
+ actual suspend fun load(): WwmMarkedAchievementIds {
+ val achievements = localStorage.getItem(ACHIEVEMENTS_KEY) ?: return emptySet()
+ return achievements.split(",").map(String::toInt).toSet()
+ }
+
+ actual suspend fun save(ids: WwmMarkedAchievementIds): Unit =
+ localStorage.setItem(ACHIEVEMENTS_KEY, ids.joinToString(","))
+}
+
+private const val ACHIEVEMENTS_KEY = "achievements"
diff --git a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsPresenter.kt b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsPresenter.kt
index 36632de..e89450e 100644
--- a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsPresenter.kt
+++ b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsPresenter.kt
@@ -8,14 +8,17 @@ 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 com.slack.circuit.retained.collectAsRetainedState
import dev.omico.wwm.data.WwmAchievements
import dev.omico.wwm.ui.WwmUiComponent
+import kotlinx.coroutines.launch
context(WwmUiComponent)
@Composable
internal fun produceAchievementsUiState(): AchievementsUiState {
+ val scope = rememberCoroutineScope()
val achievements by achievementsRepository.achievements.collectAsRetainedState(initial = WwmAchievements.Empty)
var locale by remember { mutableStateOf(achievements.locale) }
LaunchedEffect(Unit) { achievementsRepository.load() }
@@ -25,6 +28,13 @@ internal fun produceAchievementsUiState(): AchievementsUiState {
eventSink = { event ->
when (event) {
is AchievementsUiEvent.ChangeLocale -> locale = event.locale
+ is AchievementsUiEvent.ChangeAchievementMark ->
+ scope.launch {
+ when {
+ event.marked -> achievementsRepository.markAchievement(event.achievementId)
+ else -> achievementsRepository.unmarkAchievement(event.achievementId)
+ }
+ }
}
},
)
diff --git a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiEvent.kt b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiEvent.kt
index be8224a..3473475 100644
--- a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiEvent.kt
+++ b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiEvent.kt
@@ -8,4 +8,9 @@ import dev.omico.wwm.resources.model.game.WwLocale
sealed interface AchievementsUiEvent : CircuitUiEvent {
data class ChangeLocale(val locale: WwLocale) : AchievementsUiEvent
+
+ data class ChangeAchievementMark(
+ val marked: Boolean,
+ val achievementId: Int,
+ ) : AchievementsUiEvent
}
diff --git a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiState.kt b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiState.kt
index 90e9044..de9ed63 100644
--- a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiState.kt
+++ b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/AchievementsUiState.kt
@@ -5,8 +5,12 @@ package dev.omico.wwm.feature.achievements
import com.slack.circuit.runtime.CircuitUiState
import dev.omico.wwm.data.WwmAchievements
+import dev.omico.wwm.resources.model.game.WwAchievement
data class AchievementsUiState(
val achievements: WwmAchievements,
val eventSink: (AchievementsUiEvent) -> Unit,
) : CircuitUiState
+
+internal fun AchievementsUiState.isAchievementMarked(achievement: WwAchievement): Boolean =
+ achievement.id in achievements.markedAchievementIds
diff --git a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/component/AchievementsDetailContent.kt b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/component/AchievementsDetailContent.kt
index 001d069..2968173 100644
--- a/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/component/AchievementsDetailContent.kt
+++ b/wwm/core/feature/achievements/src/commonMain/kotlin/dev/omico/wwm/feature/achievements/component/AchievementsDetailContent.kt
@@ -12,7 +12,9 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import dev.omico.wwm.feature.achievements.AchievementsUiEvent
import dev.omico.wwm.feature.achievements.AchievementsUiState
+import dev.omico.wwm.feature.achievements.isAchievementMarked
import dev.omico.wwm.resources.model.game.WwAchievement
import dev.omico.wwm.resources.model.game.WwAchievementGroup
import dev.omico.wwm.resources.rememberWwText
@@ -33,12 +35,19 @@ internal fun AchievementsDetailContent(
contentPadding = contentPadding,
content = {
items(
- items = achievements,
+ items = achievements.sortedBy(state::isAchievementMarked),
key = WwAchievement::id,
itemContent = { achievement ->
AchievementsDetailItem(
- marked = false,
- onMarkedChange = {},
+ marked = state.isAchievementMarked(achievement),
+ onMarkedChange = { marked ->
+ state.eventSink(
+ AchievementsUiEvent.ChangeAchievementMark(
+ marked = marked,
+ achievementId = achievement.id,
+ ),
+ )
+ },
name = rememberWwText(
multiText = state.achievements.multiText,
name = achievement.name,
diff --git a/wwm/core/foundation/src/desktopMain/kotlin/dev/omico/wwm/foundation/DesktopApplicationEnvironments.kt b/wwm/core/foundation/src/desktopMain/kotlin/dev/omico/wwm/foundation/DesktopApplicationEnvironments.kt
new file mode 100644
index 0000000..a0ac351
--- /dev/null
+++ b/wwm/core/foundation/src/desktopMain/kotlin/dev/omico/wwm/foundation/DesktopApplicationEnvironments.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.foundation
+
+object DesktopApplicationEnvironments {
+ private val environments: Map = System.getenv()
+
+ val WWM_LOCAL_DIRECTORY: String? = environments["WWM_LOCAL_DIRECTORY"]
+}
diff --git a/wwm/core/foundation/src/desktopMain/kotlin/dev/omico/wwm/foundation/DesktopApplicationPaths.kt b/wwm/core/foundation/src/desktopMain/kotlin/dev/omico/wwm/foundation/DesktopApplicationPaths.kt
new file mode 100644
index 0000000..8186e8d
--- /dev/null
+++ b/wwm/core/foundation/src/desktopMain/kotlin/dev/omico/wwm/foundation/DesktopApplicationPaths.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 Omico. All Rights Reserved.
+ */
+package dev.omico.wwm.foundation
+
+import java.nio.file.Path
+import kotlin.io.path.Path
+import kotlin.io.path.createParentDirectories
+
+private val userHomeDirectory: Path = Path(System.getProperty("user.home"))
+
+private val wwmLocalDirectory: Path =
+ DesktopApplicationEnvironments.WWM_LOCAL_DIRECTORY?.let(::Path)
+ ?: userHomeDirectory.resolve(".wwm")
+
+val wwmMarkedAchievementsFile: Path = wwmLocalDirectory.resolve("MarkedAchievements.json").createParentDirectories()