From e57dccb014de24f32421b12a5214c6316c3083b6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Apr 2026 14:56:04 +0200 Subject: [PATCH 1/9] feat(unified-share): ui Signed-off-by: alperozturk96 --- .../fragment/FileDetailSharingFragment.java | 30 +- .../ui/fragment/share/UnifiedShareView.kt | 459 ++++++++++++++++++ .../layout/file_details_sharing_fragment.xml | 7 + 3 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index 834278b3b231..91656a86b75a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -70,6 +70,7 @@ import com.owncloud.android.ui.dialog.SharePasswordDialogFragment; import com.owncloud.android.ui.fragment.share.RemoteShareRepository; import com.owncloud.android.ui.fragment.share.ShareRepository; +import com.owncloud.android.ui.fragment.share.UnifiedShareViewKt; import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper; import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.utils.ClipboardUtil; @@ -92,6 +93,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import kotlin.Unit; +import kotlin.jvm.functions.Function0; public class FileDetailSharingFragment extends Fragment implements ShareeListAdapterListener, DisplayUtils.AvatarGenerationListener, @@ -162,7 +164,33 @@ public void onCreate(@Nullable Bundle savedInstanceState) { } fileDataStorageManager = fileActivity.getStorageManager(); - fetchSharees(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // TODO: REPLACE FAKE CONDITION + if (user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.nextcloud_34) || 2 < 4) { + showUnifiedShare(); + } else { + fetchSharees(); + } + } + + private void showUnifiedShare() { + if (binding == null) { + return; + } + + binding.shareContainer.setVisibility(View.GONE); + binding.unifiedShare.setVisibility(View.VISIBLE); + + final LinearLayout shimmerLayout = binding.shimmerLayout.getRoot(); + shimmerLayout.clearAnimation(); + shimmerLayout.setVisibility(View.GONE); + + UnifiedShareViewKt.setupUnifiedShare(binding.unifiedShare, viewThemeUtils, requireContext()); } private void fetchSharees() { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt new file mode 100644 index 000000000000..12c2993e62f3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -0,0 +1,459 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment.share + +import android.content.Context +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp +import com.owncloud.android.utils.theme.ViewThemeUtils + +enum class UnifiedShareCategory { + Invited, Anyone +} + +enum class UnifiedShareType { + InternalUser, InternalGroup, InternalLink, ExternalLink, ExternalFederated, ExternalMail; + + fun icon(): String { + return when (this) { + InternalUser -> "👤" + InternalGroup -> "👥" + InternalLink -> "🔗" + ExternalLink -> "🌍" + ExternalFederated -> "☁️" + ExternalMail -> "📧" + } + } +} + +data class UnifiedShareDownloadLimit( + val limit: Int, + val downloadCount: Int +) + +sealed class UnifiedSharePermission { + // file drop only for folder + data object FileDrop : UnifiedSharePermission() + + data object CanView : UnifiedSharePermission() + data object CanEdit : UnifiedSharePermission() + + // create only for folder + data class Custom(val read: Boolean, val edit: Boolean, val delete: Boolean, val create: Boolean) : + UnifiedSharePermission() + + fun getText(): String { + return when(this) { + FileDrop -> "FileDrop" + CanView -> "CanView" + CanEdit -> "CanEdit" + is Custom -> "Custom permissions" + } + } +} + +data class UnifiedShares( + val id: Int, + val password: String, + val note: String, + val limit: UnifiedShareDownloadLimit, + val expirationDate: Int, + val permission: UnifiedSharePermission, + val label: String, + val sharedTo: String, + val type: UnifiedShareType, + val category: UnifiedShareCategory, +) + +// TODO: MOVE TO THE ANDROID: COMMON +// TODO: MAKE LAZY COLUMN +// TODO: EXPOSE ACTIONS, IMPLEMENT VIEWMODEL, REPOSITORY TO FETCH ACTUAL SHARE, INJECT NECESSARY PARAMETERS + +@Composable +fun UnifiedShareView() { + var showAddShare by remember { mutableStateOf(false) } + + val mockUnifiedShares = listOf( + UnifiedShares( + id = 1, + password = "", + note = "Design review – please check latest changes", + limit = UnifiedShareDownloadLimit( + limit = 100, + downloadCount = 12 + ), + expirationDate = 0, + permission = UnifiedSharePermission.CanView, + label = "Alice Johnson", + sharedTo = "alice@company.com", + type = UnifiedShareType.InternalUser, + category = UnifiedShareCategory.Invited + ), + + UnifiedShares( + id = 2, + password = "", + note = "", + limit = UnifiedShareDownloadLimit( + limit = 0, + downloadCount = 0 + ), + expirationDate = 0, + permission = UnifiedSharePermission.CanEdit, + label = "Marketing Team", + sharedTo = "marketing", + type = UnifiedShareType.InternalGroup, + category = UnifiedShareCategory.Invited + ), + + UnifiedShares( + id = 3, + password = "1234", + note = "Public link for client review", + limit = UnifiedShareDownloadLimit( + limit = 50, + downloadCount = 5 + ), + expirationDate = 1710000000, + permission = UnifiedSharePermission.Custom( + read = true, + edit = false, + delete = false, + create = false + ), + label = "Public Link", + sharedTo = "https://nextcloud.com/s/abc123", + type = UnifiedShareType.InternalLink, + category = UnifiedShareCategory.Anyone + ), + + UnifiedShares( + id = 4, + password = "", + note = "External partner access", + limit = UnifiedShareDownloadLimit( + limit = 20, + downloadCount = 2 + ), + expirationDate = 0, + permission = UnifiedSharePermission.CanView, + label = "John External", + sharedTo = "john@external.com", + type = UnifiedShareType.ExternalMail, + category = UnifiedShareCategory.Anyone + ), + + UnifiedShares( + id = 5, + password = "", + note = "Federated sharing with partner instance", + limit = UnifiedShareDownloadLimit( + limit = 0, + downloadCount = 0 + ), + expirationDate = 0, + permission = UnifiedSharePermission.FileDrop, + label = "Partner Cloud", + sharedTo = "partner@nextcloud.org", + type = UnifiedShareType.ExternalFederated, + category = UnifiedShareCategory.Anyone + ) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + mockUnifiedShares.forEachIndexed { index, share -> + val type = when (index) { + 0 -> { + UnifiedSharesListItemType.Top + } + + mockUnifiedShares.lastIndex -> { + UnifiedSharesListItemType.Bottom + } + + else -> { + UnifiedSharesListItemType.Mid + } + } + + UnifiedSharesListItem(share, type) + } + + FloatingActionButton( + onClick = { showAddShare = true }, + modifier = Modifier + .align(Alignment.End) + .padding(top = 16.dp) + ) { + Icon(Icons.Default.Add, contentDescription = "Add") + } + + if (showAddShare) { + AddShareBottomSheet("Abc.txt",onDismiss = { showAddShare = false }) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { + val sheetState = rememberModalBottomSheetState() + + var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } + var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } + var categoryDropDownExpanded by remember { mutableStateOf(false) } + var permissionDropDownExpanded by remember { mutableStateOf(false) } + val availablePermissions = remember { + listOf( + UnifiedSharePermission.CanView, + UnifiedSharePermission.CanEdit, + UnifiedSharePermission.FileDrop + ) + } + var searchQuery by remember { mutableStateOf("") } + var note by remember { mutableStateOf("") } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 48.dp) // Extra padding for bottom navigation bars + ) { + Text( + text = "Share $filename", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(24.dp)) + + ExposedDropdownMenuBox( + expanded = categoryDropDownExpanded, + onExpandedChange = { categoryDropDownExpanded = !categoryDropDownExpanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = category.name, + onValueChange = {}, + readOnly = true, + label = { Text("Share type") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryDropDownExpanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = categoryDropDownExpanded, + onDismissRequest = { categoryDropDownExpanded = false } + ) { + UnifiedShareCategory.entries.forEach { selectionOption -> + DropdownMenuItem( + text = { Text(selectionOption.name) }, + onClick = { + category = selectionOption + categoryDropDownExpanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (category == UnifiedShareCategory.Invited) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Add people") }, + placeholder = { Text("Name, team, email or federated cloud ID") }, + singleLine = true, + shape = RoundedCornerShape(8.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ExposedDropdownMenuBox( + expanded = permissionDropDownExpanded, + onExpandedChange = { permissionDropDownExpanded = !permissionDropDownExpanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = permission.getText(), + onValueChange = {}, + readOnly = true, + label = { Text("Participants") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = permissionDropDownExpanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier.menuAnchor().fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = permissionDropDownExpanded, + onDismissRequest = { permissionDropDownExpanded = false } + ) { + availablePermissions.forEach { selectionOption -> + DropdownMenuItem( + text = { Text(selectionOption.getText()) }, + onClick = { + // permission = selectionOption + permissionDropDownExpanded = false + } + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = note, + onValueChange = { note = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Note to recipients") }, + singleLine = true, + shape = RoundedCornerShape(8.dp) + ) + } else { + Text( + text = "Creating a public link will allow anyone with the link to access this file.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Button( + onClick = { /* TODO: Create Public Link Logic */ }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) { + Text("Create public link") + } + } + + Row(modifier = Modifier.fillMaxWidth()) { + FilledTonalIconButton(onClick = { + + }) { + Text("Copy link") + } + + Spacer(modifier = Modifier.width(16.dp)) + + FilledTonalIconButton(onClick = { + + }) { + Text("Send") + } + } + } + } +} +enum class UnifiedSharesListItemType { + Top, Mid, Bottom; + + @Composable + fun getShape(): RoundedCornerShape { + return when (this) { + Top -> RoundedCornerShape(12.dp, 12.dp, 4.dp, 4.dp) + Mid -> RoundedCornerShape(4.dp, 4.dp, 4.dp, 4.dp) + Bottom -> RoundedCornerShape(4.dp, 4.dp, 12.dp, 12.dp) + } + } +} + +@Composable +private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clip(type.getShape()) + .clickable( + onClick = { } + ) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), + headlineContent = { + Text( + text = share.label, + style = MaterialTheme.typography.titleSmall + ) + }, + supportingContent = { + Text( + text = share.sharedTo, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent + ) + ) +} + +fun ComposeView.setupUnifiedShare(viewThemeUtils: ViewThemeUtils, context: Context) { + setContent { + MaterialTheme( + colorScheme = viewThemeUtils.getColorScheme(context), + content = { + UnifiedShareView() + } + ) + } +} diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index 14a056da8f5c..a03e8e26ba39 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -231,10 +231,17 @@ android:text="@string/show_all" /> + + + From 03a406414eab3100312951b6cdfa37787475f16e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Apr 2026 15:15:26 +0200 Subject: [PATCH 2/9] feat(unified-share): ui Signed-off-by: alperozturk96 --- .../ui/fragment/share/UnifiedShareView.kt | 422 +++++++++++++----- 1 file changed, 301 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 12c2993e62f3..9ac4b0fc71df 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -18,7 +18,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Button @@ -27,15 +29,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -88,9 +89,9 @@ sealed class UnifiedSharePermission { fun getText(): String { return when(this) { - FileDrop -> "FileDrop" - CanView -> "CanView" - CanEdit -> "CanEdit" + FileDrop -> "File drop" + CanView -> "Can view" + CanEdit -> "Can edit" is Custom -> "Custom permissions" } } @@ -246,21 +247,27 @@ fun UnifiedShareView() { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scrollState = rememberScrollState() var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } - var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } - var categoryDropDownExpanded by remember { mutableStateOf(false) } - var permissionDropDownExpanded by remember { mutableStateOf(false) } + var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } + var searchQuery by remember { mutableStateOf("") } + var note by remember { mutableStateOf("") } + + var viewFiles by remember { mutableStateOf(false) } + var editFiles by remember { mutableStateOf(false) } + var createFiles by remember { mutableStateOf(false) } + var deleteFiles by remember { mutableStateOf(false) } + val availablePermissions = remember { listOf( UnifiedSharePermission.CanView, UnifiedSharePermission.CanEdit, - UnifiedSharePermission.FileDrop + UnifiedSharePermission.FileDrop, + UnifiedSharePermission.Custom(false, false, false, false) ) } - var searchQuery by remember { mutableStateOf("") } - var note by remember { mutableStateOf("") } ModalBottomSheet( onDismissRequest = onDismiss, @@ -271,140 +278,313 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .padding(bottom = 48.dp) // Extra padding for bottom navigation bars + .padding(bottom = 32.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = "Share $filename", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface + ShareBottomSheetHeader(filename) + + ShareCategoryDropdown( + selectedCategory = category, + onCategoryChange = { category = it } ) - Spacer(modifier = Modifier.height(24.dp)) + if (category == UnifiedShareCategory.Invited) { + InvitedShareContent( + searchQuery = searchQuery, + onSearchChange = { searchQuery = it }, + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = { permission = it }, + ) + + InvitedInlineSettings() - ExposedDropdownMenuBox( - expanded = categoryDropDownExpanded, - onExpandedChange = { categoryDropDownExpanded = !categoryDropDownExpanded }, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = category.name, - onValueChange = {}, - readOnly = true, - label = { Text("Share type") }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryDropDownExpanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier - .menuAnchor() - .fillMaxWidth() + NoteToRecipients(note = note, onNoteChange = { note = it }) + } else { + AnyoneShareContent( + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = { permission = it }, ) - ExposedDropdownMenu( - expanded = categoryDropDownExpanded, - onDismissRequest = { categoryDropDownExpanded = false } - ) { - UnifiedShareCategory.entries.forEach { selectionOption -> - DropdownMenuItem( - text = { Text(selectionOption.name) }, - onClick = { - category = selectionOption - categoryDropDownExpanded = false - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding - ) - } + if (permission is UnifiedSharePermission.Custom) { + SettingsSwitchRow("View files", viewFiles) { viewFiles = it } + SettingsSwitchRow("Edit files", editFiles) { editFiles = it } + SettingsSwitchRow("Create files", createFiles) { createFiles = it } + SettingsSwitchRow("Delete files", deleteFiles) { deleteFiles = it } } + + AnyoneInlineSettings() + + NoteToRecipients(note = note, onNoteChange = { note = it }) } - Spacer(modifier = Modifier.height(16.dp)) + ShareActionButtons( + category = category, + isSendEnabled = searchQuery.isNotBlank(), + onCopyClick = { /* TODO */ }, + onSendClick = { /* TODO */ } + ) + } + } +} - if (category == UnifiedShareCategory.Invited) { - OutlinedTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Add people") }, - placeholder = { Text("Name, team, email or federated cloud ID") }, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) +@Composable +private fun ShareBottomSheetHeader(filename: String) { + Text( + text = "Share $filename", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) +} - Spacer(modifier = Modifier.height(16.dp)) - - ExposedDropdownMenuBox( - expanded = permissionDropDownExpanded, - onExpandedChange = { permissionDropDownExpanded = !permissionDropDownExpanded }, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = permission.getText(), - onValueChange = {}, - readOnly = true, - label = { Text("Participants") }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = permissionDropDownExpanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier.menuAnchor().fillMaxWidth() - ) - - ExposedDropdownMenu( - expanded = permissionDropDownExpanded, - onDismissRequest = { permissionDropDownExpanded = false } - ) { - availablePermissions.forEach { selectionOption -> - DropdownMenuItem( - text = { Text(selectionOption.getText()) }, - onClick = { - // permission = selectionOption - permissionDropDownExpanded = false - } - ) - } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShareCategoryDropdown( + selectedCategory: UnifiedShareCategory, + onCategoryChange: (UnifiedShareCategory) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = selectedCategory.name, + onValueChange = {}, + readOnly = true, + label = { Text("Share type") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + UnifiedShareCategory.entries.forEach { option -> + DropdownMenuItem( + text = { Text(option.name) }, + onClick = { + onCategoryChange(option) + expanded = false } - } + ) + } + } + } +} - Spacer(modifier = Modifier.height(16.dp)) +@Composable +private fun InvitedShareContent( + searchQuery: String, + onSearchChange: (String) -> Unit, + permission: UnifiedSharePermission, + availablePermissions: List, + onPermissionChange: (UnifiedSharePermission) -> Unit, + +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + OutlinedTextField( + value = searchQuery, + onValueChange = onSearchChange, + modifier = Modifier.fillMaxWidth(), + label = { Text("Add people") }, + placeholder = { Text("Name, team, email or federated ID") }, + singleLine = true, + shape = RoundedCornerShape(8.dp) + ) - OutlinedTextField( - value = note, - onValueChange = { note = it }, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text("Note to recipients") }, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - } else { - Text( - text = "Creating a public link will allow anyone with the link to access this file.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) - ) + PermissionDropdown( + label = "Participants", + selectedPermission = permission, + availablePermissions = availablePermissions, + onPermissionChange = onPermissionChange + ) + } +} - Button( - onClick = { /* TODO: Create Public Link Logic */ }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - Text("Create public link") - } +@Composable +private fun NoteToRecipients( + note: String, + onNoteChange: (String) -> Unit +) { + OutlinedTextField( + value = note, + onValueChange = onNoteChange, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Note to recipients") }, + shape = RoundedCornerShape(8.dp) + ) +} + +@Composable +private fun AnyoneShareContent( + permission: UnifiedSharePermission, + availablePermissions: List, + onPermissionChange: (UnifiedSharePermission) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + PermissionDropdown( + label = "Anyone with the link", + selectedPermission = permission, + availablePermissions = availablePermissions, + onPermissionChange = onPermissionChange + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PermissionDropdown( + label: String, + selectedPermission: UnifiedSharePermission, + availablePermissions: List, + onPermissionChange: (UnifiedSharePermission) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = selectedPermission.getText(), + onValueChange = {}, + readOnly = true, + label = { Text(label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + availablePermissions.forEach { option -> + DropdownMenuItem( + text = { Text(option.getText()) }, + onClick = { + onPermissionChange(option) + expanded = false + } + ) } + } + } +} - Row(modifier = Modifier.fillMaxWidth()) { - FilledTonalIconButton(onClick = { - }) { - Text("Copy link") - } +@Composable +private fun InvitedInlineSettings() { + var shareWithOthers by remember { mutableStateOf(false) } + var editFile by remember { mutableStateOf(false) } + var hasExpiration by remember { mutableStateOf(false) } + var hideDownload by remember { mutableStateOf(false) } + + Column { + SettingsSwitchRow("Share with others", shareWithOthers) { shareWithOthers = it } + SettingsSwitchRow("Edit file", editFile) { editFile = it } + SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } + SettingsSwitchRow("Hide download and sync options", hideDownload) { hideDownload = it } + } +} - Spacer(modifier = Modifier.width(16.dp)) +@Composable +private fun AnyoneInlineSettings() { + var hasPassword by remember { mutableStateOf(false) } + var hasExpiration by remember { mutableStateOf(false) } + var limitDownloads by remember { mutableStateOf(false) } + + var hideDownloads by remember { mutableStateOf(false) } + var videoVerification by remember { mutableStateOf(false) } + var showFilesInGridView by remember { mutableStateOf(false) } + + Column { + OutlinedTextField( + value = "", + onValueChange = {}, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + label = { Text("Label") }, + placeholder = { Text("Optional name for this link") }, + singleLine = true + ) - FilledTonalIconButton(onClick = { + SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } + SettingsSwitchRow("Password", hasPassword) { hasPassword = it } + SettingsSwitchRow("Limit downloads", limitDownloads) { limitDownloads = it } - }) { - Text("Send") - } + SettingsSwitchRow("Hide downloads", hideDownloads) { hideDownloads = it } + SettingsSwitchRow("Video verification", videoVerification) { videoVerification = it } + SettingsSwitchRow("Show files in grid view", showFilesInGridView) { showFilesInGridView = it } + + } +} + +@Composable +private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = label, style = MaterialTheme.typography.bodyLarge) + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} + +// --- ACTION BUTTONS --- + +@Composable +private fun ShareActionButtons( + category: UnifiedShareCategory, + isSendEnabled: Boolean, + onCopyClick: () -> Unit, + onSendClick: () -> Unit +) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp)) { + if (category == UnifiedShareCategory.Invited) { + FilledTonalButton( + onClick = onCopyClick, + modifier = Modifier.weight(1f) + ) { + Text("Copy link") + } + Spacer(modifier = Modifier.width(16.dp)) + Button( + onClick = onSendClick, + modifier = Modifier.weight(1f), + enabled = isSendEnabled // Disabled if search query is empty + ) { + Text("Send") + } + } else { + // For "Anyone" (Public link), usually just one big action to create/copy + Button( + onClick = onCopyClick, + modifier = Modifier.fillMaxWidth() + ) { + Text("Create public link") } } } } + enum class UnifiedSharesListItemType { Top, Mid, Bottom; From b3f554ab222cd49030514d97921656fb694e7d18 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:06:24 +0200 Subject: [PATCH 3/9] add todos Signed-off-by: alperozturk96 --- .../android/ui/fragment/share/UnifiedShareView.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 9ac4b0fc71df..01859705454b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -244,6 +244,13 @@ fun UnifiedShareView() { } } +// TODO: Instead of showing all options in the bottom sheet collect extra sharing options inside the expadable/collable sub-menu + +// TODO: Use conntected button group for invited and anyone type + +// TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. + + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { @@ -598,6 +605,13 @@ enum class UnifiedSharesListItemType { } } +// TODO: - Show avatar, email group or user in the leading content as a one rounded ICON +// TODO: - Replace supporting content with share permission but without editing option +// TODO: - Add right arrow icon end of the list item to access share detail and change share settings +// TODO: - Add context menu to access copy link, delete, and other actions u have with previous 3 dot menu + +// NOTE: To just create a public link anyone tab + just send DOES SAME THING + @Composable private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { ListItem( From 8ae279f095914599e9ec55db569d06f624227f9a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:27:21 +0200 Subject: [PATCH 4/9] add todos Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/share/UnifiedShareView.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 01859705454b..870ec507fde8 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -607,8 +607,9 @@ enum class UnifiedSharesListItemType { // TODO: - Show avatar, email group or user in the leading content as a one rounded ICON // TODO: - Replace supporting content with share permission but without editing option -// TODO: - Add right arrow icon end of the list item to access share detail and change share settings -// TODO: - Add context menu to access copy link, delete, and other actions u have with previous 3 dot menu +// TODO: - Add more icon end of the list item to access options we have before, delete, send email ... +// TODO: - Add context menu just does same thing like more icon +// TODO: - When user taps the list item it should show share detail bottom sheet // NOTE: To just create a public link anyone tab + just send DOES SAME THING From fa91fdd0449c05b0b95448b3c81b43042069e827 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:29:05 +0200 Subject: [PATCH 5/9] add todos Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/fragment/share/UnifiedShareView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 870ec507fde8..0dd36f154728 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -250,6 +250,7 @@ fun UnifiedShareView() { // TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. +// TODO: Replace FAB with person icon @OptIn(ExperimentalMaterial3Api::class) @Composable From 3b21b6ab13007b9eefe19fb746ab51b2cdf30a94 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:47:41 +0200 Subject: [PATCH 6/9] wip Signed-off-by: alperozturk96 --- app/build.gradle.kts | 1 + .../ui/fragment/share/UnifiedShareView.kt | 117 +++++++++++------- gradle/libs.versions.toml | 2 + 3 files changed, 77 insertions(+), 43 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d320cce6152..529b61b483ba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -339,6 +339,7 @@ dependencies { implementation(libs.compose.activity) implementation(libs.compose.ui.tooling.preview) implementation(libs.foundation) + implementation(libs.material3) debugImplementation(libs.compose.ui.tooling) // endregion diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 0dd36f154728..11d346569ae7 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -8,6 +8,7 @@ package com.owncloud.android.ui.fragment.share import android.content.Context +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -23,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -36,6 +40,9 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -235,7 +242,7 @@ fun UnifiedShareView() { .align(Alignment.End) .padding(top = 16.dp) ) { - Icon(Icons.Default.Add, contentDescription = "Add") + Icon(Icons.Default.Person, contentDescription = "Add") } if (showAddShare) { @@ -244,14 +251,8 @@ fun UnifiedShareView() { } } -// TODO: Instead of showing all options in the bottom sheet collect extra sharing options inside the expadable/collable sub-menu - -// TODO: Use conntected button group for invited and anyone type - // TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. -// TODO: Replace FAB with person icon - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { @@ -263,6 +264,10 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { var searchQuery by remember { mutableStateOf("") } var note by remember { mutableStateOf("") } + // Toggle states for collapse/expand + var showInvitedSettings by remember { mutableStateOf(false) } + var showAnyoneSettings by remember { mutableStateOf(false) } + var viewFiles by remember { mutableStateOf(false) } var editFiles by remember { mutableStateOf(false) } var createFiles by remember { mutableStateOf(false) } @@ -292,7 +297,7 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { ) { ShareBottomSheetHeader(filename) - ShareCategoryDropdown( + ShareCategoryButtonGroup( selectedCategory = category, onCategoryChange = { category = it } ) @@ -306,9 +311,12 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { onPermissionChange = { permission = it }, ) - InvitedInlineSettings() - - NoteToRecipients(note = note, onNoteChange = { note = it }) + CollapsibleSettingsSection( + isExpanded = showInvitedSettings, + onToggle = { showInvitedSettings = !showInvitedSettings } + ) { + InvitedInlineSettings() + } } else { AnyoneShareContent( permission = permission, @@ -323,11 +331,17 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { SettingsSwitchRow("Delete files", deleteFiles) { deleteFiles = it } } - AnyoneInlineSettings() - - NoteToRecipients(note = note, onNoteChange = { note = it }) + CollapsibleSettingsSection( + isExpanded = showAnyoneSettings, + onToggle = { showAnyoneSettings = !showAnyoneSettings } + ) { + AnyoneInlineSettings() + } } + NoteToRecipients(note = note, onNoteChange = { note = it }) + + ShareActionButtons( category = category, isSendEnabled = searchQuery.isNotBlank(), @@ -338,6 +352,41 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { } } +@Composable +private fun CollapsibleSettingsSection( + isExpanded: Boolean, + onToggle: () -> Unit, + content: @Composable () -> Unit +) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onToggle() } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Settings", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Icon( + imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + + AnimatedVisibility(visible = isExpanded) { + Column { + content() + } + } + } +} + @Composable private fun ShareBottomSheetHeader(filename: String) { Text( @@ -350,40 +399,23 @@ private fun ShareBottomSheetHeader(filename: String) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ShareCategoryDropdown( +private fun ShareCategoryButtonGroup( selectedCategory: UnifiedShareCategory, onCategoryChange: (UnifiedShareCategory) -> Unit ) { - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, + SingleChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth() ) { - OutlinedTextField( - value = selectedCategory.name, - onValueChange = {}, - readOnly = true, - label = { Text("Share type") }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier - .menuAnchor() - .fillMaxWidth() - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - UnifiedShareCategory.entries.forEach { option -> - DropdownMenuItem( - text = { Text(option.name) }, - onClick = { - onCategoryChange(option) - expanded = false - } + UnifiedShareCategory.entries.forEachIndexed { index, option -> + SegmentedButton( + selected = selectedCategory == option, + onClick = { onCategoryChange(option) }, + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = UnifiedShareCategory.entries.size ) + ) { + Text(option.name) } } } @@ -491,7 +523,6 @@ private fun PermissionDropdown( } } - @Composable private fun InvitedInlineSettings() { var shareWithOthers by remember { mutableStateOf(false) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 272f2bbf79ef..dca8c584298c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -85,6 +85,7 @@ stateless4jVersion = "2.6.0" webkitVersion = "1.15.0" workRuntime = "2.11.2" foundationVersion = "1.10.6" +material3Version = "1.4.0" [libraries] # Crypto @@ -240,6 +241,7 @@ qrcodescanner = { module = "com.github.nextcloud-deps:qrcodescanner", version.re work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" } work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntime" } foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundationVersion" } +material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3Version" } [bundles] media3 = ["media3-ui", "media3-session", "media3-exoplayer", "media3-datasource"] From fb0e68b8558abcf776a36db1b6ce40e51fa8e191 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:48:52 +0200 Subject: [PATCH 7/9] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/share/UnifiedShareView.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 11d346569ae7..a394cd7eba7b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem @@ -638,13 +639,11 @@ enum class UnifiedSharesListItemType { } // TODO: - Show avatar, email group or user in the leading content as a one rounded ICON -// TODO: - Replace supporting content with share permission but without editing option // TODO: - Add more icon end of the list item to access options we have before, delete, send email ... // TODO: - Add context menu just does same thing like more icon // TODO: - When user taps the list item it should show share detail bottom sheet // NOTE: To just create a public link anyone tab + just send DOES SAME THING - @Composable private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { ListItem( @@ -663,11 +662,14 @@ private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListI }, supportingContent = { Text( - text = share.sharedTo, + text = share.permission.getText(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) }, + trailingContent = { + Icon(Icons.Default.MoreVert, contentDescription = "More") + }, colors = ListItemDefaults.colors( containerColor = Color.Transparent ) From 6563046f5bccaf6a4c9755c0cba4dfb403cab5f6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:54:06 +0200 Subject: [PATCH 8/9] wip Signed-off-by: alperozturk96 --- .../ui/fragment/share/UnifiedShareView.kt | 78 ++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index a394cd7eba7b..9a6584ef4210 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -11,15 +11,19 @@ import android.content.Context import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -29,13 +33,16 @@ import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme @@ -56,7 +63,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import com.owncloud.android.utils.theme.ViewThemeUtils @@ -586,8 +595,6 @@ private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: } } -// --- ACTION BUTTONS --- - @Composable private fun ShareActionButtons( category: UnifiedShareCategory, @@ -638,22 +645,36 @@ enum class UnifiedSharesListItemType { } } -// TODO: - Show avatar, email group or user in the leading content as a one rounded ICON -// TODO: - Add more icon end of the list item to access options we have before, delete, send email ... -// TODO: - Add context menu just does same thing like more icon -// TODO: - When user taps the list item it should show share detail bottom sheet - // NOTE: To just create a public link anyone tab + just send DOES SAME THING @Composable private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { + var showContextMenu by remember { mutableStateOf(false) } + var showDetailSheet by remember { mutableStateOf(false) } + val haptics = LocalHapticFeedback.current + ListItem( modifier = Modifier .fillMaxWidth() .clip(type.getShape()) - .clickable( - onClick = { } + .combinedClickable( + onClick = { showDetailSheet = true }, + onLongClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + showContextMenu = true + }, ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), + leadingContent = { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center + ) { + Text(text = share.type.icon()) + } + }, headlineContent = { Text( text = share.label, @@ -668,12 +689,49 @@ private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListI ) }, trailingContent = { - Icon(Icons.Default.MoreVert, contentDescription = "More") + Box { + IconButton(onClick = { showContextMenu = true }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + + DropdownMenu( + expanded = showContextMenu, + onDismissRequest = { showContextMenu = false } + ) { + DropdownMenuItem( + text = { Text("Edit") }, + onClick = { + showContextMenu = false + showDetailSheet = true + } + ) + + DropdownMenuItem( + text = { Text("Send email") }, + onClick = { showContextMenu = false } + ) + + HorizontalDivider() + + DropdownMenuItem( + text = { Text("Delete", color = MaterialTheme.colorScheme.error) }, + onClick = { showContextMenu = false } + ) + } + } }, colors = ListItemDefaults.colors( containerColor = Color.Transparent ) ) + + // TODO: USE EXISTING SHARE DETAILS + if (showDetailSheet) { + AddShareBottomSheet( + filename = share.label, + onDismiss = { showDetailSheet = false } + ) + } } fun ComposeView.setupUnifiedShare(viewThemeUtils: ViewThemeUtils, context: Context) { From 1f6c7f37b4e9e54803d3df1498611341883e7930 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:54:22 +0200 Subject: [PATCH 9/9] wip Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/fragment/share/UnifiedShareView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 9a6584ef4210..0b14bea5f943 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert