Vadik Sirekanyan пре 2 година
родитељ
комит
db2fec7208

+ 16 - 6
app/src/main/java/org/sirekanyan/outline/MainContent.kt

@@ -19,6 +19,7 @@ import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -29,13 +30,14 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
 import org.sirekanyan.outline.ext.plus
 import org.sirekanyan.outline.feature.keys.KeysContent
 import org.sirekanyan.outline.feature.keys.KeysErrorContent
 import org.sirekanyan.outline.feature.keys.KeysErrorState
+import org.sirekanyan.outline.feature.keys.KeysIdleState
 import org.sirekanyan.outline.feature.keys.KeysLoadingState
-import org.sirekanyan.outline.feature.keys.KeysSuccessState
 import org.sirekanyan.outline.feature.sort.SortBottomSheet
 import org.sirekanyan.outline.feature.sort.Sorting
 import org.sirekanyan.outline.ui.AddKeyButton
@@ -64,7 +66,15 @@ fun MainContent(state: MainState) {
                 )
             }
             is SelectedPage -> {
-                when (val keys = page.keys) {
+                val serverEntity = page.server
+                val keys by rememberFlowAsState(listOf(), serverEntity.id) {
+                    state.servers.observeKeys(serverEntity)
+                }
+                KeysContent(insets, state, keys, sorting)
+                when (page.keys) {
+                    is KeysIdleState -> {
+                        // nothing
+                    }
                     is KeysLoadingState -> {
                         Box(Modifier.fillMaxSize().padding(insets), Alignment.Center) {
                             CircularProgressIndicator()
@@ -80,14 +90,10 @@ fun MainContent(state: MainState) {
                             },
                         )
                     }
-                    is KeysSuccessState -> {
-                        KeysContent(insets, state, keys, sorting)
-                    }
                 }
                 LaunchedEffect(page.server) {
                     state.refreshCurrentKeys(showLoading = true)
                 }
-                val serverEntity = page.server
                 val cachedServer = state.servers.getCachedServer(serverEntity)
                 val server by produceState(cachedServer, serverEntity) {
                     value = state.servers.getServer(serverEntity)
@@ -140,3 +146,7 @@ fun MainContent(state: MainState) {
         }
     }
 }
+
+@Composable
+private fun <T> rememberFlowAsState(initial: T, key: Any? = null, block: () -> Flow<T>): State<T> =
+    remember(key, calculation = block).collectAsState(initial)

+ 15 - 7
app/src/main/java/org/sirekanyan/outline/MainState.kt

@@ -13,22 +13,26 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
 import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
+import kotlinx.coroutines.withContext
 import org.sirekanyan.outline.api.OutlineApi
 import org.sirekanyan.outline.api.model.Key
+import org.sirekanyan.outline.db.KeyDao
 import org.sirekanyan.outline.db.KeyValueDao
 import org.sirekanyan.outline.db.ServerDao
 import org.sirekanyan.outline.db.model.ServerEntity
+import org.sirekanyan.outline.db.rememberKeyDao
 import org.sirekanyan.outline.db.rememberKeyValueDao
 import org.sirekanyan.outline.db.rememberServerDao
 import org.sirekanyan.outline.ext.logError
 import org.sirekanyan.outline.feature.keys.KeysErrorState
+import org.sirekanyan.outline.feature.keys.KeysIdleState
 import org.sirekanyan.outline.feature.keys.KeysLoadingState
 import org.sirekanyan.outline.feature.keys.KeysState
-import org.sirekanyan.outline.feature.keys.KeysSuccessState
 import org.sirekanyan.outline.feature.sort.Sorting
 import org.sirekanyan.outline.repository.ServerRepository
 import java.net.ConnectException
@@ -58,7 +62,8 @@ fun rememberMainState(): MainState {
     val api = remember { OutlineApi() }
     val dao = rememberServerDao()
     val prefs = rememberKeyValueDao()
-    return remember { MainState(scope + supervisor, api, dao, prefs) }
+    val cache = rememberKeyDao()
+    return remember { MainState(scope + supervisor, api, dao, prefs, cache) }
 }
 
 class MainState(
@@ -66,15 +71,16 @@ class MainState(
     val api: OutlineApi,
     val dao: ServerDao,
     private val prefs: KeyValueDao,
+    cache: KeyDao,
 ) {
 
-    val servers = ServerRepository(api)
+    val servers = ServerRepository(api, cache)
     val drawer = DrawerState(DrawerValue.Closed)
     var page by mutableStateOf<Page>(HelloPage)
     var dialog by mutableStateOf<Dialog?>(null)
     val selectedPage by derivedStateOf { page as? SelectedPage }
     var selectedKey by mutableStateOf<Key?>(null)
-    val isFabVisible by derivedStateOf { (page as? SelectedPage)?.keys is KeysSuccessState }
+    val isFabVisible by derivedStateOf { (page as? SelectedPage)?.keys is KeysIdleState }
     var isFabLoading by mutableStateOf(false)
     var deletingKey by mutableStateOf<Key?>(null)
     val sorting = prefs.observe(Sorting.KEY).map(Sorting::getByKey)
@@ -102,12 +108,14 @@ class MainState(
     }
 
     suspend fun refreshCurrentKeys(showLoading: Boolean) {
-        (page as? SelectedPage)?.let { page ->
+        val page = page as? SelectedPage ?: return
+        withContext(Dispatchers.IO) {
             if (showLoading) {
                 page.keys = KeysLoadingState
             }
             page.keys = try {
-                KeysSuccessState(api.getKeys(page.server))
+                servers.updateKeys(page.server)
+                KeysIdleState
             } catch (exception: Exception) {
                 exception.printStackTrace()
                 KeysErrorState
@@ -122,7 +130,7 @@ sealed class Page
 data object HelloPage : Page()
 
 data class SelectedPage(val server: ServerEntity) : Page() {
-    var keys by mutableStateOf<KeysState>(KeysLoadingState)
+    var keys by mutableStateOf<KeysState>(KeysIdleState)
 }
 
 sealed class Dialog

+ 14 - 0
app/src/main/java/org/sirekanyan/outline/api/model/Key.kt

@@ -1,3 +1,17 @@
 package org.sirekanyan.outline.api.model
 
+import org.sirekanyan.outline.db.model.KeyEntity
+import org.sirekanyan.outline.db.model.ServerEntity
+
+fun List<Key>.toEntities(server: ServerEntity): List<KeyEntity> =
+    map { key ->
+        val accessKey = key.accessKey
+        KeyEntity(server.id, accessKey.id, accessKey.accessUrl, accessKey.name, key.traffic)
+    }
+
+fun List<KeyEntity>.fromEntities(): List<Key> =
+    map { entity ->
+        Key(AccessKey(entity.id, entity.url, entity.name), entity.traffic)
+    }
+
 class Key(val accessKey: AccessKey, val traffic: Long?)

+ 2 - 0
app/src/main/java/org/sirekanyan/outline/db/DebugDao.kt

@@ -7,6 +7,7 @@ import org.sirekanyan.outline.db.model.ServerEntity
 
 class DebugDao(private val database: OutlineDatabase) {
 
+    private val keyQueries = database.keyEntityQueries
     private val serverQueries = database.serverEntityQueries
 
     init {
@@ -18,6 +19,7 @@ class DebugDao(private val database: OutlineDatabase) {
     suspend fun reset() {
         withContext(Dispatchers.IO) {
             database.transaction {
+                keyQueries.truncate()
                 serverQueries.truncate()
                 listOf<String>(
                     // add your debug servers here

+ 37 - 0
app/src/main/java/org/sirekanyan/outline/db/KeyDao.kt

@@ -0,0 +1,37 @@
+package org.sirekanyan.outline.db
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import app.cash.sqldelight.coroutines.asFlow
+import app.cash.sqldelight.coroutines.mapToList
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+import org.sirekanyan.outline.app
+import org.sirekanyan.outline.db.model.KeyEntity
+import org.sirekanyan.outline.db.model.ServerEntity
+
+@Composable
+fun rememberKeyDao(): KeyDao {
+    val database = LocalContext.current.app().database
+    return remember { KeyDao(database) }
+}
+
+class KeyDao(database: OutlineDatabase) {
+
+    private val queries = database.keyEntityQueries
+
+    fun observe(server: ServerEntity): Flow<List<KeyEntity>> =
+        queries.selectKeys(server.id).asFlow().mapToList(Dispatchers.IO)
+
+    suspend fun update(server: ServerEntity, keys: List<KeyEntity>) {
+        withContext(Dispatchers.IO) {
+            queries.transaction {
+                queries.deleteKeys(server.id)
+                keys.forEach(queries::insertKey)
+            }
+        }
+    }
+
+}

+ 4 - 3
app/src/main/java/org/sirekanyan/outline/feature/keys/KeysContent.kt

@@ -11,14 +11,15 @@ import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import org.sirekanyan.outline.MainState
+import org.sirekanyan.outline.api.model.Key
 import org.sirekanyan.outline.ext.plus
 import org.sirekanyan.outline.feature.sort.Sorting
 
 @Composable
-fun KeysContent(insets: PaddingValues, state: MainState, keys: KeysSuccessState, sorting: Sorting) {
-    val sortedKeys by produceState(listOf(), keys.values, sorting.key) {
+fun KeysContent(insets: PaddingValues, state: MainState, keys: List<Key>, sorting: Sorting) {
+    val sortedKeys by produceState(listOf(), keys, sorting.key) {
         value = withContext(Dispatchers.IO) {
-            keys.values.sortedWith(sorting.comparator)
+            keys.sortedWith(sorting.comparator)
         }
     }
     LazyColumn(contentPadding = insets + PaddingValues(bottom = 88.dp)) {

+ 2 - 4
app/src/main/java/org/sirekanyan/outline/feature/keys/KeysState.kt

@@ -1,11 +1,9 @@
 package org.sirekanyan.outline.feature.keys
 
-import org.sirekanyan.outline.api.model.Key
-
 sealed class KeysState
 
+data object KeysIdleState : KeysState()
+
 data object KeysLoadingState : KeysState()
 
 data object KeysErrorState : KeysState()
-
-data class KeysSuccessState(val values: List<Key>) : KeysState()

+ 16 - 1
app/src/main/java/org/sirekanyan/outline/repository/ServerRepository.kt

@@ -1,13 +1,20 @@
 package org.sirekanyan.outline.repository
 
 import android.net.Uri
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 import org.sirekanyan.outline.api.OutlineApi
+import org.sirekanyan.outline.api.model.Key
 import org.sirekanyan.outline.api.model.Server
+import org.sirekanyan.outline.api.model.fromEntities
+import org.sirekanyan.outline.api.model.toEntities
+import org.sirekanyan.outline.db.KeyDao
+import org.sirekanyan.outline.db.model.KeyEntity
 import org.sirekanyan.outline.db.model.ServerEntity
 import org.sirekanyan.outline.ext.logDebug
 import java.util.concurrent.ConcurrentHashMap
 
-class ServerRepository(private val api: OutlineApi) {
+class ServerRepository(private val api: OutlineApi, private val keyDao: KeyDao) {
 
     private val cache: MutableMap<String, Server> = ConcurrentHashMap()
 
@@ -30,4 +37,12 @@ class ServerRepository(private val api: OutlineApi) {
         return getCachedServer(server)
     }
 
+    fun observeKeys(server: ServerEntity): Flow<List<Key>> =
+        keyDao.observe(server).map(List<KeyEntity>::fromEntities)
+
+    suspend fun updateKeys(server: ServerEntity) {
+        val keys = api.getKeys(server)
+        keyDao.update(server, keys.toEntities(server))
+    }
+
 }

+ 9 - 0
app/src/main/sqldelight/migrations/2.sqm

@@ -1,2 +1,11 @@
 ALTER TABLE ApiUrl RENAME TO ServerEntity;
 ALTER TABLE KeyValue RENAME TO KeyValueEntity;
+
+CREATE TABLE IF NOT EXISTS KeyEntity (
+  serverId TEXT NOT NULL,
+  id TEXT NOT NULL,
+  url TEXT NOT NULL,
+  name TEXT NOT NULL,
+  traffic INTEGER,
+  PRIMARY KEY (serverId, id)
+);

+ 20 - 0
app/src/main/sqldelight/org/sirekanyan/outline/db/model/KeyEntity.sq

@@ -0,0 +1,20 @@
+CREATE TABLE IF NOT EXISTS KeyEntity (
+  serverId TEXT NOT NULL,
+  id TEXT NOT NULL,
+  url TEXT NOT NULL,
+  name TEXT NOT NULL,
+  traffic INTEGER,
+  PRIMARY KEY (serverId, id)
+);
+
+selectKeys:
+SELECT * FROM KeyEntity WHERE serverId = ?;
+
+insertKey:
+INSERT OR REPLACE INTO KeyEntity VALUES ?;
+
+deleteKeys:
+DELETE FROM KeyEntity WHERE serverId = ?;
+
+truncate:
+DELETE FROM KeyEntity;