ソースを参照

Added loading and error states for keys

Vadik Sirekanyan 2 年 前
コミット
f4cc1c0d43

+ 28 - 12
app/src/main/java/org/sirekanyan/outline/MainContent.kt

@@ -9,10 +9,10 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
@@ -31,21 +31,24 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.launch
 import org.sirekanyan.outline.api.OutlineApi
-import org.sirekanyan.outline.api.model.Key
 import org.sirekanyan.outline.db.ApiUrlDao
 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.KeysLoadingState
+import org.sirekanyan.outline.feature.keys.KeysSuccessState
 import org.sirekanyan.outline.ui.AddKeyButton
 import org.sirekanyan.outline.ui.DrawerContent
 import org.sirekanyan.outline.ui.KeyBottomSheet
-import org.sirekanyan.outline.ui.KeyContent
 
 @Composable
 fun MainContent(api: OutlineApi, dao: ApiUrlDao, state: MainState) {
     ModalNavigationDrawer({ DrawerContent(api, dao, state) }, drawerState = state.drawer) {
-        val contentPadding = WindowInsets.systemBars.asPaddingValues() + PaddingValues(top = 64.dp)
+        val insets = WindowInsets.systemBars.asPaddingValues() + PaddingValues(top = 64.dp)
         when (val page = state.page) {
             is HelloPage -> {
-                Box(Modifier.fillMaxSize().padding(contentPadding), Alignment.Center) {
+                Box(Modifier.fillMaxSize().padding(insets), Alignment.Center) {
                     TextButton(onClick = { state.dialog = AddServerDialog }) {
                         Icon(Icons.Default.Add, null)
                         Spacer(Modifier.size(8.dp))
@@ -54,15 +57,28 @@ fun MainContent(api: OutlineApi, dao: ApiUrlDao, state: MainState) {
                 }
             }
             is SelectedPage -> {
-                LazyColumn(contentPadding = contentPadding + PaddingValues(bottom = 88.dp)) {
-                    page.keys.sortedByDescending(Key::traffic).forEach { key ->
-                        item {
-                            KeyContent(key, onClick = { state.selectedKey = key })
+                when (val keys = page.keys) {
+                    is KeysLoadingState -> {
+                        Box(Modifier.fillMaxSize().padding(insets), Alignment.Center) {
+                            CircularProgressIndicator()
                         }
                     }
+                    is KeysErrorState -> {
+                        KeysErrorContent(
+                            insets = insets,
+                            onRetry = {
+                                state.scope.launch {
+                                    state.refreshCurrentKeys(showLoading = true)
+                                }
+                            },
+                        )
+                    }
+                    is KeysSuccessState -> {
+                        KeysContent(insets, state, keys)
+                    }
                 }
                 LaunchedEffect(page.selected) {
-                    state.refreshCurrentKeys()
+                    state.refreshCurrentKeys(showLoading = true)
                 }
             }
         }
@@ -75,7 +91,7 @@ fun MainContent(api: OutlineApi, dao: ApiUrlDao, state: MainState) {
                 state.selected?.let {
                     state.scope.launch {
                         api.createAccessKey(it)
-                        state.refreshCurrentKeys()
+                        state.refreshCurrentKeys(showLoading = false)
                     }
                 }
             },
@@ -91,7 +107,7 @@ fun MainContent(api: OutlineApi, dao: ApiUrlDao, state: MainState) {
                     onDeleteClick = {
                         state.scope.launch {
                             api.deleteAccessKey(selected, selectedKey.accessKey.id)
-                            state.refreshCurrentKeys()
+                            state.refreshCurrentKeys(showLoading = false)
                         }
                     },
                 )

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

@@ -13,6 +13,10 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.sirekanyan.outline.api.OutlineApi
 import org.sirekanyan.outline.api.model.Key
+import org.sirekanyan.outline.feature.keys.KeysErrorState
+import org.sirekanyan.outline.feature.keys.KeysLoadingState
+import org.sirekanyan.outline.feature.keys.KeysState
+import org.sirekanyan.outline.feature.keys.KeysSuccessState
 
 @Composable
 fun rememberMainState(api: OutlineApi): MainState {
@@ -44,9 +48,17 @@ class MainState(val scope: CoroutineScope, private val api: OutlineApi) {
         }
     }
 
-    suspend fun refreshCurrentKeys() {
+    suspend fun refreshCurrentKeys(showLoading: Boolean) {
         (page as? SelectedPage)?.let { page ->
-            page.keys = api.getKeys(page.selected)
+            if (showLoading) {
+                page.keys = KeysLoadingState
+            }
+            page.keys = try {
+                KeysSuccessState(api.getKeys(page.selected))
+            } catch (exception: Exception) {
+                exception.printStackTrace()
+                KeysErrorState
+            }
         }
     }
 
@@ -57,7 +69,7 @@ sealed class Page
 data object HelloPage : Page()
 
 data class SelectedPage(val selected: String) : Page() {
-    var keys by mutableStateOf(listOf<Key>())
+    var keys by mutableStateOf<KeysState>(KeysLoadingState)
 }
 
 sealed class Dialog

+ 1 - 1
app/src/main/java/org/sirekanyan/outline/ui/KeyContent.kt → app/src/main/java/org/sirekanyan/outline/feature/keys/KeyContent.kt

@@ -1,4 +1,4 @@
-package org.sirekanyan.outline.ui
+package org.sirekanyan.outline.feature.keys
 
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement

+ 20 - 0
app/src/main/java/org/sirekanyan/outline/feature/keys/KeysContent.kt

@@ -0,0 +1,20 @@
+package org.sirekanyan.outline.feature.keys
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import org.sirekanyan.outline.MainState
+import org.sirekanyan.outline.api.model.Key
+import org.sirekanyan.outline.ext.plus
+
+@Composable
+fun KeysContent(insets: PaddingValues, state: MainState, keys: KeysSuccessState) {
+    LazyColumn(contentPadding = insets + PaddingValues(bottom = 88.dp)) {
+        keys.values.sortedByDescending(Key::traffic).forEach { key ->
+            item {
+                KeyContent(key, onClick = { state.selectedKey = key })
+            }
+        }
+    }
+}

+ 33 - 0
app/src/main/java/org/sirekanyan/outline/feature/keys/KeysErrorContent.kt

@@ -0,0 +1,33 @@
+package org.sirekanyan.outline.feature.keys
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun KeysErrorContent(insets: PaddingValues, onRetry: () -> Unit) {
+    Column(
+        modifier = Modifier.fillMaxSize().padding(insets).padding(16.dp),
+        verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Text(
+            text = "Check your network connection",
+            modifier = Modifier.fillMaxWidth(),
+            textAlign = TextAlign.Center,
+        )
+        Button(onClick = onRetry) {
+            Text(text = "Try again")
+        }
+    }
+}

+ 11 - 0
app/src/main/java/org/sirekanyan/outline/feature/keys/KeysState.kt

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

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

@@ -45,7 +45,7 @@ fun EditKeyContent(api: OutlineApi, state: MainState, dialog: EditKeyDialog) {
                         exception.printStackTrace()
                         error = "Check name or try again"
                     }
-                    state.refreshCurrentKeys()
+                    state.refreshCurrentKeys(showLoading = false)
                 }
             },
         )