From e418a26f414c0d4cf63e26ab3f7b702868c87c8e Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Fri, 3 Oct 2025 16:26:20 +0100 Subject: [PATCH] Add support for copying/cloning follow sets in the UI. --- .../loggedIn/lists/CustomListsScreen.kt | 13 ++ .../ui/screen/loggedIn/lists/CustomSetItem.kt | 113 +++++++++++++++++- .../loggedIn/lists/FollowSetFeedView.kt | 6 + amethyst/src/main/res/values/strings.xml | 5 + 4 files changed, 134 insertions(+), 3 deletions(-) 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 5c7b1e47a..df36448dc 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 @@ -137,6 +137,14 @@ fun ListsAndSetsScreen( account = accountViewModel.account, ) }, + cloneItem = { followSet, customName, customDescription -> + followSetsViewModel.cloneFollowSet( + currentFollowSet = followSet, + customCloneName = customName, + customCloneDescription = customDescription, + account = accountViewModel.account, + ) + }, deleteItem = { followSet -> followSetsViewModel.deleteFollowSet( followSet = followSet, @@ -155,6 +163,7 @@ fun CustomListsScreen( addItem: (title: String, description: String?) -> Unit, openItem: (identifier: String) -> Unit, renameItem: (followSet: FollowSet, newName: String) -> Unit, + cloneItem: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, deleteItem: (followSet: FollowSet) -> Unit, accountViewModel: AccountViewModel, nav: INav, @@ -213,6 +222,7 @@ fun CustomListsScreen( onRefresh = refresh, onOpenItem = openItem, onRenameItem = renameItem, + onItemClone = cloneItem, onDeleteItem = deleteItem, ) @@ -372,6 +382,9 @@ private fun SetItemPreview() { onFollowSetRename = { println("Follow set new name: $it") }, + onFollowSetClone = { newName, newDesc -> + println("The follow set has been cloned, and has custom name: $newName, Desc: $newDesc") + }, onFollowSetDelete = { println(" The follow set ${sampleFollowSet.title} has been deleted.") }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt index 0d81b60ac..8569624f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/lists/CustomSetItem.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer @@ -67,6 +68,7 @@ fun CustomSetItem( followSet: FollowSet, onFollowSetClick: () -> Unit, onFollowSetRename: (String) -> Unit, + onFollowSetClone: (customName: String?, customDescription: String?) -> Unit, onFollowSetDelete: () -> Unit, ) { val context = LocalContext.current @@ -122,7 +124,7 @@ fun CustomSetItem( selected = true, onClick = {}, label = { - Text(text = "$publicMemberSize $membersLabel") + Text(text = "$publicMemberSize") }, leadingIcon = { Icon( @@ -149,7 +151,7 @@ fun CustomSetItem( selected = true, onClick = {}, label = { - Text(text = "$privateMemberSize $membersLabel") + Text(text = "$privateMemberSize") }, leadingIcon = { Icon( @@ -182,6 +184,7 @@ fun CustomSetItem( SetOptionsButton( followSetName = followSet.title, onListRename = onFollowSetRename, + onSetCloneCreate = onFollowSetClone, onListDelete = onFollowSetDelete, ) } @@ -189,10 +192,11 @@ fun CustomSetItem( } @Composable -fun SetOptionsButton( +private fun SetOptionsButton( modifier: Modifier = Modifier, followSetName: String, onListRename: (String) -> Unit, + onSetCloneCreate: (optionalName: String?, optionalDec: String?) -> Unit, onListDelete: () -> Unit, ) { val isMenuOpen = remember { mutableStateOf(false) } @@ -207,6 +211,7 @@ fun SetOptionsButton( isExpanded = isMenuOpen.value, onDismiss = { isMenuOpen.value = false }, onListRename = onListRename, + onSetClone = onSetCloneCreate, onDelete = onListDelete, ) } @@ -218,12 +223,17 @@ private fun SetOptionsMenu( isExpanded: Boolean, listName: String, onListRename: (String) -> Unit, + onSetClone: (optionalNewName: String?, optionalNewDesc: String?) -> Unit, onDelete: () -> Unit, onDismiss: () -> Unit, ) { val isRenameDialogOpen = remember { mutableStateOf(false) } val renameString = remember { mutableStateOf("") } + val isCopyDialogOpen = remember { mutableStateOf(false) } + val optionalCloneName = remember { mutableStateOf(null) } + val optionalCloneDescription = remember { mutableStateOf(null) } + DropdownMenu( expanded = isExpanded, onDismissRequest = onDismiss, @@ -245,6 +255,15 @@ private fun SetOptionsMenu( onDismiss() }, ) + DropdownMenuItem( + text = { + Text(text = stringRes(R.string.follow_set_copy_action_btn_label)) + }, + onClick = { + isCopyDialogOpen.value = true + onDismiss() + }, + ) } if (isRenameDialogOpen.value) { @@ -260,6 +279,23 @@ private fun SetOptionsMenu( }, ) } + + if (isCopyDialogOpen.value) { + SetCloneDialog( + optionalNewName = optionalCloneName.value, + optionalNewDesc = optionalCloneDescription.value, + onCloneNameChange = { + optionalCloneName.value = it + }, + onCloneDescChange = { + optionalCloneDescription.value = it + }, + onCloneCreate = { name, description -> + onSetClone(optionalCloneName.value, optionalCloneDescription.value) + }, + onDismiss = { isCopyDialogOpen.value = false }, + ) + } } @Composable @@ -321,3 +357,74 @@ private fun RenameDialog( }, ) } + +@Composable +private fun SetCloneDialog( + modifier: Modifier = Modifier, + optionalNewName: String?, + optionalNewDesc: String?, + onCloneNameChange: (String?) -> Unit, + onCloneDescChange: (String?) -> Unit, + onCloneCreate: (customName: String?, customDescription: String?) -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringRes(R.string.follow_set_copy_dialog_title), + ) + } + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.follow_set_copy_indicator_description), + fontSize = 15.sp, + fontWeight = FontWeight.Light, + fontStyle = FontStyle.Italic, + ) + // For the set clone name + TextField( + value = optionalNewName ?: "", + onValueChange = onCloneNameChange, + label = { + Text(text = stringRes(R.string.follow_set_copy_name_label)) + }, + ) + Spacer(modifier = DoubleVertSpacer) + // For the set clone description + TextField( + value = optionalNewDesc ?: "", + onValueChange = onCloneDescChange, + label = { + Text(text = stringRes(R.string.follow_set_copy_desc_label)) + }, + ) + } + }, + confirmButton = { + Button( + onClick = { + onCloneCreate(optionalNewName, optionalNewDesc) + onDismiss() + }, + ) { + Text(stringRes(R.string.follow_set_copy_action_btn_label)) + } + }, + dismissButton = { + Button( + onClick = onDismiss, + ) { + Text(stringRes(R.string.cancel)) + } + }, + ) +} 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 index 1ecaca898..a90011f4c 100644 --- 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 @@ -51,6 +51,7 @@ fun FollowSetFeedView( onRefresh: () -> Unit = {}, onOpenItem: (String) -> Unit = {}, onRenameItem: (targetSet: FollowSet, newName: String) -> Unit, + onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, onDeleteItem: (followSet: FollowSet) -> Unit, ) { when (followSetFeedState) { @@ -63,6 +64,7 @@ fun FollowSetFeedView( onRefresh = onRefresh, onItemClick = onOpenItem, onItemRename = onRenameItem, + onItemClone = onItemClone, onItemDelete = onDeleteItem, ) } @@ -91,6 +93,7 @@ fun FollowSetLoaded( onRefresh: () -> Unit = {}, onItemClick: (itemIdentifier: String) -> Unit = {}, onItemRename: (followSet: FollowSet, newName: String) -> Unit, + onItemClone: (followSet: FollowSet, customName: String?, customDesc: String?) -> Unit, onItemDelete: (followSet: FollowSet) -> Unit, ) { Log.d("FollowSetComposable", "FollowSetLoaded: Follow Set size: ${loadedFeedState.size}") @@ -113,6 +116,9 @@ fun FollowSetLoaded( onFollowSetRename = { onItemRename(set, it) }, + onFollowSetClone = { cloneName, cloneDescription -> + onItemClone(set, cloneName, cloneDescription) + }, onFollowSetDelete = { onItemDelete(set) }, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6ff31fe76..a4743acf1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -534,9 +534,14 @@ New list with %1$s membership Creates a new follow set, and adds %1$s as a %2$s member. New Follow Set + Copy/Clone Follow Set + You can set a custom name/description for this clone set below. Set name + (Original Set name) Set description(optional) + (Original Set description) Create set + Copy/Clone set Rename set You are renaming from to..