1package soy.iko.opencode.data.repo
3import android.content.Context
4import android.content.SharedPreferences
5import androidx.datastore.preferences.core.edit
6import androidx.datastore.preferences.core.stringPreferencesKey
7import androidx.datastore.preferences.preferencesDataStore
8import androidx.security.crypto.EncryptedSharedPreferences
9import androidx.security.crypto.MasterKey
10import soy.iko.opencode.data.model.ServerProfile
11import soy.iko.opencode.data.network.OpencodeJson
12import kotlinx.coroutines.flow.Flow
13import kotlinx.coroutines.flow.map
14import kotlinx.serialization.Serializable
15import kotlinx.serialization.builtins.ListSerializer
17private val Context.dataStore by preferencesDataStore(name = "server_profiles")
20 * Persists server profiles. Non-secret fields go to DataStore (as a JSON list);
21 * the Basic-auth password is stored separately in EncryptedSharedPreferences, keyed
22 * by profile id, and merged back in on read.
24class ProfileStore(context: Context) {
26 private val appContext = context.applicationContext
27 private val profilesKey = stringPreferencesKey("profiles_json")
29 private val securePrefs: SharedPreferences by lazy {
30 val masterKey = MasterKey.Builder(appContext)
31 .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
33 EncryptedSharedPreferences.create(
37 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
38 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
42 /** Stored shape on DataStore (everything except the secret password). */
44 private data class StoredProfile(
48 val username: String? = null,
49 val lastUsed: Long = 0,
52 val profiles: Flow<List<ServerProfile>> = appContext.dataStore.data.map { prefs ->
53 val json = prefs[profilesKey] ?: return@map emptyList()
55 OpencodeJson.decodeFromString(ListSerializer(StoredProfile.serializer()), json)
56 }.getOrDefault(emptyList())
57 .sortedByDescending { it.lastUsed }
58 .map { it.toProfile() }
61 private fun StoredProfile.toProfile() = ServerProfile(
66 password = username?.let { passwordFor(id) },
70 /** Resolve a single profile with its password loaded, for opening a connection. */
71 suspend fun resolve(profile: ServerProfile): ServerProfile =
72 profile.copy(password = if (profile.hasAuth) passwordFor(profile.id) else null)
74 fun passwordFor(id: String): String? = securePrefs.getString(passwordKey(id), null)
76 suspend fun save(profile: ServerProfile) {
77 appContext.dataStore.edit { prefs ->
78 val current = prefs[profilesKey]?.let {
79 runCatching { OpencodeJson.decodeFromString(ListSerializer(StoredProfile.serializer()), it) }
80 .getOrDefault(emptyList())
82 val stored = StoredProfile(
84 label = profile.label,
85 baseUrl = profile.baseUrl,
86 username = profile.username?.takeIf { it.isNotBlank() },
87 lastUsed = profile.lastUsed,
89 val updated = current.filterNot { it.id == profile.id } + stored
90 prefs[profilesKey] = OpencodeJson.encodeToString(ListSerializer(StoredProfile.serializer()), updated)
92 // Secret goes to encrypted storage (or is cleared when auth removed).
93 securePrefs.edit().apply {
94 val pw = profile.password
95 if (!profile.username.isNullOrBlank() && !pw.isNullOrEmpty()) {
96 putString(passwordKey(profile.id), pw)
98 remove(passwordKey(profile.id))
103 suspend fun delete(id: String) {
104 appContext.dataStore.edit { prefs ->
105 val current = prefs[profilesKey]?.let {
106 runCatching { OpencodeJson.decodeFromString(ListSerializer(StoredProfile.serializer()), it) }
107 .getOrDefault(emptyList())
109 prefs[profilesKey] = OpencodeJson.encodeToString(
110 ListSerializer(StoredProfile.serializer()),
111 current.filterNot { it.id == id },
114 securePrefs.edit().remove(passwordKey(id)).apply()
117 private fun passwordKey(id: String) = "pw_$id"
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list