Browse Source

Added name and traffic to server entity

Vadik Sirekanyan 2 years ago
parent
commit
2871bee58d

+ 2 - 2
app/src/main/java/org/sirekanyan/outline/MainActivity.kt

@@ -55,9 +55,9 @@ class MainActivity : ComponentActivity() {
                                 )
                             }
                             is DeleteServerDialog -> {
-                                val (server, serverName) = dialog
+                                val (server) = dialog
                                 DeleteServerContent(
-                                    serverName = serverName,
+                                    serverName = server.name,
                                     onDismiss = { state.dialog = null },
                                     onConfirm = {
                                         state.scope.launch(IO) {

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

@@ -27,11 +27,9 @@ 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
-import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
@@ -40,9 +38,9 @@ import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.platform.LocalContext
 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.ext.rememberFlowAsState
 import org.sirekanyan.outline.ext.showToast
 import org.sirekanyan.outline.feature.keys.KeysContent
 import org.sirekanyan.outline.feature.keys.KeysErrorContent
@@ -77,9 +75,8 @@ fun MainContent(state: MainState) {
                 )
             }
             is SelectedPage -> {
-                val serverEntity = page.server
-                val keys by rememberFlowAsState(listOf(), serverEntity.id) {
-                    state.keys.observeKeys(serverEntity)
+                val keys by rememberFlowAsState(listOf(), page.server.id) {
+                    state.keys.observeKeys(page.server)
                 }
                 KeysContent(insets, state, keys, sorting)
                 val hasKeys = keys.isNotEmpty()
@@ -129,22 +126,18 @@ fun MainContent(state: MainState) {
                 LaunchedEffect(page.server) {
                     state.refreshCurrentKeys(showLoading = true)
                 }
-                val cachedServer = state.servers.getCachedServer(serverEntity)
-                val server by produceState(cachedServer, serverEntity) {
-                    value = state.servers.getServer(serverEntity)
-                }
                 MainTopAppBar(
-                    title = server.name,
+                    title = page.server.name,
                     onMenuClick = state::openDrawer,
                     items = listOf(
                         MenuItem("Sort by…", IconSort) {
                             isSortingVisible = true
                         },
                         MenuItem("Edit", Icons.Default.Edit) {
-                            state.dialog = RenameServerDialog(page.server, server.name)
+                            state.dialog = RenameServerDialog(page.server)
                         },
                         MenuItem("Delete", Icons.Default.Delete) {
-                            state.dialog = DeleteServerDialog(page.server, server.name)
+                            state.dialog = DeleteServerDialog(page.server)
                         },
                     ),
                 )
@@ -184,7 +177,3 @@ 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)

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

@@ -75,7 +75,7 @@ class MainState(
     cache: KeyDao,
 ) {
 
-    val servers = ServerRepository(api)
+    val servers = ServerRepository(api, dao)
     val keys = KeyRepository(api, cache)
     val drawer = DrawerState(DrawerValue.Closed)
     var page by mutableStateOf<Page>(HelloPage)
@@ -139,10 +139,10 @@ sealed class Dialog
 
 data object AddServerDialog : Dialog()
 
-data class RenameServerDialog(val server: ServerEntity, val serverName: String) : Dialog()
+data class RenameServerDialog(val server: ServerEntity) : Dialog()
 
 data class RenameKeyDialog(val server: ServerEntity, val key: Key) : Dialog()
 
 data class DeleteKeyDialog(val server: ServerEntity, val key: Key) : Dialog()
 
-data class DeleteServerDialog(val server: ServerEntity, val serverName: String) : Dialog()
+data class DeleteServerDialog(val server: ServerEntity) : Dialog()

+ 3 - 0
app/src/main/java/org/sirekanyan/outline/api/model/Server.kt

@@ -3,5 +3,8 @@ package org.sirekanyan.outline.api.model
 import android.net.Uri
 import org.sirekanyan.outline.db.model.ServerEntity
 
+fun createServerEntity(url: String, insecure: Boolean): ServerEntity =
+    ServerEntity(url, insecure, name = "", traffic = null)
+
 fun ServerEntity.getHost(): String =
     Uri.parse(id).host.orEmpty()

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

@@ -1,6 +1,6 @@
 package org.sirekanyan.outline.db
 
-import org.sirekanyan.outline.db.model.ServerEntity
+import org.sirekanyan.outline.api.model.createServerEntity
 import org.sirekanyan.outline.isDebugBuild
 
 class DebugDao(private val database: OutlineDatabase) {
@@ -21,7 +21,7 @@ class DebugDao(private val database: OutlineDatabase) {
             listOf<String>(
                 // add your debug servers here
             ).forEach { url ->
-                serverQueries.insertUrl(ServerEntity(url, insecure = true))
+                serverQueries.insert(createServerEntity(url, insecure = true))
             }
         }
     }

+ 17 - 9
app/src/main/java/org/sirekanyan/outline/db/ServerDao.kt

@@ -3,11 +3,9 @@ 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.Query
 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.ServerEntity
 
@@ -21,16 +19,26 @@ class ServerDao(database: OutlineDatabase) {
 
     private val queries = database.serverEntityQueries
 
-    fun observeUrls(): Flow<List<ServerEntity>> =
-        queries.selectUrls().asFlow().mapToList(Dispatchers.IO)
+    fun selectAll(): List<ServerEntity> =
+        queries.selectAll().executeAsList()
 
-    suspend fun insertUrl(server: ServerEntity) =
-        withContext(Dispatchers.IO) {
-            queries.insertUrl(server)
+    fun observeAll(): Flow<Query<ServerEntity>> =
+        queries.selectAll().asFlow()
+
+    fun insert(server: ServerEntity) {
+        queries.insert(server)
+    }
+
+    fun insertAll(servers: List<ServerEntity>) {
+        queries.transaction {
+            servers.forEach {
+                queries.insert(it)
+            }
         }
+    }
 
     fun deleteUrl(id: String) {
-        queries.deleteUrl(id)
+        queries.delete(id)
     }
 
 }

+ 11 - 0
app/src/main/java/org/sirekanyan/outline/ext/State.kt

@@ -0,0 +1,11 @@
+package org.sirekanyan.outline.ext
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.remember
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+fun <T> rememberFlowAsState(initial: T, key: Any? = null, block: () -> Flow<T>): State<T> =
+    remember(key, calculation = block).collectAsState(initial)

+ 38 - 23
app/src/main/java/org/sirekanyan/outline/repository/ServerRepository.kt

@@ -1,37 +1,52 @@
 package org.sirekanyan.outline.repository
 
+import app.cash.sqldelight.coroutines.mapToList
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
 import org.sirekanyan.outline.api.OutlineApi
-import org.sirekanyan.outline.api.model.getHost
+import org.sirekanyan.outline.db.ServerDao
 import org.sirekanyan.outline.db.model.ServerEntity
 import org.sirekanyan.outline.ext.logDebug
-import java.util.concurrent.ConcurrentHashMap
 
-class ServerRepository(private val api: OutlineApi) {
-
-    private val cache: MutableMap<String, ServerEntity> = ConcurrentHashMap()
-
-    fun getCachedServer(server: ServerEntity): ServerEntity =
-        cache[server.id] ?: server.copy(name = server.getHost(), traffic = null)
-
-    suspend fun fetchServer(server: ServerEntity): ServerEntity =
-        api.getServer(server).also { fetched ->
-            cache[server.id] = fetched
-        }
-
-    suspend fun getServer(server: ServerEntity): ServerEntity {
-        if (!cache.containsKey(server.id)) {
-            try {
-                return fetchServer(server)
-            } catch (exception: Exception) {
-                logDebug("Cannot fetch server name", exception)
+class ServerRepository(private val api: OutlineApi, private val serverDao: ServerDao) {
+
+    fun observeServers(): Flow<List<ServerEntity>> =
+        serverDao.observeAll().mapToList(IO)
+
+    suspend fun updateServers(servers: List<ServerEntity>) {
+        withContext(IO) {
+            val deferredServers = servers.map {
+                async {
+                    try {
+                        api.getServer(it)
+                    } catch (exception: Exception) {
+                        logDebug("Cannot get server", exception)
+                        null
+                    }
+                }
             }
+            serverDao.insertAll(deferredServers.awaitAll().filterNotNull())
         }
-        return getCachedServer(server)
     }
 
+    suspend fun updateServer(server: ServerEntity): ServerEntity =
+        withContext(IO) {
+            refreshServer(server)
+        }
+
     suspend fun renameServer(server: ServerEntity, newName: String) {
-        api.renameServer(server, newName)
-        cache.clear()
+        withContext(IO) {
+            api.renameServer(server, newName)
+            refreshServer(server)
+        }
     }
 
+    private suspend fun refreshServer(server: ServerEntity): ServerEntity =
+        api.getServer(server).also { newServer ->
+            serverDao.insert(newServer)
+        }
+
 }

+ 3 - 5
app/src/main/java/org/sirekanyan/outline/ui/AddServerContent.kt

@@ -27,7 +27,7 @@ import kotlinx.coroutines.launch
 import org.sirekanyan.outline.MainState
 import org.sirekanyan.outline.NotSupportedContent
 import org.sirekanyan.outline.SelectedPage
-import org.sirekanyan.outline.db.model.ServerEntity
+import org.sirekanyan.outline.api.model.createServerEntity
 import javax.net.ssl.SSLException
 
 @Composable
@@ -44,11 +44,9 @@ fun AddServerContent(state: MainState) {
         }
         try {
             isLoading = true
-            val serverEntity = ServerEntity(draft, insecure)
-            state.servers.fetchServer(serverEntity)
-            state.dao.insertUrl(serverEntity)
+            val server = state.servers.updateServer(createServerEntity(draft, insecure))
             state.dialog = null
-            state.page = SelectedPage(serverEntity)
+            state.page = SelectedPage(server)
             state.closeDrawer(animated = false)
         } catch (exception: SSLException) {
             exception.printStackTrace()

+ 10 - 9
app/src/main/java/org/sirekanyan/outline/ui/DrawerContent.kt

@@ -24,9 +24,8 @@ import androidx.compose.material3.ModalDrawerSheet
 import androidx.compose.material3.NavigationDrawerItem
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
@@ -41,6 +40,7 @@ import org.sirekanyan.outline.R
 import org.sirekanyan.outline.SelectedPage
 import org.sirekanyan.outline.app
 import org.sirekanyan.outline.db.DebugDao
+import org.sirekanyan.outline.ext.rememberFlowAsState
 import org.sirekanyan.outline.isDebugBuild
 import org.sirekanyan.outline.isPlayFlavor
 import org.sirekanyan.outline.text.formatTraffic
@@ -69,13 +69,14 @@ private fun DrawerSheetContent(state: MainState, insets: PaddingValues) {
             modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp),
             style = MaterialTheme.typography.titleSmall,
         )
-        val serverEntities by remember { state.dao.observeUrls() }.collectAsState(listOf())
-        serverEntities.forEach { serverEntity ->
-            val isSelected = state.selectedPage?.server?.id == serverEntity.id
-            val cachedServer = state.servers.getCachedServer(serverEntity)
-            val server by produceState(cachedServer, state.drawer.isOpen) {
-                value = state.servers.getServer(serverEntity)
+        val servers by rememberFlowAsState(listOf()) { state.servers.observeServers() }
+        if (servers.isNotEmpty()) {
+            LaunchedEffect(Unit) {
+                state.servers.updateServers(servers)
             }
+        }
+        servers.forEach { server ->
+            val isSelected = state.selectedPage?.server?.id == server.id
             DrawerItem(
                 icon = Icons.Default.Done,
                 label = server.name,
@@ -90,7 +91,7 @@ private fun DrawerSheetContent(state: MainState, insets: PaddingValues) {
                 },
                 selected = isSelected,
                 onClick = {
-                    state.page = SelectedPage(serverEntity)
+                    state.page = SelectedPage(server)
                     state.closeDrawer()
                 },
             )

+ 1 - 1
app/src/main/java/org/sirekanyan/outline/ui/RenameServerContent.kt

@@ -7,7 +7,7 @@ import org.sirekanyan.outline.api.model.getHost
 
 @Composable
 fun RenameServerContent(state: MainState, dialog: RenameServerDialog) {
-    RenameContent(state, "Edit server", dialog.serverName, dialog.server.getHost()) { newName ->
+    RenameContent(state, "Edit server", dialog.server.name, dialog.server.getHost()) { newName ->
         state.servers.renameServer(dialog.server, newName)
     }
 }

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

@@ -1,6 +1,9 @@
 ALTER TABLE ApiUrl RENAME TO ServerEntity;
 ALTER TABLE KeyValue RENAME TO KeyValueEntity;
 
+ALTER TABLE ServerEntity ADD COLUMN name TEXT NOT NULL DEFAULT '';
+ALTER TABLE ServerEntity ADD COLUMN traffic INTEGER;
+
 CREATE TABLE IF NOT EXISTS KeyEntity (
   serverId TEXT NOT NULL,
   id TEXT NOT NULL,

+ 7 - 5
app/src/main/sqldelight/org/sirekanyan/outline/db/model/ServerEntity.sq

@@ -1,15 +1,17 @@
 CREATE TABLE IF NOT EXISTS ServerEntity (
   id TEXT NOT NULL PRIMARY KEY,
-  insecure INTEGER AS kotlin.Boolean NOT NULL DEFAULT 0
+  insecure INTEGER AS kotlin.Boolean NOT NULL DEFAULT 0,
+  name TEXT NOT NULL DEFAULT '',
+  traffic INTEGER
 );
 
-selectUrls:
-SELECT * FROM ServerEntity;
+selectAll:
+SELECT * FROM ServerEntity ORDER BY id;
 
-insertUrl:
+insert:
 INSERT OR REPLACE INTO ServerEntity VALUES ?;
 
-deleteUrl:
+delete:
 DELETE FROM ServerEntity WHERE id = ?;
 
 truncate: