From 5a6cfad8485ceb57195b2614d88cf8c822a48917 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Wed, 30 Apr 2025 17:11:28 +0100 Subject: [PATCH] Separate components out into their logical groups(by file). Add new icons for public/private list creation, and add FloatingActionButtons for each. Add test code for iterating on changes quickly(TEMPORARY). --- .../screen/loggedIn/lists/CustomListItem.kt | 271 +++++++++++++++++ .../loggedIn/lists/CustomListsScreen.kt | 286 +++--------------- .../loggedIn/lists/FollowSetFeedView.kt | 139 +++++++++ .../profile/header/FollowSetsActionMenu.kt | 1 - amethyst/src/main/res/drawable/earth_plus.xml | 1 + amethyst/src/main/res/drawable/lock_plus.xml | 1 + 6 files changed, 462 insertions(+), 237 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListItem.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt create mode 100644 amethyst/src/main/res/drawable/earth_plus.xml create mode 100644 amethyst/src/main/res/drawable/lock_plus.xml diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListItem.kt new file mode 100644 index 000000000..a407a4341 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListItem.kt @@ -0,0 +1,271 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.People +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.components.ClickableBox +import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun CustomListItem( + modifier: Modifier = Modifier, + followSet: FollowSet, + onFollowSetClick: () -> Unit, + onFollowSetRename: (String) -> Unit, + onFollowSetDelete: () -> Unit, +) { + Row( + modifier = + modifier + .clickable(onClick = onFollowSetClick) + .border( + width = Dp.Hairline, + color = Color.Gray, + shape = RoundedCornerShape(percent = 20), + ).padding(horizontal = 10.dp), + ) { + Row( + modifier = + Modifier + .padding(bottom = 12.dp) + .weight(1f), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text(followSet.title, fontWeight = FontWeight.Bold) + Spacer(modifier = StdHorzSpacer) + FilterChip( + selected = true, + onClick = {}, + label = { + Text(text = "${followSet.profileList.size}") + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.People, + contentDescription = null, + ) + }, + shape = ButtonBorder, + ) + } + Spacer(modifier = StdVertSpacer) + Text( + followSet.description ?: "", + fontWeight = FontWeight.Light, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + ) + } + + followSet.visibility.let { + val text by derivedStateOf { + when (it) { + ListVisibility.Public -> "Public" + ListVisibility.Private -> "Private" + ListVisibility.Mixed -> "Mixed" + } + } + Column( + modifier = Modifier.padding(top = 15.dp), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = + painterResource( + when (it) { + ListVisibility.Public -> R.drawable.ic_public + ListVisibility.Private -> R.drawable.lock + ListVisibility.Mixed -> R.drawable.format_list_bulleted_type + }, + ), + contentDescription = "Icon for $text List", + ) + Text(text, color = Color.Gray, fontWeight = FontWeight.Light) + } + } + } + + Column( + modifier = + Modifier + .padding(start = 5.dp) + .padding(vertical = 7.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.End, + ) { + ListOptionsButton( + followSetName = followSet.title, + onListRename = onFollowSetRename, + onListDelete = onFollowSetDelete, + ) + } + } +} + +@Composable +fun ListOptionsButton( + modifier: Modifier = Modifier, + followSetName: String, + onListRename: (String) -> Unit, + onListDelete: () -> Unit, +) { + val isMenuOpen = remember { mutableStateOf(false) } + + ClickableBox( + onClick = { isMenuOpen.value = true }, + ) { + VerticalDotsIcon() + + ListOptionsMenu( + listName = followSetName, + isExpanded = isMenuOpen.value, + onDismiss = { isMenuOpen.value = false }, + onListRename = onListRename, + onDelete = onListDelete, + ) + } +} + +@Composable +fun ListOptionsMenu( + modifier: Modifier = Modifier, + isExpanded: Boolean, + listName: String, + onListRename: (String) -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit, +) { + val isRenameDialogOpen = remember { mutableStateOf(false) } + val renameString = remember { mutableStateOf("") } + + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + ) { + DropdownMenuItem( + text = { + Text(text = "Delete") + }, + onClick = { + println("The list named $listName has been selected for deletion.") + onDelete() + }, + ) + DropdownMenuItem( + text = { + Text(text = "Rename list") + }, + onClick = { + println("The list $listName should be renamed...") + isRenameDialogOpen.value = true + onDismiss() + }, + ) + } + + if (isRenameDialogOpen.value) { + RenameDialog( + renameString = renameString.value, + onStringRenameChange = { + renameString.value = it + }, + onDismissDialog = { isRenameDialogOpen.value = false }, + onListRename = { + onListRename(renameString.value) + }, + ) + } +} + +@Composable +fun RenameDialog( + modifier: Modifier = Modifier, + renameString: String, + onStringRenameChange: (String) -> Unit, + onDismissDialog: () -> Unit, + onListRename: (String) -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissDialog, + title = { + Text(text = "Rename List") + }, + text = { + TextField( + value = renameString, + onValueChange = onStringRenameChange, + ) + }, + confirmButton = { + Button( + onClick = { + onListRename(renameString) + onDismissDialog() + }, + ) { Text(text = "Rename") } + }, + dismissButton = { + Button(onClick = onDismissDialog) { Text(text = "Cancel") } + }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt index 870d95d47..ab0be6a24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomListsScreen.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists -import android.util.Log -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,17 +27,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.People -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FilterChip +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab @@ -48,20 +38,14 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -69,21 +53,12 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.components.ClickableBox -import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty -import com.vitorpamplona.amethyst.ui.feeds.FeedError -import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed -import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton -import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.screen.NostrUserListFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn @@ -120,10 +95,6 @@ fun ListsScreen( val followSetsFlow by followSetsViewModel.feedContent.collectAsStateWithLifecycle() - LaunchedEffect(followSetsFlow) { - followSetsViewModel.invalidateData() - } - CustomListsScreen( followSetsFlow, refresh = { @@ -140,6 +111,12 @@ fun ListsScreen( } } }, + renameItem = { index, newValue -> + followSetsViewModel.renameFollowSet(newName = newValue, setIndex = index) + }, + deleteItem = { + followSetsViewModel.deleteFollowSet(it) + }, accountViewModel, nav, ) @@ -150,6 +127,8 @@ fun CustomListsScreen( followSetState: FollowSetState, refresh: () -> Unit, openItem: (identifier: String) -> Unit, + renameItem: (Int, String) -> Unit, + deleteItem: (Int) -> Unit, accountViewModel: AccountViewModel, nav: INav, ) { @@ -172,23 +151,24 @@ fun CustomListsScreen( Tab( selected = pagerState.currentPage == 0, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } }, - text = { Text(text = "Follow Sets", overflow = TextOverflow.Ellipsis, maxLines = 1) }, + text = { Text(text = "Follow Sets", overflow = TextOverflow.Visible) }, ) Tab( selected = pagerState.currentPage == 1, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, - text = { Text(text = "Labeled Bookmarks", overflow = TextOverflow.Ellipsis, maxLines = 1) }, + text = { Text(text = "Labeled Bookmarks", overflow = TextOverflow.Visible) }, ) Tab( selected = pagerState.currentPage == 2, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } }, - text = { Text(text = "General Bookmarks", overflow = TextOverflow.Ellipsis, maxLines = 1) }, + text = { Text(text = "General Bookmarks", overflow = TextOverflow.Visible) }, ) } } }, floatingButton = { - // TODO: Add buttons for list creation and/or removal. + // TODO: Show components based on current tab + FollowSetFabsAndMenu() }, ) { Column( @@ -203,6 +183,8 @@ fun CustomListsScreen( followSetState = followSetState, onRefresh = refresh, onOpenItem = openItem, + onRenameItem = renameItem, + onDeleteItem = deleteItem, ) 1 -> LabeledBookmarksFeedView() @@ -214,36 +196,36 @@ fun CustomListsScreen( } @Composable -fun FollowSetFeedView( - modifier: Modifier = Modifier, - followSetState: FollowSetState, - onRefresh: () -> Unit = {}, - onOpenItem: (String) -> Unit = {}, -) { - when (followSetState) { - FollowSetState.Loading -> LoadingFeed() - - is FollowSetState.Loaded -> { - val followSetFeed = followSetState.feed - FollowListLoaded( - loadedFeedState = followSetFeed, - onRefresh = onRefresh, - onItemClick = onOpenItem, +fun FollowSetFabsAndMenu(modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + FloatingActionButton( + onClick = { + println("The private list addition...") + }, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + painter = painterResource(R.drawable.lock_plus), + contentDescription = null, + tint = Color.White, ) } - - is FollowSetState.Empty -> { - FeedEmpty { - onRefresh() - } + FloatingActionButton( + onClick = { + println("The public list creation...") + }, + shape = CircleShape, + containerColor = MaterialTheme.colorScheme.primary, + ) { + Icon( + painter = painterResource(R.drawable.earth_plus), + contentDescription = null, + tint = Color.White, + ) } - - is FollowSetState.FeedError -> - FeedError( - followSetState.errorMessage, - ) { - onRefresh() - } } } @@ -271,180 +253,6 @@ fun GeneralBookmarksFeedView() { } } -@Composable -fun FollowListLoaded( - modifier: Modifier = Modifier, - loadedFeedState: List, - onRefresh: () -> Unit = {}, - onItemClick: (String) -> Unit = {}, -) { - Log.d("FollowSetComposable", "FollowListLoaded: Follow Set size: ${loadedFeedState.size}") - - val listState = rememberLazyListState() - RefresheableBox( - onRefresh = onRefresh, - ) { - LazyColumn( - state = listState, - contentPadding = FeedPadding, - ) { - itemsIndexed(loadedFeedState, key = { _, item -> item.identifierTag }) { index, set -> - CustomListItem( - followSet = set, - onFollowSetClick = { - onItemClick(set.identifierTag) - }, - ) - } - } - } -} - -@Composable -fun CustomListItem( - modifier: Modifier = Modifier, - followSet: FollowSet, - onFollowSetClick: () -> Unit, -) { - Row( - modifier = - modifier - .clickable(onClick = onFollowSetClick) - .border( - width = Dp.Hairline, - color = Color.Gray, - shape = RoundedCornerShape(percent = 20), - ).padding(horizontal = 10.dp), - ) { - Row( - modifier = - modifier - .padding(bottom = 12.dp) - .weight(1f), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = modifier.weight(1f), - verticalArrangement = Arrangement.Center, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text(followSet.title, fontWeight = FontWeight.Bold) - Spacer(modifier = StdHorzSpacer) - FilterChip( - selected = true, - onClick = {}, - label = { - Text(text = "${followSet.profileList.size}") - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.People, - contentDescription = null, - ) - }, - shape = ButtonBorder, - ) - } - Spacer(modifier = StdVertSpacer) - Text( - followSet.description ?: "", - fontWeight = FontWeight.Light, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - ) - } - - followSet.visibility.let { - val text by derivedStateOf { - when (it) { - ListVisibility.Public -> "Public" - ListVisibility.Private -> "Private" - ListVisibility.Mixed -> "Mixed" - } - } - Column( - modifier = modifier.padding(top = 15.dp), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = - painterResource( - when (it) { - ListVisibility.Public -> R.drawable.ic_public - ListVisibility.Private -> R.drawable.lock - ListVisibility.Mixed -> R.drawable.format_list_bulleted_type - }, - ), - contentDescription = "Icon for $text List", - ) - Text(text, color = Color.Gray, fontWeight = FontWeight.Light) - } - } - } - - Column( - modifier = modifier.padding(vertical = 7.dp), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.End, - ) { - ListOptionsButton(followSetName = followSet.title) - } - } -} - -@Composable -fun ListOptionsButton( - modifier: Modifier = Modifier, - followSetName: String, -) { - val isMenuOpen = remember { mutableStateOf(false) } - - ClickableBox( - onClick = { isMenuOpen.value = true }, - ) { - VerticalDotsIcon() - - ListOptionsMenu( - listName = followSetName, - isExpanded = isMenuOpen.value, - onDismiss = { isMenuOpen.value = false }, - ) - } -} - -@Composable -fun ListOptionsMenu( - modifier: Modifier = Modifier, - isExpanded: Boolean, - listName: String, - onDismiss: () -> Unit, -) { - DropdownMenu( - expanded = isExpanded, - onDismissRequest = onDismiss, - ) { - DropdownMenuItem( - text = { - Text(text = "Delete") - }, - onClick = { - println("The list named $listName has been selected for deletion.") - }, - ) - DropdownMenuItem( - text = { - Text(text = "Rename list") - }, - onClick = { - println("The list $listName should be renamed...") - }, - ) - } -} - @Preview(showSystemUi = true) @Composable private fun ListItemPreview() { @@ -463,6 +271,12 @@ private fun ListItemPreview() { onFollowSetClick = { println("follow set: ${sampleFollowSet.identifierTag}") }, + onFollowSetRename = { + println("Follow set new name: $it") + }, + onFollowSetDelete = { + println(" The follow set ${sampleFollowSet.title} has been deleted.") + }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt new file mode 100644 index 000000000..5378b9796 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/FollowSetFeedView.kt @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.feeds.FeedError +import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed +import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun FollowSetFeedView( + modifier: Modifier = Modifier, + followSetState: FollowSetState, + onRefresh: () -> Unit = {}, + onOpenItem: (String) -> Unit = {}, + onRenameItem: (Int, String) -> Unit, + onDeleteItem: (Int) -> Unit, +) { + when (followSetState) { + FollowSetState.Loading -> LoadingFeed() + + is FollowSetState.Loaded -> { + val followSetFeed = followSetState.feed + FollowListLoaded( + loadedFeedState = followSetFeed, + onRefresh = onRefresh, + onItemClick = onOpenItem, + onItemRename = onRenameItem, + onItemDelete = onDeleteItem, + ) + } + + is FollowSetState.Empty -> { + FollowListFeedEmpty( + message = + "It seems you do not have any follow lists yet.\n " + + "Tap below to refresh, or tap the add buttons to create a new one.", + ) { + onRefresh() + } + } + + is FollowSetState.FeedError -> + FeedError( + followSetState.errorMessage, + ) { + onRefresh() + } + } +} + +@Composable +fun FollowListLoaded( + modifier: Modifier = Modifier, + loadedFeedState: List, + onRefresh: () -> Unit = {}, + onItemClick: (String) -> Unit = {}, + onItemRename: (index: Int, newName: String) -> Unit, + onItemDelete: (itemIndex: Int) -> Unit, +) { + Log.d("FollowSetComposable", "FollowListLoaded: Follow Set size: ${loadedFeedState.size}") + + val listState = rememberLazyListState() + RefresheableBox( + onRefresh = onRefresh, + ) { + LazyColumn( + state = listState, + contentPadding = FeedPadding, + ) { + itemsIndexed(loadedFeedState, key = { _, item -> item.identifierTag }) { index, set -> + CustomListItem( + modifier = Modifier.animateItem(), + followSet = set, + onFollowSetClick = { + onItemClick(set.identifierTag) + }, + onFollowSetRename = { + onItemRename(index, it) + }, + onFollowSetDelete = { + onItemDelete(index) + }, + ) + Spacer(modifier = StdVertSpacer) + } + } + } +} + +@Composable +fun FollowListFeedEmpty( + message: String = stringRes(R.string.feed_is_empty), + onRefresh: () -> Unit, +) { + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(message) + Spacer(modifier = StdVertSpacer) + OutlinedButton(onClick = onRefresh) { Text(text = stringRes(R.string.refresh)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/FollowSetsActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/FollowSetsActionMenu.kt index 6366643d6..cc73aa866 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/FollowSetsActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/FollowSetsActionMenu.kt @@ -181,7 +181,6 @@ fun generateFollowLists(): List = visibility = when { index % 2 == 0 -> ListVisibility.Private - index in listOf(3, 7, 9) -> ListVisibility.Mixed else -> ListVisibility.Public }, profileList = emptySet(), diff --git a/amethyst/src/main/res/drawable/earth_plus.xml b/amethyst/src/main/res/drawable/earth_plus.xml new file mode 100644 index 000000000..2c3963e89 --- /dev/null +++ b/amethyst/src/main/res/drawable/earth_plus.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/amethyst/src/main/res/drawable/lock_plus.xml b/amethyst/src/main/res/drawable/lock_plus.xml new file mode 100644 index 000000000..ca8a3cbdc --- /dev/null +++ b/amethyst/src/main/res/drawable/lock_plus.xml @@ -0,0 +1 @@ + \ No newline at end of file