Explorar o código

Added sort bottom sheet

Vadik Sirekanyan %!s(int64=2) %!d(string=hai) anos
pai
achega
9d4cb64c70

+ 20 - 1
app/src/main/java/org/sirekanyan/outline/MainContent.kt

@@ -19,8 +19,12 @@ import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
@@ -32,12 +36,17 @@ import org.sirekanyan.outline.feature.keys.KeysErrorContent
 import org.sirekanyan.outline.feature.keys.KeysErrorState
 import org.sirekanyan.outline.feature.keys.KeysErrorState
 import org.sirekanyan.outline.feature.keys.KeysLoadingState
 import org.sirekanyan.outline.feature.keys.KeysLoadingState
 import org.sirekanyan.outline.feature.keys.KeysSuccessState
 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
 import org.sirekanyan.outline.ui.AddKeyButton
 import org.sirekanyan.outline.ui.DrawerContent
 import org.sirekanyan.outline.ui.DrawerContent
 import org.sirekanyan.outline.ui.KeyBottomSheet
 import org.sirekanyan.outline.ui.KeyBottomSheet
+import org.sirekanyan.outline.ui.icons.IconSort
 
 
 @Composable
 @Composable
 fun MainContent(state: MainState) {
 fun MainContent(state: MainState) {
+    val sorting by state.sorting.collectAsState(Sorting.DEFAULT)
+    var isSortingVisible by remember { mutableStateOf(false) }
     ModalNavigationDrawer({ DrawerContent(state) }, drawerState = state.drawer) {
     ModalNavigationDrawer({ DrawerContent(state) }, drawerState = state.drawer) {
         val insets = WindowInsets.systemBars.asPaddingValues() + PaddingValues(top = 64.dp)
         val insets = WindowInsets.systemBars.asPaddingValues() + PaddingValues(top = 64.dp)
         when (val page = state.page) {
         when (val page = state.page) {
@@ -72,7 +81,7 @@ fun MainContent(state: MainState) {
                         )
                         )
                     }
                     }
                     is KeysSuccessState -> {
                     is KeysSuccessState -> {
-                        KeysContent(insets, state, keys)
+                        KeysContent(insets, state, keys, sorting)
                     }
                     }
                 }
                 }
                 LaunchedEffect(page.apiUrl) {
                 LaunchedEffect(page.apiUrl) {
@@ -86,6 +95,9 @@ fun MainContent(state: MainState) {
                     title = server.name,
                     title = server.name,
                     onMenuClick = state::openDrawer,
                     onMenuClick = state::openDrawer,
                     items = listOf(
                     items = listOf(
+                        MenuItem("Sort by…", IconSort) {
+                            isSortingVisible = true
+                        },
                         MenuItem("Delete", Icons.Default.Delete) {
                         MenuItem("Delete", Icons.Default.Delete) {
                             state.dialog = DeleteServerDialog(page.apiUrl, server.name)
                             state.dialog = DeleteServerDialog(page.apiUrl, server.name)
                         },
                         },
@@ -118,5 +130,12 @@ fun MainContent(state: MainState) {
                 )
                 )
             }
             }
         }
         }
+        if (isSortingVisible) {
+            SortBottomSheet(
+                sorting = sorting,
+                onSortingChange = { state.putSorting(it) },
+                onDismissRequest = { isSortingVisible = false },
+            )
+        }
     }
     }
 }
 }

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

@@ -14,18 +14,22 @@ import androidx.compose.ui.platform.LocalContext
 import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
 import kotlinx.coroutines.plus
 import org.sirekanyan.outline.api.OutlineApi
 import org.sirekanyan.outline.api.OutlineApi
 import org.sirekanyan.outline.api.model.Key
 import org.sirekanyan.outline.api.model.Key
 import org.sirekanyan.outline.db.ApiUrlDao
 import org.sirekanyan.outline.db.ApiUrlDao
+import org.sirekanyan.outline.db.KeyValueDao
 import org.sirekanyan.outline.db.model.ApiUrl
 import org.sirekanyan.outline.db.model.ApiUrl
 import org.sirekanyan.outline.db.rememberApiUrlDao
 import org.sirekanyan.outline.db.rememberApiUrlDao
+import org.sirekanyan.outline.db.rememberKeyValueDao
 import org.sirekanyan.outline.ext.logError
 import org.sirekanyan.outline.ext.logError
 import org.sirekanyan.outline.feature.keys.KeysErrorState
 import org.sirekanyan.outline.feature.keys.KeysErrorState
 import org.sirekanyan.outline.feature.keys.KeysLoadingState
 import org.sirekanyan.outline.feature.keys.KeysLoadingState
 import org.sirekanyan.outline.feature.keys.KeysState
 import org.sirekanyan.outline.feature.keys.KeysState
 import org.sirekanyan.outline.feature.keys.KeysSuccessState
 import org.sirekanyan.outline.feature.keys.KeysSuccessState
+import org.sirekanyan.outline.feature.sort.Sorting
 import org.sirekanyan.outline.repository.ServerRepository
 import org.sirekanyan.outline.repository.ServerRepository
 import java.net.ConnectException
 import java.net.ConnectException
 import java.net.UnknownHostException
 import java.net.UnknownHostException
@@ -53,10 +57,16 @@ fun rememberMainState(): MainState {
     val supervisor = remember { SupervisorJob() }
     val supervisor = remember { SupervisorJob() }
     val api = remember { OutlineApi() }
     val api = remember { OutlineApi() }
     val dao = rememberApiUrlDao()
     val dao = rememberApiUrlDao()
-    return remember { MainState(scope + supervisor, api, dao) }
+    val prefs = rememberKeyValueDao()
+    return remember { MainState(scope + supervisor, api, dao, prefs) }
 }
 }
 
 
-class MainState(val scope: CoroutineScope, val api: OutlineApi, val dao: ApiUrlDao) {
+class MainState(
+    val scope: CoroutineScope,
+    val api: OutlineApi,
+    val dao: ApiUrlDao,
+    private val prefs: KeyValueDao,
+) {
 
 
     val servers = ServerRepository(api)
     val servers = ServerRepository(api)
     val drawer = DrawerState(DrawerValue.Closed)
     val drawer = DrawerState(DrawerValue.Closed)
@@ -67,6 +77,13 @@ class MainState(val scope: CoroutineScope, val api: OutlineApi, val dao: ApiUrlD
     val isFabVisible by derivedStateOf { (page as? SelectedPage)?.keys is KeysSuccessState }
     val isFabVisible by derivedStateOf { (page as? SelectedPage)?.keys is KeysSuccessState }
     var isFabLoading by mutableStateOf(false)
     var isFabLoading by mutableStateOf(false)
     var deletingKey by mutableStateOf<Key?>(null)
     var deletingKey by mutableStateOf<Key?>(null)
+    val sorting = prefs.observe(Sorting.KEY).map(Sorting::getByKey)
+
+    fun putSorting(sorting: Sorting) {
+        scope.launch {
+            prefs.put(Sorting.KEY, sorting.key)
+        }
+    }
 
 
     fun openDrawer() {
     fun openDrawer() {
         scope.launch {
         scope.launch {

+ 1 - 1
app/src/main/java/org/sirekanyan/outline/api/model/AccessKey.kt

@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
 class AccessKey(
 class AccessKey(
     val id: String,
     val id: String,
     val accessUrl: String,
     val accessUrl: String,
-    private val name: String,
+    val name: String,
 ) {
 ) {
     val defaultName: String
     val defaultName: String
         get() = "Key $id"
         get() = "Key $id"

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

@@ -5,15 +5,24 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.LocalContentColor
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import org.sirekanyan.outline.MainState
 import org.sirekanyan.outline.MainState
-import org.sirekanyan.outline.api.model.Key
 import org.sirekanyan.outline.ext.plus
 import org.sirekanyan.outline.ext.plus
+import org.sirekanyan.outline.feature.sort.Sorting
 
 
 @Composable
 @Composable
-fun KeysContent(insets: PaddingValues, state: MainState, keys: KeysSuccessState) {
+fun KeysContent(insets: PaddingValues, state: MainState, keys: KeysSuccessState, sorting: Sorting) {
+    val sortedKeys by produceState(listOf(), keys.values, sorting.key) {
+        value = withContext(Dispatchers.IO) {
+            keys.values.sortedWith(sorting.comparator)
+        }
+    }
     LazyColumn(contentPadding = insets + PaddingValues(bottom = 88.dp)) {
     LazyColumn(contentPadding = insets + PaddingValues(bottom = 88.dp)) {
-        keys.values.sortedByDescending(Key::traffic).forEach { key ->
+        sortedKeys.forEach { key ->
             item {
             item {
                 val isDeleting = key.accessKey.accessUrl == state.deletingKey?.accessKey?.accessUrl
                 val isDeleting = key.accessKey.accessUrl == state.deletingKey?.accessKey?.accessUrl
                 val alpha = if (isDeleting) 0.5f else 1f
                 val alpha = if (isDeleting) 0.5f else 1f

+ 60 - 0
app/src/main/java/org/sirekanyan/outline/feature/sort/SortBottomSheet.kt

@@ -0,0 +1,60 @@
+package org.sirekanyan.outline.feature.sort
+
+import androidx.compose.foundation.clickable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import kotlinx.coroutines.launch
+import org.sirekanyan.outline.ui.SimpleBottomSheet
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun SortBottomSheet(
+    sorting: Sorting,
+    onSortingChange: (Sorting) -> Unit,
+    onDismissRequest: () -> Unit,
+) {
+    val coroutineScope = rememberCoroutineScope()
+    SimpleBottomSheet(
+        title = "Sort by…",
+        onDismissRequest = onDismissRequest,
+        items = { sheetState ->
+            Sorting.values().forEach { option ->
+                ListItem(
+                    headlineContent = {
+                        Text(
+                            text = stringResource(option.title),
+                            color = if (sorting == option) {
+                                MaterialTheme.colorScheme.primary
+                            } else {
+                                Color.Unspecified
+                            },
+                        )
+                    },
+                    trailingContent = {
+                        if (sorting == option) {
+                            Icon(Icons.Default.Done, null, tint = MaterialTheme.colorScheme.primary)
+                        }
+                    },
+                    modifier = Modifier.clickable {
+                        coroutineScope.launch {
+                            sheetState.hide()
+                        }.invokeOnCompletion {
+                            onDismissRequest()
+                        }
+                        onSortingChange(option)
+                    },
+                )
+            }
+        },
+    )
+}

+ 44 - 0
app/src/main/java/org/sirekanyan/outline/feature/sort/Sorting.kt

@@ -0,0 +1,44 @@
+package org.sirekanyan.outline.feature.sort
+
+import androidx.annotation.StringRes
+import org.sirekanyan.outline.R
+import org.sirekanyan.outline.api.model.Key
+
+enum class Sorting(val key: String, @StringRes val title: Int, val comparator: Comparator<Key>) {
+
+    ID(
+        key = "id",
+        title = R.string.outln_sorting_by_id,
+        comparator = compareBy { it.accessKey.id.toLongOrNull() },
+    ),
+
+    NAME(
+        key = "name",
+        title = R.string.outln_sorting_by_name,
+        comparator = compareBy({ it.accessKey.name.isEmpty() }, { it.accessKey.name.lowercase() }),
+    ),
+
+    TRAFFIC(
+        key = "traffic",
+        title = R.string.outln_sorting_by_traffic,
+        comparator = compareByDescending { it.traffic },
+    );
+
+    companion object {
+
+        init {
+            check(values().distinctBy(Sorting::key).size == values().size) { "Keys must be unique" }
+        }
+
+        const val KEY = "Sorting"
+        val DEFAULT = TRAFFIC
+
+        fun getByKey(key: String?): Sorting =
+            key?.let(::findByKey) ?: DEFAULT
+
+        private fun findByKey(key: String): Sorting? =
+            values().find { it.key == key }
+
+    }
+
+}

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

@@ -12,7 +12,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Icon
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.ListItem
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
-import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
@@ -31,15 +30,13 @@ fun KeyBottomSheet(
     onEditClick: () -> Unit,
     onEditClick: () -> Unit,
     onDeleteClick: () -> Unit,
     onDeleteClick: () -> Unit,
 ) {
 ) {
-    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
     val localClipboard = LocalClipboardManager.current
     val localClipboard = LocalClipboardManager.current
     val localContext = LocalContext.current
     val localContext = LocalContext.current
     val coroutineScope = rememberCoroutineScope()
     val coroutineScope = rememberCoroutineScope()
     SimpleBottomSheet(
     SimpleBottomSheet(
         title = key.accessKey.nameOrDefault,
         title = key.accessKey.nameOrDefault,
-        sheetState = sheetState,
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
-        items = {
+        items = { sheetState ->
             ListItem(
             ListItem(
                 headlineContent = { Text("Copy") },
                 headlineContent = { Text("Copy") },
                 leadingContent = { Icon(IconCopy, null) },
                 leadingContent = { Icon(IconCopy, null) },

+ 4 - 3
app/src/main/java/org/sirekanyan/outline/ui/SimpleBottomSheet.kt

@@ -10,6 +10,7 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.ModalBottomSheet
 import androidx.compose.material3.ModalBottomSheet
 import androidx.compose.material3.SheetState
 import androidx.compose.material3.SheetState
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.RectangleShape
@@ -19,12 +20,12 @@ import androidx.compose.ui.unit.dp
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 fun SimpleBottomSheet(
 fun SimpleBottomSheet(
     title: String,
     title: String,
-    sheetState: SheetState,
     onDismissRequest: () -> Unit,
     onDismissRequest: () -> Unit,
-    items: @Composable () -> Unit,
+    items: @Composable (SheetState) -> Unit,
 ) {
 ) {
     // insets should be kept outside of the modal bottom sheet to work properly
     // insets should be kept outside of the modal bottom sheet to work properly
     val insets = WindowInsets.navigationBars.asPaddingValues()
     val insets = WindowInsets.navigationBars.asPaddingValues()
+    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
     ModalBottomSheet(
     ModalBottomSheet(
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
         sheetState = sheetState,
         sheetState = sheetState,
@@ -34,7 +35,7 @@ fun SimpleBottomSheet(
     ) {
     ) {
         Column(Modifier.padding(insets).padding(top = 4.dp)) {
         Column(Modifier.padding(insets).padding(top = 4.dp)) {
             Text(title, Modifier.padding(16.dp), style = MaterialTheme.typography.labelLarge)
             Text(title, Modifier.padding(16.dp), style = MaterialTheme.typography.labelLarge)
-            items()
+            items(sheetState)
         }
         }
     }
     }
 }
 }