From 2fc044e6694ec43e443e5bd5d9f30fc74e2f89d6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 20 Apr 2026 16:00:01 +0200 Subject: [PATCH 001/174] feat: unified share Signed-off-by: alperozturk96 --- gradle/verification-metadata.xml | 5 + material-color-utilities/build.gradle | 1 + ui/build.gradle | 3 + .../common/ui/share/UnifiedShareView.kt | 611 ++++++++++++++ .../common/ui/share/UnifiedShareViewModel.kt | 32 + .../ui/share/model/UnifiedShareCategory.kt | 12 + .../share/model/UnifiedShareDownloadLimit.kt | 13 + .../ui/share/model/UnifiedSharePermission.kt | 30 + .../common/ui/share/model/UnifiedShareType.kt | 31 + .../common/ui/share/model/UnifiedShares.kt | 21 + .../ui/share/previews/UnifiedSharePreviews.kt | 753 ++++++++++++++++++ .../repository/MockUnifiedShareRepository.kt | 105 +++ .../UnifiedShareRemoteRepository.kt | 16 + .../repository/UnifiedShareRepository.kt | 14 + ui/src/main/res/drawable/ic_circles.xml | 16 + ui/src/main/res/drawable/ic_email.xml | 16 + ui/src/main/res/drawable/ic_group.xml | 16 + ui/src/main/res/drawable/ic_link.xml | 16 + ui/src/main/res/drawable/ic_person_add.xml | 9 + ui/src/main/res/drawable/ic_talk.xml | 18 + ui/src/main/res/drawable/ic_user.xml | 16 + 21 files changed, 1754 insertions(+) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt create mode 100644 ui/src/main/res/drawable/ic_circles.xml create mode 100644 ui/src/main/res/drawable/ic_email.xml create mode 100644 ui/src/main/res/drawable/ic_group.xml create mode 100644 ui/src/main/res/drawable/ic_link.xml create mode 100644 ui/src/main/res/drawable/ic_person_add.xml create mode 100644 ui/src/main/res/drawable/ic_talk.xml create mode 100644 ui/src/main/res/drawable/ic_user.xml diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 35515104..7208210f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -336,6 +336,11 @@ + + + + + diff --git a/material-color-utilities/build.gradle b/material-color-utilities/build.gradle index 83e301dd..119c41b4 100644 --- a/material-color-utilities/build.gradle +++ b/material-color-utilities/build.gradle @@ -1,3 +1,4 @@ + /* * Nextcloud Android Common Library * diff --git a/ui/build.gradle b/ui/build.gradle index 213b129e..d04aa1d5 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -45,12 +45,15 @@ android { } dependencies { + implementation 'androidx.compose.ui:ui-tooling-preview:1.10.6' + debugImplementation 'androidx.compose.ui:ui-tooling:1.10.6' kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}" implementation(platform("androidx.compose:compose-bom:2026.08.00")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-core") implementation("com.vanniktech:ui:0.10.0") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt new file mode 100644 index 00000000..b31eb528 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt @@ -0,0 +1,611 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share + +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 +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.ColorScheme +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 +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 +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.UnifiedShares +import com.nextcloud.android.common.ui.share.repository.MockUnifiedShareRepository + + +// 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(viewModel: UnifiedShareViewModel) { + var showAddShare by remember { mutableStateOf(false) } + val shares by viewModel.shares.collectAsState() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + shares.forEachIndexed { index, share -> + val type = when (index) { + 0 -> { + UnifiedSharesListItemType.Top + } + + shares.lastIndex -> { + UnifiedSharesListItemType.Bottom + } + + else -> { + UnifiedSharesListItemType.Mid + } + } + + UnifiedSharesListItem(share, type) + } + + FloatingActionButton( + onClick = { showAddShare = true }, + modifier = Modifier + .align(Alignment.End) + .padding(top = 16.dp) + ) { + Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") + } + + if (showAddShare) { + AddShareBottomSheet("Abc.txt",onDismiss = { showAddShare = false }) + } + } +} + +// 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 +fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scrollState = rememberScrollState() + + var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } + var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } + 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) } + var deleteFiles by remember { mutableStateOf(false) } + + val availablePermissions = remember { + listOf( + UnifiedSharePermission.CanView, + UnifiedSharePermission.CanEdit, + UnifiedSharePermission.FileDrop, + UnifiedSharePermission.Custom(false, false, false, false) + ) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ShareBottomSheetHeader(filename) + + ShareCategoryButtonGroup( + selectedCategory = category, + onCategoryChange = { category = it } + ) + + if (category == UnifiedShareCategory.Invited) { + InvitedShareContent( + searchQuery = searchQuery, + onSearchChange = { searchQuery = it }, + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = { permission = it }, + ) + + CollapsibleSettingsSection( + isExpanded = showInvitedSettings, + onToggle = { showInvitedSettings = !showInvitedSettings } + ) { + InvitedInlineSettings() + } + } else { + AnyoneShareContent( + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = { permission = it }, + ) + + 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 } + } + + CollapsibleSettingsSection( + isExpanded = showAnyoneSettings, + onToggle = { showAnyoneSettings = !showAnyoneSettings } + ) { + AnyoneInlineSettings() + } + } + + NoteToRecipients(note = note, onNoteChange = { note = it }) + + + ShareActionButtons( + category = category, + isSendEnabled = searchQuery.isNotBlank(), + onCopyClick = { /* TODO */ }, + onSendClick = { /* TODO */ } + ) + } + } +} + +@Composable +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 +fun ShareBottomSheetHeader(filename: String) { + Text( + text = "Share $filename", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareCategoryButtonGroup( + selectedCategory: UnifiedShareCategory, + onCategoryChange: (UnifiedShareCategory) -> Unit +) { + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() + ) { + UnifiedShareCategory.entries.forEachIndexed { index, option -> + SegmentedButton( + selected = selectedCategory == option, + onClick = { onCategoryChange(option) }, + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = UnifiedShareCategory.entries.size + ) + ) { + Text(option.name) + } + } + } +} + +@Composable +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) + ) + + PermissionDropdown( + label = "Participants", + selectedPermission = permission, + availablePermissions = availablePermissions, + onPermissionChange = onPermissionChange + ) + } +} + +@Composable +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 +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 +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 + } + ) + } + } + } +} + +@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 } + } +} + +@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 + ) + + SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } + SettingsSwitchRow("Password", hasPassword) { hasPassword = it } + SettingsSwitchRow("Limit downloads", limitDownloads) { limitDownloads = it } + + SettingsSwitchRow("Hide downloads", hideDownloads) { hideDownloads = it } + SettingsSwitchRow("Video verification", videoVerification) { videoVerification = it } + SettingsSwitchRow("Show files in grid view", showFilesInGridView) { showFilesInGridView = it } + + } +} + +@Composable +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) + } +} + +@Composable +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; + + @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) + } + } +} + +// NOTE: To just create a public link anyone tab + just send DOES SAME THING +@Composable +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()) + .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 + ) { + share.type.Icon() + } + }, + headlineContent = { + Text( + text = share.label, + style = MaterialTheme.typography.titleSmall + ) + }, + supportingContent = { + Text( + text = share.permission.getText(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + trailingContent = { + 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(colorScheme: ColorScheme) { + // TODO: REPLACE + val viewModel = UnifiedShareViewModel(repository = MockUnifiedShareRepository()) + + setContent { + MaterialTheme( + colorScheme = colorScheme, + content = { + UnifiedShareView(viewModel) + } + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt new file mode 100644 index 00000000..33310cb1 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.android.common.ui.share.model.UnifiedShares +import com.nextcloud.android.common.ui.share.repository.UnifiedShareRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class UnifiedShareViewModel(private val repository: UnifiedShareRepository): ViewModel() { + private val _shares = MutableStateFlow>(emptyList()) + val shares: StateFlow> = _shares + + init { + viewModelScope.launch(Dispatchers.IO) { + val shares = repository.fetchShares() + _shares.update { + shares + } + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt new file mode 100644 index 00000000..bf9775cf --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model + +enum class UnifiedShareCategory { + Invited, Anyone +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt new file mode 100644 index 00000000..d2d0ce5a --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model + +data class UnifiedShareDownloadLimit( + val limit: Int, + val downloadCount: Int +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt new file mode 100644 index 00000000..46150894 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model + +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 -> "File drop" + CanView -> "Can view" + CanEdit -> "Can edit" + is Custom -> "Custom permissions" + } + } +} + diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt new file mode 100644 index 00000000..f4c1139f --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model + +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import com.nextcloud.android.common.ui.R + +enum class UnifiedShareType { + InternalUser, InternalGroup, InternalLink, ExternalLink, ExternalFederated, ExternalMail; + + @Composable + fun Icon() { + val iconId = when (this) { + InternalUser -> R.drawable.ic_user + InternalGroup -> R.drawable.ic_group + InternalLink -> R.drawable.ic_email + ExternalLink -> R.drawable.ic_link + ExternalFederated -> R.drawable.ic_group + ExternalMail -> R.drawable.ic_email + } + + Icon(painterResource(iconId), contentDescription = "share type icon") + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt new file mode 100644 index 00000000..3b287701 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model + +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, +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt new file mode 100644 index 00000000..6671d6e3 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt @@ -0,0 +1,753 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.previews + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.share.AnyoneShareContent +import com.nextcloud.android.common.ui.share.CollapsibleSettingsSection +import com.nextcloud.android.common.ui.share.InvitedShareContent +import com.nextcloud.android.common.ui.share.NoteToRecipients +import com.nextcloud.android.common.ui.share.PermissionDropdown +import com.nextcloud.android.common.ui.share.SettingsSwitchRow +import com.nextcloud.android.common.ui.share.ShareActionButtons +import com.nextcloud.android.common.ui.share.ShareBottomSheetHeader +import com.nextcloud.android.common.ui.share.ShareCategoryButtonGroup +import com.nextcloud.android.common.ui.share.UnifiedShareView +import com.nextcloud.android.common.ui.share.UnifiedShareViewModel +import com.nextcloud.android.common.ui.share.UnifiedSharesListItem +import com.nextcloud.android.common.ui.share.UnifiedSharesListItemType +import com.nextcloud.android.common.ui.share.model.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.UnifiedShareDownloadLimit +import com.nextcloud.android.common.ui.share.model.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.UnifiedShareType +import com.nextcloud.android.common.ui.share.model.UnifiedShares +import com.nextcloud.android.common.ui.share.repository.MockUnifiedShareRepository + +@Composable +private fun PreviewTheme( + darkTheme: Boolean = false, + content: @Composable () -> Unit +) { + MaterialTheme { + Surface(content = content) + } +} + +@Preview(name = "UnifiedShareView – light", showBackground = true) +@Composable +fun Preview_UnifiedShareView_Light() { + PreviewTheme { + UnifiedShareView(viewModel = UnifiedShareViewModel(MockUnifiedShareRepository())) + } +} + +@Preview(name = "UnifiedShareView – dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun Preview_UnifiedShareView_Dark() { + PreviewTheme(darkTheme = true) { + UnifiedShareView(viewModel = UnifiedShareViewModel(MockUnifiedShareRepository())) + } +} + +@Preview(name = "ListItem – Top", showBackground = true, group = "List Item") +@Composable +fun Preview_UnifiedSharesListItem_Top() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewUserShare(), + type = UnifiedSharesListItemType.Top + ) + } + } +} + +@Preview(name = "ListItem – Mid", showBackground = true, group = "List Item") +@Composable +fun Preview_UnifiedSharesListItem_Mid() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewGroupShare(), + type = UnifiedSharesListItemType.Mid + ) + } + } +} + +@Preview(name = "ListItem – Bottom", showBackground = true, group = "List Item") +@Composable +fun Preview_UnifiedSharesListItem_Bottom() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewPublicLinkShare(), + type = UnifiedSharesListItemType.Bottom + ) + } + } +} + +@Preview(name = "ListItem – Single (all three stacked)", showBackground = true, group = "List Item") +@Composable +fun Preview_UnifiedSharesListItem_AllTypes() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper(previewUserShare(), UnifiedSharesListItemType.Top) + UnifiedSharesListItemPreviewHelper(previewGroupShare(), UnifiedSharesListItemType.Mid) + UnifiedSharesListItemPreviewHelper(previewPublicLinkShare(), UnifiedSharesListItemType.Bottom) + } + } +} + +@Composable +private fun UnifiedSharesListItemPreviewHelper(share: UnifiedShares, type: UnifiedSharesListItemType) { + UnifiedSharesListItem(share = share, type = type) +} + +@Preview(name = "ListItem – CanView permission", showBackground = true, group = "Permissions") +@Composable +fun Preview_ListItem_CanView() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewUserShare(permission = UnifiedSharePermission.CanView), + type = UnifiedSharesListItemType.Top + ) + } + } +} + +@Preview(name = "ListItem – CanEdit permission", showBackground = true, group = "Permissions") +@Composable +fun Preview_ListItem_CanEdit() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewUserShare(permission = UnifiedSharePermission.CanEdit), + type = UnifiedSharesListItemType.Top + ) + } + } +} + +@Preview(name = "ListItem – FileDrop permission", showBackground = true, group = "Permissions") +@Composable +fun Preview_ListItem_FileDrop() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewUserShare(permission = UnifiedSharePermission.FileDrop), + type = UnifiedSharesListItemType.Top + ) + } + } +} + +@Preview(name = "ListItem – Custom permission", showBackground = true, group = "Permissions") +@Composable +fun Preview_ListItem_CustomPermission() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemPreviewHelper( + share = previewUserShare( + permission = UnifiedSharePermission.Custom( + true, + false, + true, + false + ) + ), + type = UnifiedSharesListItemType.Top + ) + } + } +} + +@Preview( + name = "BottomSheet – Invited / default", + showBackground = true, + heightDp = 900, + group = "Bottom Sheet" +) +@Composable +fun Preview_AddShareBottomSheet_Invited() { + PreviewTheme { + AddShareBottomSheetContentPreview( + category = UnifiedShareCategory.Invited, + permission = UnifiedSharePermission.CanView, + searchQuery = "", + note = "", + showSettings = false + ) + } +} + +@Preview( + name = "BottomSheet – Invited / search filled", + showBackground = true, + heightDp = 900, + group = "Bottom Sheet" +) +@Composable +fun Preview_AddShareBottomSheet_InvitedWithSearch() { + PreviewTheme { + AddShareBottomSheetContentPreview( + category = UnifiedShareCategory.Invited, + permission = UnifiedSharePermission.CanEdit, + searchQuery = "alice@nextcloud.example", + note = "Here are the Q2 reports!", + showSettings = false + ) + } +} + +@Preview( + name = "BottomSheet – Invited / settings expanded", + showBackground = true, + heightDp = 1100, + group = "Bottom Sheet" +) +@Composable +fun Preview_AddShareBottomSheet_InvitedSettingsExpanded() { + PreviewTheme { + AddShareBottomSheetContentPreview( + category = UnifiedShareCategory.Invited, + permission = UnifiedSharePermission.CanView, + searchQuery = "bob", + note = "", + showSettings = true + ) + } +} + +@Preview( + name = "BottomSheet – Anyone / default", + showBackground = true, + heightDp = 900, + group = "Bottom Sheet" +) +@Composable +fun Preview_AddShareBottomSheet_Anyone() { + PreviewTheme { + AddShareBottomSheetContentPreview( + category = UnifiedShareCategory.Anyone, + permission = UnifiedSharePermission.CanView, + searchQuery = "", + note = "", + showSettings = false + ) + } +} + +@Preview( + name = "BottomSheet – Anyone / Custom permission (extra switches)", + showBackground = true, + heightDp = 1100, + group = "Bottom Sheet" +) +@Composable +fun Preview_AddShareBottomSheet_AnyoneCustomPermission() { + PreviewTheme { + AddShareBottomSheetContentPreview( + category = UnifiedShareCategory.Anyone, + permission = UnifiedSharePermission.Custom( + true, + false, + false, + false + ), + searchQuery = "", + note = "", + showSettings = false + ) + } +} + +@Preview( + name = "BottomSheet – Anyone / settings expanded", + showBackground = true, + heightDp = 1200, + group = "Bottom Sheet" +) +@Composable +fun Preview_AddShareBottomSheet_AnyoneSettingsExpanded() { + PreviewTheme { + AddShareBottomSheetContentPreview( + category = UnifiedShareCategory.Anyone, + permission = UnifiedSharePermission.CanView, + searchQuery = "", + note = "Public note", + showSettings = true + ) + } +} + +@Preview(name = "CategoryButtons – Invited selected", showBackground = true, group = "Category Buttons") +@Composable +fun Preview_ShareCategoryButtonGroup_Invited() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + ShareCategoryButtonGroup( + selectedCategory = UnifiedShareCategory.Invited, + onCategoryChange = {} + ) + } + } +} + +@Preview(name = "CategoryButtons – Anyone selected", showBackground = true, group = "Category Buttons") +@Composable +fun Preview_ShareCategoryButtonGroup_Anyone() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + ShareCategoryButtonGroup( + selectedCategory = UnifiedShareCategory.Anyone, + onCategoryChange = {} + ) + } + } +} + +@Preview(name = "ActionButtons – Invited / Send disabled", showBackground = true, group = "Action Buttons") +@Composable +fun Preview_ShareActionButtons_InvitedSendDisabled() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + ShareActionButtons( + category = UnifiedShareCategory.Invited, + isSendEnabled = false, + onCopyClick = {}, + onSendClick = {} + ) + } + } +} + +@Preview(name = "ActionButtons – Invited / Send enabled", showBackground = true, group = "Action Buttons") +@Composable +fun Preview_ShareActionButtons_InvitedSendEnabled() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + ShareActionButtons( + category = UnifiedShareCategory.Invited, + isSendEnabled = true, + onCopyClick = {}, + onSendClick = {} + ) + } + } +} + +@Preview(name = "ActionButtons – Anyone", showBackground = true, group = "Action Buttons") +@Composable +fun Preview_ShareActionButtons_Anyone() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + ShareActionButtons( + category = UnifiedShareCategory.Anyone, + isSendEnabled = false, + onCopyClick = {}, + onSendClick = {} + ) + } + } +} + +@Preview(name = "CollapsibleSettings – collapsed", showBackground = true, group = "Settings Section") +@Composable +fun Preview_CollapsibleSettingsSection_Collapsed() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + CollapsibleSettingsSection(isExpanded = false, onToggle = {}) { + InvitedInlineSettingsPreview() + } + } + } +} + +@Preview(name = "CollapsibleSettings – expanded (Invited)", showBackground = true, group = "Settings Section") +@Composable +fun Preview_CollapsibleSettingsSection_ExpandedInvited() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + CollapsibleSettingsSection(isExpanded = true, onToggle = {}) { + InvitedInlineSettingsPreview() + } + } + } +} + +@Preview(name = "CollapsibleSettings – expanded (Anyone)", showBackground = true, group = "Settings Section") +@Composable +fun Preview_CollapsibleSettingsSection_ExpandedAnyone() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + CollapsibleSettingsSection(isExpanded = true, onToggle = {}) { + AnyoneInlineSettingsPreview() + } + } + } +} + +private val allPermissions = listOf( + UnifiedSharePermission.CanView, + UnifiedSharePermission.CanEdit, + UnifiedSharePermission.FileDrop, + UnifiedSharePermission.Custom(false, false, false, false) +) + +@Preview(name = "PermissionDropdown – CanView", showBackground = true, group = "Permission Dropdown") +@Composable +fun Preview_PermissionDropdown_CanView() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + PermissionDropdown( + label = "Participants", + selectedPermission = UnifiedSharePermission.CanView, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "PermissionDropdown – CanEdit", showBackground = true, group = "Permission Dropdown") +@Composable +fun Preview_PermissionDropdown_CanEdit() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + PermissionDropdown( + label = "Anyone with the link", + selectedPermission = UnifiedSharePermission.CanEdit, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "PermissionDropdown – FileDrop", showBackground = true, group = "Permission Dropdown") +@Composable +fun Preview_PermissionDropdown_FileDrop() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + PermissionDropdown( + label = "Anyone with the link", + selectedPermission = UnifiedSharePermission.FileDrop, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "PermissionDropdown – Custom", showBackground = true, group = "Permission Dropdown") +@Composable +fun Preview_PermissionDropdown_Custom() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + PermissionDropdown( + label = "Participants", + selectedPermission = UnifiedSharePermission.Custom(true, false, false, false), + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "NoteToRecipients – empty", showBackground = true, group = "Note") +@Composable +fun Preview_NoteToRecipients_Empty() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + NoteToRecipients(note = "", onNoteChange = {}) + } + } +} + +@Preview(name = "NoteToRecipients – with text", showBackground = true, group = "Note") +@Composable +fun Preview_NoteToRecipients_WithText() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + NoteToRecipients(note = "Please review by end of week!", onNoteChange = {}) + } + } +} + +@Preview(name = "InvitedShareContent – empty query", showBackground = true, group = "Invited Content") +@Composable +fun Preview_InvitedShareContent_EmptyQuery() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + InvitedShareContent( + searchQuery = "", + onSearchChange = {}, + permission = UnifiedSharePermission.CanView, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "InvitedShareContent – with query", showBackground = true, group = "Invited Content") +@Composable +fun Preview_InvitedShareContent_WithQuery() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + InvitedShareContent( + searchQuery = "carol@company.org", + onSearchChange = {}, + permission = UnifiedSharePermission.CanEdit, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "AnyoneShareContent – CanView", showBackground = true, group = "Anyone Content") +@Composable +fun Preview_AnyoneShareContent_CanView() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + AnyoneShareContent( + permission = UnifiedSharePermission.CanView, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "AnyoneShareContent – FileDrop", showBackground = true, group = "Anyone Content") +@Composable +fun Preview_AnyoneShareContent_FileDrop() { + PreviewTheme { + Box(Modifier.padding(16.dp)) { + AnyoneShareContent( + permission = UnifiedSharePermission.FileDrop, + availablePermissions = allPermissions, + onPermissionChange = {} + ) + } + } +} + +@Preview(name = "SwitchRow – off", showBackground = true, group = "Switch Row") +@Composable +fun Preview_SettingsSwitchRow_Off() { + PreviewTheme { + Box(Modifier.padding(horizontal = 16.dp)) { + SettingsSwitchRow(label = "Hide download and sync options", checked = false, onCheckedChange = {}) + } + } +} + +@Preview(name = "SwitchRow – on", showBackground = true, group = "Switch Row") +@Composable +fun Preview_SettingsSwitchRow_On() { + PreviewTheme { + Box(Modifier.padding(horizontal = 16.dp)) { + SettingsSwitchRow(label = "Expiration date", checked = true, onCheckedChange = {}) + } + } +} + +@Preview(name = "Item shape – all types", showBackground = true, group = "Shape") +@Composable +fun Preview_ItemShapes_AllTypes() { + PreviewTheme { + Column(Modifier.padding(8.dp)) { + UnifiedSharesListItemType.entries.forEach { type -> + UnifiedSharesListItemPreviewHelper(share = previewUserShare(), type = type) + } + } + } +} + +@Preview( + name = "UnifiedShareView – tablet landscape", + showBackground = true, + widthDp = 840, + heightDp = 600 +) +@Composable +fun Preview_UnifiedShareView_Tablet() { + PreviewTheme { + UnifiedShareView(viewModel = UnifiedShareViewModel(MockUnifiedShareRepository())) + } +} + +@Composable +private fun AddShareBottomSheetContentPreview( + category: UnifiedShareCategory, + permission: UnifiedSharePermission, + searchQuery: String, + note: String, + showSettings: Boolean +) { + val availablePermissions = listOf( + UnifiedSharePermission.CanView, + UnifiedSharePermission.CanEdit, + UnifiedSharePermission.FileDrop, + UnifiedSharePermission.Custom(false, false, false, false) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ShareBottomSheetHeader(filename = "Abc.txt") + + ShareCategoryButtonGroup( + selectedCategory = category, + onCategoryChange = {} + ) + + if (category == UnifiedShareCategory.Invited) { + InvitedShareContent( + searchQuery = searchQuery, + onSearchChange = {}, + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = {} + ) + CollapsibleSettingsSection( + isExpanded = showSettings, + onToggle = {} + ) { + InvitedInlineSettingsPreview() + } + } else { + AnyoneShareContent( + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = {} + ) + if (permission is UnifiedSharePermission.Custom) { + SettingsSwitchRow("View files", false) {} + SettingsSwitchRow("Edit files", false) {} + SettingsSwitchRow("Create files", false) {} + SettingsSwitchRow("Delete files", false) {} + } + CollapsibleSettingsSection( + isExpanded = showSettings, + onToggle = {} + ) { + AnyoneInlineSettingsPreview() + } + } + + NoteToRecipients(note = note, onNoteChange = {}) + + ShareActionButtons( + category = category, + isSendEnabled = searchQuery.isNotBlank(), + onCopyClick = {}, + onSendClick = {} + ) + } +} + +@Composable +private fun InvitedInlineSettingsPreview() { + Column { + SettingsSwitchRow("Share with others", false) {} + SettingsSwitchRow("Edit file", false) {} + SettingsSwitchRow("Expiration date", true) {} + SettingsSwitchRow("Hide download and sync options", false) {} + } +} + +@Composable +private fun AnyoneInlineSettingsPreview() { + Column { + OutlinedTextField( + value = "Public reports link", + onValueChange = {}, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + label = { Text("Label") }, + placeholder = { Text("Optional name for this link") }, + singleLine = true + ) + SettingsSwitchRow("Expiration date", false) {} + SettingsSwitchRow("Password", true) {} + SettingsSwitchRow("Limit downloads", false) {} + SettingsSwitchRow("Hide downloads", false) {} + SettingsSwitchRow("Video verification", false) {} + SettingsSwitchRow("Show files in grid view", true) {} + } +} + +private fun previewUserShare( + permission: UnifiedSharePermission = UnifiedSharePermission.CanView +) = UnifiedShares( + label = "Alice Smith", + type = UnifiedShareType.InternalUser, + permission = permission, + expirationDate = 0, + sharedTo = "", + category = UnifiedShareCategory.Invited, + id = 1, + password = "", + note = "", + limit = UnifiedShareDownloadLimit(0, 0), +) + +private fun previewGroupShare( + permission: UnifiedSharePermission = UnifiedSharePermission.CanEdit +) = UnifiedShares( + label = "Design Team", + type = UnifiedShareType.InternalGroup, + permission = permission, + expirationDate = 0, + sharedTo = "", + category = UnifiedShareCategory.Invited, + id = 1, + password = "", + note = "", + limit = UnifiedShareDownloadLimit(0, 0), +) + +private fun previewPublicLinkShare( + permission: UnifiedSharePermission = UnifiedSharePermission.FileDrop +) = UnifiedShares( + label = "Public link", + type = UnifiedShareType.ExternalLink, + permission = permission, + expirationDate = 0, + sharedTo = "", + category = UnifiedShareCategory.Invited, + id = 1, + password = "", + note = "", + limit = UnifiedShareDownloadLimit(0, 0), +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt new file mode 100644 index 00000000..85396398 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt @@ -0,0 +1,105 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.repository + +import com.nextcloud.android.common.ui.share.model.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.UnifiedShareDownloadLimit +import com.nextcloud.android.common.ui.share.model.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.UnifiedShareType +import com.nextcloud.android.common.ui.share.model.UnifiedShares + +class MockUnifiedShareRepository: UnifiedShareRepository { + override suspend fun fetchShares(): List { + return 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 + ) + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt new file mode 100644 index 00000000..e2d9beab --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.repository + +import com.nextcloud.android.common.ui.share.model.UnifiedShares + +class UnifiedShareRemoteRepository: UnifiedShareRepository { + override suspend fun fetchShares(): List { + TODO("Not yet implemented") + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt new file mode 100644 index 00000000..09596cdb --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.repository + +import com.nextcloud.android.common.ui.share.model.UnifiedShares + +interface UnifiedShareRepository { + suspend fun fetchShares(): List +} diff --git a/ui/src/main/res/drawable/ic_circles.xml b/ui/src/main/res/drawable/ic_circles.xml new file mode 100644 index 00000000..5b07aff7 --- /dev/null +++ b/ui/src/main/res/drawable/ic_circles.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui/src/main/res/drawable/ic_email.xml b/ui/src/main/res/drawable/ic_email.xml new file mode 100644 index 00000000..3319f67e --- /dev/null +++ b/ui/src/main/res/drawable/ic_email.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui/src/main/res/drawable/ic_group.xml b/ui/src/main/res/drawable/ic_group.xml new file mode 100644 index 00000000..e68f08e7 --- /dev/null +++ b/ui/src/main/res/drawable/ic_group.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui/src/main/res/drawable/ic_link.xml b/ui/src/main/res/drawable/ic_link.xml new file mode 100644 index 00000000..3cb49187 --- /dev/null +++ b/ui/src/main/res/drawable/ic_link.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui/src/main/res/drawable/ic_person_add.xml b/ui/src/main/res/drawable/ic_person_add.xml new file mode 100644 index 00000000..db9f2514 --- /dev/null +++ b/ui/src/main/res/drawable/ic_person_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/ui/src/main/res/drawable/ic_talk.xml b/ui/src/main/res/drawable/ic_talk.xml new file mode 100644 index 00000000..e55ac5d9 --- /dev/null +++ b/ui/src/main/res/drawable/ic_talk.xml @@ -0,0 +1,18 @@ + + + + diff --git a/ui/src/main/res/drawable/ic_user.xml b/ui/src/main/res/drawable/ic_user.xml new file mode 100644 index 00000000..d6267b2f --- /dev/null +++ b/ui/src/main/res/drawable/ic_user.xml @@ -0,0 +1,16 @@ + + + + From 6cd68d1b833712585b02b2d18fc59f6597925a85 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 09:52:07 +0200 Subject: [PATCH 002/174] add api models Signed-off-by: alperozturk96 --- .../android/common/ui/network/ApiResult.kt | 13 +++ .../android/common/ui/network/OcsResponse.kt | 38 ++++++++ .../{UnifiedShareView.kt => ShareView.kt} | 13 ++- ...iedShareViewModel.kt => ShareViewModel.kt} | 6 +- .../model/api/create/CreateShareRequest.kt | 23 +++++ .../model/api/create/ShareDataResponse.kt | 27 ++++++ .../common/ui/share/model/api/owner/Owner.kt | 21 +++++ .../model/api/recipients/ShareRecipients.kt | 29 ++++++ .../model/api/update/UpdateShareRequest.kt | 32 +++++++ .../ui/share/model/api/user/ShareUser.kt | 20 ++++ .../model/{ => ui}/UnifiedShareCategory.kt | 2 +- .../{ => ui}/UnifiedShareDownloadLimit.kt | 2 +- .../model/{ => ui}/UnifiedSharePermission.kt | 2 +- .../share/model/{ => ui}/UnifiedShareType.kt | 2 +- .../ui/share/model/{ => ui}/UnifiedShares.kt | 2 +- ...ifiedSharePreviews.kt => SharePreviews.kt} | 21 ++--- ...reRepository.kt => MockShareRepository.kt} | 12 +-- .../share/repository/ShareRemoteRepository.kt | 91 +++++++++++++++++++ .../ui/share/repository/ShareRepository.kt | 37 ++++++++ .../UnifiedShareRemoteRepository.kt | 16 ---- .../repository/UnifiedShareRepository.kt | 14 --- 21 files changed, 361 insertions(+), 62 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt rename ui/src/main/java/com/nextcloud/android/common/ui/share/{UnifiedShareView.kt => ShareView.kt} (97%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/{UnifiedShareViewModel.kt => ShareViewModel.kt} (77%) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/{ => ui}/UnifiedShareCategory.kt (79%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/{ => ui}/UnifiedShareDownloadLimit.kt (81%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/{ => ui}/UnifiedSharePermission.kt (93%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/{ => ui}/UnifiedShareType.kt (94%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/{ => ui}/UnifiedShares.kt (89%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/previews/{UnifiedSharePreviews.kt => SharePreviews.kt} (96%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/repository/{MockUnifiedShareRepository.kt => MockShareRepository.kt} (89%) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt new file mode 100644 index 00000000..1827178f --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +sealed class ApiResult { + data class Success(val data: T) : ApiResult() + data class Error(val error: OcsResponse) : ApiResult() +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt new file mode 100644 index 00000000..1dc0e7d9 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OcsResponse( + val ocs: Ocs +) + +@Serializable +data class Ocs( + val meta: Meta, + val data: T +) + +@Serializable +data class Meta( + val status: String, + + @SerialName("statuscode") + val statusCode: Int, + + val message: String, + + @SerialName("totalitems") + val totalItems: String, + + @SerialName("itemsperpage") + val itemsPerPage: String +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt similarity index 97% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index b31eb528..15e0c6a5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -29,7 +29,6 @@ import androidx.compose.material.icons.Icons 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.ColorScheme import androidx.compose.material3.DropdownMenu @@ -69,10 +68,10 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.model.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.UnifiedShares -import com.nextcloud.android.common.ui.share.repository.MockUnifiedShareRepository +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares +import com.nextcloud.android.common.ui.share.repository.MockShareRepository // TODO: MOVE TO THE ANDROID: COMMON @@ -80,7 +79,7 @@ import com.nextcloud.android.common.ui.share.repository.MockUnifiedShareReposito // TODO: EXPOSE ACTIONS, IMPLEMENT VIEWMODEL, REPOSITORY TO FETCH ACTUAL SHARE, INJECT NECESSARY PARAMETERS @Composable -fun UnifiedShareView(viewModel: UnifiedShareViewModel) { +fun UnifiedShareView(viewModel: ShareViewModel) { var showAddShare by remember { mutableStateOf(false) } val shares by viewModel.shares.collectAsState() @@ -598,7 +597,7 @@ fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme) { // TODO: REPLACE - val viewModel = UnifiedShareViewModel(repository = MockUnifiedShareRepository()) + val viewModel = ShareViewModel(repository = MockShareRepository()) setContent { MaterialTheme( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt similarity index 77% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 33310cb1..db00eea0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/UnifiedShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -9,15 +9,15 @@ package com.nextcloud.android.common.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.android.common.ui.share.model.UnifiedShares -import com.nextcloud.android.common.ui.share.repository.UnifiedShareRepository +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares +import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class UnifiedShareViewModel(private val repository: UnifiedShareRepository): ViewModel() { +class ShareViewModel(private val repository: ShareRepository): ViewModel() { private val _shares = MutableStateFlow>(emptyList()) val shares: StateFlow> = _shares diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt new file mode 100644 index 00000000..54fa9da7 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.create + +import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import kotlinx.serialization.Serializable + +@Serializable +data class CreateShareRequest( + val data: ShareDataRequest +) + +@Serializable +data class ShareDataRequest( + val sources: List, + val recipients: List, + val properties: Map>> +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt new file mode 100644 index 00000000..0a2fcb93 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.create + +import com.nextcloud.android.common.ui.share.model.api.owner.Owner +import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ShareDataResponse( + val sources: List, + val recipients: List, + val properties: Map>>, + + val id: String, + + @SerialName("last_updated") + val lastUpdated: Long, + + val owner: Owner +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt new file mode 100644 index 00000000..cb75c5d7 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.owner + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class Owner( + @SerialName("user_id") + val userId: String, + + @SerialName("display_name") + val displayName: String +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt new file mode 100644 index 00000000..a9bba350 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.recipients + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ShareRecipients( + val type: String, + val value: String, + + @SerialName("display_name") + val displayName: String, + + @SerialName("display_name_unique") + val displayNameUnique: String, + + @SerialName("icon_url_light") + val iconUrlLight: String, + + @SerialName("icon_url_dark") + val iconUrlDark: String +) \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt new file mode 100644 index 00000000..926177af --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.update + +import com.nextcloud.android.common.ui.share.model.api.owner.Owner +import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateShareRequest( + val data: UpdateShareData +) + +@Serializable +data class UpdateShareData( + val sources: List, + val recipients: List, + val properties: Map>>, + + val id: String, + + @SerialName("last_updated") + val lastUpdated: Long, + + val owner: Owner +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt new file mode 100644 index 00000000..c0709a89 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ShareUser( + val type: String, + val value: String, + + @SerialName("display_name") + val displayName: String +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt similarity index 79% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt index bf9775cf..e93bd3d4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareCategory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share.model +package com.nextcloud.android.common.ui.share.model.ui enum class UnifiedShareCategory { Invited, Anyone diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt similarity index 81% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt index d2d0ce5a..46a600be 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareDownloadLimit.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share.model +package com.nextcloud.android.common.ui.share.model.ui data class UnifiedShareDownloadLimit( val limit: Int, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt similarity index 93% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt index 46150894..595230fb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedSharePermission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share.model +package com.nextcloud.android.common.ui.share.model.ui sealed class UnifiedSharePermission { // file drop only for folder diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt similarity index 94% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt index f4c1139f..5e9c01d2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShareType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share.model +package com.nextcloud.android.common.ui.share.model.ui import androidx.compose.material3.Icon import androidx.compose.runtime.Composable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt similarity index 89% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt index 3b287701..4e7e28a4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/UnifiedShares.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share.model +package com.nextcloud.android.common.ui.share.model.ui data class UnifiedShares( val id: Int, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt similarity index 96% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt index 6671d6e3..eb23395f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/UnifiedSharePreviews.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.share.AnyoneShareContent import com.nextcloud.android.common.ui.share.CollapsibleSettingsSection @@ -32,15 +31,15 @@ import com.nextcloud.android.common.ui.share.ShareActionButtons import com.nextcloud.android.common.ui.share.ShareBottomSheetHeader import com.nextcloud.android.common.ui.share.ShareCategoryButtonGroup import com.nextcloud.android.common.ui.share.UnifiedShareView -import com.nextcloud.android.common.ui.share.UnifiedShareViewModel +import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.UnifiedSharesListItem import com.nextcloud.android.common.ui.share.UnifiedSharesListItemType -import com.nextcloud.android.common.ui.share.model.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.UnifiedShareDownloadLimit -import com.nextcloud.android.common.ui.share.model.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.UnifiedShareType -import com.nextcloud.android.common.ui.share.model.UnifiedShares -import com.nextcloud.android.common.ui.share.repository.MockUnifiedShareRepository +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareDownloadLimit +import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares +import com.nextcloud.android.common.ui.share.repository.MockShareRepository @Composable private fun PreviewTheme( @@ -56,7 +55,7 @@ private fun PreviewTheme( @Composable fun Preview_UnifiedShareView_Light() { PreviewTheme { - UnifiedShareView(viewModel = UnifiedShareViewModel(MockUnifiedShareRepository())) + UnifiedShareView(viewModel = ShareViewModel(MockShareRepository())) } } @@ -64,7 +63,7 @@ fun Preview_UnifiedShareView_Light() { @Composable fun Preview_UnifiedShareView_Dark() { PreviewTheme(darkTheme = true) { - UnifiedShareView(viewModel = UnifiedShareViewModel(MockUnifiedShareRepository())) + UnifiedShareView(viewModel = ShareViewModel(MockShareRepository())) } } @@ -597,7 +596,7 @@ fun Preview_ItemShapes_AllTypes() { @Composable fun Preview_UnifiedShareView_Tablet() { PreviewTheme { - UnifiedShareView(viewModel = UnifiedShareViewModel(MockUnifiedShareRepository())) + UnifiedShareView(viewModel = ShareViewModel(MockShareRepository())) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt similarity index 89% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 85396398..5d3cb32b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockUnifiedShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -7,13 +7,13 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.share.model.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.UnifiedShareDownloadLimit -import com.nextcloud.android.common.ui.share.model.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.UnifiedShareType -import com.nextcloud.android.common.ui.share.model.UnifiedShares +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareDownloadLimit +import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares -class MockUnifiedShareRepository: UnifiedShareRepository { +class MockShareRepository: ShareRepository { override suspend fun fetchShares(): List { return listOf( UnifiedShares( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt new file mode 100644 index 00000000..bbae3611 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.repository + +import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest +import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse +import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients +import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest + +class ShareRemoteRepository: ShareRepository { + + // TODO: ALL OCS-APIRequest //boolean header + + /** + * Searches for recipients + */ + override suspend fun fetchRecipients( + recipientType: String, + query: String, + limit: Int, + offset: Int + ): ApiResult> { + /* + GET + /ocs/v2.php/apps/sharing/api/v1/recipients + + */ + + TODO("Not yet implemented") + } + + override suspend fun createShare(request: CreateShareRequest): ApiResult { + /* + POST + /ocs/v2.php/apps/sharing/api/v1/share + */ + TODO("Not yet implemented") + } + + override suspend fun fetchShare(id: String): ApiResult { + /* + POST + /ocs/v2.php/apps/sharing/api/v1/share/{id} + */ + TODO("Not yet implemented") + } + + override suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult { + /* + PUT + /ocs/v2.php/apps/sharing/api/v1/share/{id} + */ + TODO("Not yet implemented") + } + + override suspend fun deleteShare(id: String): ApiResult { + /* + DELETE + /ocs/v2.php/apps/sharing/api/v1/share/{id} + */ + TODO("Not yet implemented") + } + + /** + * @param sourceType + * Optional filter to return only shares matching a specific source type. + * When null, shares of all source types are returned. + * + * @param lastShareId + * Pagination cursor representing the last known share ID. + * Only shares with an ID greater than this value will be returned. + * When null, results start from the first available share. + */ + override suspend fun fetchShares( + sourceType: String?, + lastShareId: String?, + limit: Int + ): ApiResult> { + /* + GET + /ocs/v2.php/apps/sharing/api/v1/shares + */ + TODO("Not yet implemented") + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt new file mode 100644 index 00000000..99418a1b --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.repository + +import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest +import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse +import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients +import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest + +interface ShareRepository { + suspend fun fetchRecipients( + recipientType: String, + query: String, + limit: Int = 10, + offset: Int = 0 + ): ApiResult> + + suspend fun createShare(request: CreateShareRequest): ApiResult + + suspend fun fetchShare(id: String): ApiResult + + suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult + + suspend fun deleteShare(id: String): ApiResult + + suspend fun fetchShares( + sourceType: String? = null, + lastShareId: String? = null, + limit: Int = 100 + ): ApiResult> +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt deleted file mode 100644 index e2d9beab..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRemoteRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.repository - -import com.nextcloud.android.common.ui.share.model.UnifiedShares - -class UnifiedShareRemoteRepository: UnifiedShareRepository { - override suspend fun fetchShares(): List { - TODO("Not yet implemented") - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt deleted file mode 100644 index 09596cdb..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/UnifiedShareRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.repository - -import com.nextcloud.android.common.ui.share.model.UnifiedShares - -interface UnifiedShareRepository { - suspend fun fetchShares(): List -} From 980457abb106d8eaee5a6c37d3cbc2a2feaa2913 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 10:08:58 +0200 Subject: [PATCH 003/174] adopt mock models to api models Signed-off-by: alperozturk96 --- ui/src/main/AndroidManifest.xml | 2 +- .../android/common/ui/share/ShareView.kt | 4 +- .../android/common/ui/share/ShareViewModel.kt | 38 ++- .../model/api/create/ShareDataResponse.kt | 23 ++ .../ui/share/model/ui/UnifiedShareType.kt | 13 + .../common/ui/share/model/ui/UnifiedShares.kt | 24 +- .../common/ui/share/previews/SharePreviews.kt | 82 +++-- .../share/repository/MockShareRepository.kt | 294 ++++++++++++++---- .../share/repository/ShareRemoteRepository.kt | 3 +- .../ui/share/repository/ShareRepository.kt | 3 +- 10 files changed, 385 insertions(+), 101 deletions(-) diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index b830d265..447589b1 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -5,6 +5,6 @@ ~ SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors ~ SPDX-License-Identifier: MIT --> - + diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 15e0c6a5..3b1d96b0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -68,9 +68,9 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares import com.nextcloud.android.common.ui.share.repository.MockShareRepository @@ -508,7 +508,7 @@ enum class UnifiedSharesListItemType { // NOTE: To just create a public link anyone tab + just send DOES SAME THING @Composable -fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { +fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) { var showContextMenu by remember { mutableStateOf(false) } var showDetailSheet by remember { mutableStateOf(false) } val haptics = LocalHapticFeedback.current diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index db00eea0..9df7c264 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -9,7 +9,8 @@ package com.nextcloud.android.common.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares +import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -17,16 +18,39 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class ShareViewModel(private val repository: ShareRepository): ViewModel() { - private val _shares = MutableStateFlow>(emptyList()) - val shares: StateFlow> = _shares +class ShareViewModel( + private val repository: ShareRepository +) : ViewModel() { + + private val _shares = MutableStateFlow>(emptyList()) + val shares: StateFlow> = _shares + + private val _loading = MutableStateFlow(false) + val loading: StateFlow = _loading + + private val _error = MutableStateFlow(null) + val error: StateFlow = _error init { + loadShares() + } + + private fun loadShares() { viewModelScope.launch(Dispatchers.IO) { - val shares = repository.fetchShares() - _shares.update { - shares + _loading.value = true + _error.value = null + + when (val result = repository.fetchShares()) { + is ApiResult.Success -> { + _shares.update { result.data } + } + + is ApiResult.Error -> { + _error.value = result.error.ocs.meta.message + } } + + _loading.value = false } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt index 0a2fcb93..39eb1e90 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt @@ -9,6 +9,10 @@ package com.nextcloud.android.common.ui.share.model.api.create import com.nextcloud.android.common.ui.share.model.api.owner.Owner import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory +import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -25,3 +29,22 @@ data class ShareDataResponse( val owner: Owner ) + +fun ShareDataResponse.toUnifiedShare(): UnifiedShare { + val primarySource = sources.firstOrNull() + return UnifiedShare( + id = id, + sources = sources, + recipients = recipients, + properties = properties, + lastUpdated = lastUpdated, + owner = owner, + type = UnifiedShareType.toUnifiedShareType(primarySource?.type), + category = UnifiedShareCategory.Invited, // TODO map from properties + permission = UnifiedSharePermission.CanView, // TODO map from properties + label = primarySource?.displayName ?: "Unknown", + note = "", + password = "", + limit = null + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt index 5e9c01d2..ec3a8acc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt @@ -28,4 +28,17 @@ enum class UnifiedShareType { Icon(painterResource(iconId), contentDescription = "share type icon") } + + companion object { + fun toUnifiedShareType(value: String?): UnifiedShareType { + return when (value?.lowercase()) { + "user" -> InternalUser + "group" -> InternalGroup + "link" -> InternalLink + "federated" -> ExternalFederated + "mail" -> ExternalMail + else -> ExternalLink + } + } + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt index 4e7e28a4..c77094bc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt @@ -7,15 +7,23 @@ package com.nextcloud.android.common.ui.share.model.ui -data class UnifiedShares( - val id: Int, - val password: String, - val note: String, - val limit: UnifiedShareDownloadLimit, - val expirationDate: Int, +import com.nextcloud.android.common.ui.share.model.api.owner.Owner +import com.nextcloud.android.common.ui.share.model.api.user.ShareUser + +data class UnifiedShare( + val id: String, + val sources: List, + val recipients: List, + val properties: Map>>, + + val lastUpdated: Long, + val owner: Owner, + val permission: UnifiedSharePermission, - val label: String, - val sharedTo: String, val type: UnifiedShareType, val category: UnifiedShareCategory, + val label: String, + val note: String = "", + val password: String = "", + val limit: UnifiedShareDownloadLimit? = null ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt index eb23395f..53be607c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt @@ -30,15 +30,17 @@ import com.nextcloud.android.common.ui.share.SettingsSwitchRow import com.nextcloud.android.common.ui.share.ShareActionButtons import com.nextcloud.android.common.ui.share.ShareBottomSheetHeader import com.nextcloud.android.common.ui.share.ShareCategoryButtonGroup -import com.nextcloud.android.common.ui.share.UnifiedShareView import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.UnifiedShareView import com.nextcloud.android.common.ui.share.UnifiedSharesListItem import com.nextcloud.android.common.ui.share.UnifiedSharesListItemType +import com.nextcloud.android.common.ui.share.model.api.owner.Owner +import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareDownloadLimit import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares import com.nextcloud.android.common.ui.share.repository.MockShareRepository @Composable @@ -119,7 +121,7 @@ fun Preview_UnifiedSharesListItem_AllTypes() { } @Composable -private fun UnifiedSharesListItemPreviewHelper(share: UnifiedShares, type: UnifiedSharesListItemType) { +private fun UnifiedSharesListItemPreviewHelper(share: UnifiedShare, type: UnifiedSharesListItemType) { UnifiedSharesListItem(share = share, type = type) } @@ -708,45 +710,81 @@ private fun AnyoneInlineSettingsPreview() { private fun previewUserShare( permission: UnifiedSharePermission = UnifiedSharePermission.CanView -) = UnifiedShares( +) = UnifiedShare( + id = "1", + sources = listOf( + ShareUser( + type = "user", + value = "alice@company.com", + displayName = "Alice Smith" + ) + ), + recipients = emptyList(), + properties = emptyMap(), + lastUpdated = 0, + owner = Owner( + userId = "alice", + displayName = "Alice Smith" + ), label = "Alice Smith", type = UnifiedShareType.InternalUser, permission = permission, - expirationDate = 0, - sharedTo = "", category = UnifiedShareCategory.Invited, - id = 1, - password = "", note = "", - limit = UnifiedShareDownloadLimit(0, 0), + password = "", + limit = UnifiedShareDownloadLimit(0, 0) ) private fun previewGroupShare( permission: UnifiedSharePermission = UnifiedSharePermission.CanEdit -) = UnifiedShares( +) = UnifiedShare( + id = "2", + sources = listOf( + ShareUser( + type = "group", + value = "design", + displayName = "Design Team" + ) + ), + recipients = emptyList(), + properties = emptyMap(), + lastUpdated = 0, + owner = Owner( + userId = "system", + displayName = "System" + ), label = "Design Team", type = UnifiedShareType.InternalGroup, permission = permission, - expirationDate = 0, - sharedTo = "", category = UnifiedShareCategory.Invited, - id = 1, - password = "", note = "", - limit = UnifiedShareDownloadLimit(0, 0), + password = "", + limit = UnifiedShareDownloadLimit(0, 0) ) private fun previewPublicLinkShare( permission: UnifiedSharePermission = UnifiedSharePermission.FileDrop -) = UnifiedShares( +) = UnifiedShare( + id = "3", + sources = listOf( + ShareUser( + type = "link", + value = "https://nextcloud.com/s/abc123", + displayName = "Public Link" + ) + ), + recipients = emptyList(), + properties = emptyMap(), + lastUpdated = 1710000000, + owner = Owner( + userId = "system", + displayName = "System" + ), label = "Public link", type = UnifiedShareType.ExternalLink, permission = permission, - expirationDate = 0, - sharedTo = "", - category = UnifiedShareCategory.Invited, - id = 1, - password = "", + category = UnifiedShareCategory.Anyone, note = "", - limit = UnifiedShareDownloadLimit(0, 0), + password = "1234", + limit = UnifiedShareDownloadLimit(50, 5) ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 5d3cb32b..73aa1227 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -7,56 +7,199 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareDownloadLimit -import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShares - -class MockShareRepository: ShareRepository { - override suspend fun fetchShares(): List { - return listOf( - UnifiedShares( - id = 1, - password = "", - note = "Design review – please check latest changes", - limit = UnifiedShareDownloadLimit( - limit = 100, - downloadCount = 12 +import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest +import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse +import com.nextcloud.android.common.ui.share.model.api.owner.Owner +import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients +import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest +import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import com.nextcloud.android.common.ui.share.model.ui.* + +class MockShareRepository : ShareRepository { + override suspend fun fetchRecipients( + recipientType: String, + query: String, + limit: Int, + offset: Int + ): ApiResult> { + + val mock = listOf( + ShareRecipients( + type = recipientType, + value = "alice@company.com", + displayName = "Alice Johnson", + displayNameUnique = "Alice Johnson (Company)", + iconUrlLight = "https://mock/icons/user_light.png", + iconUrlDark = "https://mock/icons/user_dark.png" + ), + + ShareRecipients( + type = recipientType, + value = "marketing", + displayName = "Marketing Team", + displayNameUnique = "Marketing Team (Group)", + iconUrlLight = "https://mock/icons/group_light.png", + iconUrlDark = "https://mock/icons/group_dark.png" + ), + + ShareRecipients( + type = recipientType, + value = "john@external.com", + displayName = "John External", + displayNameUnique = "John External (External)", + iconUrlLight = "https://mock/icons/external_light.png", + iconUrlDark = "https://mock/icons/external_dark.png" + ) + ) + + return ApiResult.Success(mock) + } + + override suspend fun createShare( + request: CreateShareRequest + ): ApiResult { + + val response = ShareDataResponse( + sources = request.data.sources, + recipients = request.data.recipients, + properties = request.data.properties, + id = "mock-share-${System.currentTimeMillis()}", + lastUpdated = System.currentTimeMillis(), + owner = Owner( + userId = "mock-user", + displayName = "Mock User" + ) + ) + + return ApiResult.Success(response) + } + + override suspend fun fetchShare(id: String): ApiResult { + + val mock = ShareDataResponse( + sources = emptyList(), + recipients = listOf( + ShareUser( + type = "user", + value = "alice@company.com", + displayName = "Alice Johnson" + ) + ), + properties = emptyMap(), + id = id, + lastUpdated = 0, + owner = Owner( + userId = "alice", + displayName = "Alice Johnson" + ) + ) + + return ApiResult.Success(mock) + } + + override suspend fun updateShare( + id: String, + request: UpdateShareRequest + ): ApiResult { + + val updated = ShareDataResponse( + sources = request.data.sources, + recipients = request.data.recipients, + properties = request.data.properties, + id = id, + lastUpdated = System.currentTimeMillis(), + owner = request.data.owner + ) + + return ApiResult.Success(updated) + } + + override suspend fun deleteShare(id: String): ApiResult { + return ApiResult.Success(Unit) + } + + override suspend fun fetchShares( + sourceType: String?, + lastShareId: String?, + limit: Int + ): ApiResult> { + val data = listOf( + UnifiedShare( + id = "1", + sources = emptyList(), + recipients = listOf( + ShareUser( + type = "user", + value = "alice@company.com", + displayName = "Alice Johnson" + ) + ), + properties = emptyMap(), + lastUpdated = 0, + owner = Owner( + userId = "alice", + displayName = "Alice Johnson" ), - expirationDate = 0, + permission = UnifiedSharePermission.CanView, label = "Alice Johnson", - sharedTo = "alice@company.com", + note = "Design review – please check latest changes", + password = "", type = UnifiedShareType.InternalUser, - category = UnifiedShareCategory.Invited + category = UnifiedShareCategory.Invited, + limit = UnifiedShareDownloadLimit( + limit = 100, + downloadCount = 12 + ) ), - UnifiedShares( - id = 2, - password = "", - note = "", - limit = UnifiedShareDownloadLimit( - limit = 0, - downloadCount = 0 + UnifiedShare( + id = "2", + sources = emptyList(), + recipients = listOf( + ShareUser( + type = "group", + value = "marketing", + displayName = "Marketing Team" + ) + ), + properties = emptyMap(), + lastUpdated = 0, + owner = Owner( + userId = "system", + displayName = "System" ), - expirationDate = 0, + permission = UnifiedSharePermission.CanEdit, label = "Marketing Team", - sharedTo = "marketing", + note = "", + password = "", type = UnifiedShareType.InternalGroup, - category = UnifiedShareCategory.Invited + category = UnifiedShareCategory.Invited, + limit = UnifiedShareDownloadLimit( + limit = 0, + downloadCount = 0 + ) ), - UnifiedShares( - id = 3, - password = "1234", - note = "Public link for client review", - limit = UnifiedShareDownloadLimit( - limit = 50, - downloadCount = 5 + UnifiedShare( + id = "3", + sources = listOf( + ShareUser( + type = "link", + value = "https://nextcloud.com/s/abc123", + displayName = "Public Link" + ) + ), + recipients = emptyList(), + properties = emptyMap(), + lastUpdated = 1710000000, + owner = Owner( + userId = "system", + displayName = "System" ), - expirationDate = 1710000000, + permission = UnifiedSharePermission.Custom( read = true, edit = false, @@ -64,42 +207,75 @@ class MockShareRepository: ShareRepository { create = false ), label = "Public Link", - sharedTo = "https://nextcloud.com/s/abc123", + note = "Public link for client review", + password = "1234", type = UnifiedShareType.InternalLink, - category = UnifiedShareCategory.Anyone + category = UnifiedShareCategory.Anyone, + limit = UnifiedShareDownloadLimit( + limit = 50, + downloadCount = 5 + ) ), - UnifiedShares( - id = 4, - password = "", - note = "External partner access", - limit = UnifiedShareDownloadLimit( - limit = 20, - downloadCount = 2 + UnifiedShare( + id = "4", + sources = emptyList(), + recipients = listOf( + ShareUser( + type = "mail", + value = "john@external.com", + displayName = "John External" + ) ), - expirationDate = 0, + properties = emptyMap(), + lastUpdated = 0, + owner = Owner( + userId = "john", + displayName = "John External" + ), + permission = UnifiedSharePermission.CanView, label = "John External", - sharedTo = "john@external.com", + note = "External partner access", + password = "", type = UnifiedShareType.ExternalMail, - category = UnifiedShareCategory.Anyone + category = UnifiedShareCategory.Anyone, + limit = UnifiedShareDownloadLimit( + limit = 20, + downloadCount = 2 + ) ), - UnifiedShares( - id = 5, - password = "", - note = "Federated sharing with partner instance", - limit = UnifiedShareDownloadLimit( - limit = 0, - downloadCount = 0 + UnifiedShare( + id = "5", + sources = emptyList(), + recipients = listOf( + ShareUser( + type = "federated", + value = "partner@nextcloud.org", + displayName = "Partner Cloud" + ) ), - expirationDate = 0, + properties = emptyMap(), + lastUpdated = 0, + owner = Owner( + userId = "partner", + displayName = "Partner Cloud" + ), + permission = UnifiedSharePermission.FileDrop, label = "Partner Cloud", - sharedTo = "partner@nextcloud.org", + note = "Federated sharing with partner instance", + password = "", type = UnifiedShareType.ExternalFederated, - category = UnifiedShareCategory.Anyone + category = UnifiedShareCategory.Anyone, + limit = UnifiedShareDownloadLimit( + limit = 0, + downloadCount = 0 + ) ) ) + + return ApiResult.Success(data) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index bbae3611..bea010e3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -12,6 +12,7 @@ import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare class ShareRemoteRepository: ShareRepository { @@ -81,7 +82,7 @@ class ShareRemoteRepository: ShareRepository { sourceType: String?, lastShareId: String?, limit: Int - ): ApiResult> { + ): ApiResult> { /* GET /ocs/v2.php/apps/sharing/api/v1/shares diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 99418a1b..c6bb626d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -12,6 +12,7 @@ import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest +import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare interface ShareRepository { suspend fun fetchRecipients( @@ -33,5 +34,5 @@ interface ShareRepository { sourceType: String? = null, lastShareId: String? = null, limit: Int = 100 - ): ApiResult> + ): ApiResult> } From 30e723bc5e582b4723851bf1493da30286597fbd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 12:41:47 +0200 Subject: [PATCH 004/174] add translations, bind ui actions Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 408 +++++---- .../android/common/ui/share/ShareViewModel.kt | 39 +- .../model/api/create/ShareDataResponse.kt | 6 +- .../share/model/ui/ShareBottomSheetState.kt | 14 + .../share/model/ui/UnifiedSharePermission.kt | 63 +- .../common/ui/share/model/ui/UnifiedShares.kt | 30 +- .../common/ui/share/previews/SharePreviews.kt | 790 ------------------ ui/src/main/res/values/strings.xml | 60 ++ 8 files changed, 449 insertions(+), 961 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt create mode 100644 ui/src/main/res/values/strings.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 3b1d96b0..caed3858 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -7,6 +7,8 @@ package com.nextcloud.android.common.ui.share +import android.content.ClipData +import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,6 +23,8 @@ 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -46,17 +50,23 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -64,14 +74,22 @@ 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.LocalClipboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.toClipEntry import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.ui.ShareBottomSheetState import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission +import com.nextcloud.android.common.ui.share.model.ui.customPermissionFields import com.nextcloud.android.common.ui.share.repository.MockShareRepository +import kotlinx.coroutines.launch // TODO: MOVE TO THE ANDROID: COMMON @@ -79,77 +97,105 @@ import com.nextcloud.android.common.ui.share.repository.MockShareRepository // TODO: EXPOSE ACTIONS, IMPLEMENT VIEWMODEL, REPOSITORY TO FETCH ACTUAL SHARE, INJECT NECESSARY PARAMETERS @Composable -fun UnifiedShareView(viewModel: ShareViewModel) { - var showAddShare by remember { mutableStateOf(false) } +private fun ShareView(viewModel: ShareViewModel) { + val errorMessageId by viewModel.errorMessageId.collectAsState() + var bottomSheetState by remember { mutableStateOf(ShareBottomSheetState.Idle) } val shares by viewModel.shares.collectAsState() + val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - shares.forEachIndexed { index, share -> - val type = when (index) { - 0 -> { - UnifiedSharesListItemType.Top - } + LaunchedEffect(errorMessageId) { + errorMessageId?.let { + snackbarHostState.showSnackbar(context.getString(it)) + viewModel.updateErrorMessage(null) + } + } - shares.lastIndex -> { - UnifiedSharesListItemType.Bottom - } + Scaffold(floatingActionButton = { + FloatingActionButton( + onClick = { bottomSheetState = ShareBottomSheetState.New(UnifiedShare.new()) }, + ) { + Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") + } + }, snackbarHost = { + SnackbarHost(snackbarHostState) + }) { + LazyColumn(modifier = Modifier.padding(it)) { + itemsIndexed(shares) { index, share -> + val type = when (index) { + 0 -> { + UnifiedSharesListItemType.Top + } + + shares.lastIndex -> { + UnifiedSharesListItemType.Bottom + } - else -> { - UnifiedSharesListItemType.Mid + else -> { + UnifiedSharesListItemType.Mid + } } - } - UnifiedSharesListItem(share, type) + UnifiedSharesListItem(share, type, onSelectShare = { share -> + bottomSheetState = ShareBottomSheetState.Edit(share) + }, onDeleteShare = { + viewModel.delete(share) + }, onSendEmail = { + // TODO: + }) + } } + } - FloatingActionButton( - onClick = { showAddShare = true }, - modifier = Modifier - .align(Alignment.End) - .padding(top = 16.dp) - ) { - Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") + when (bottomSheetState) { + is ShareBottomSheetState.Edit -> { + val state = (bottomSheetState as ShareBottomSheetState.Edit) + AddOrEditShareBottomSheet( + title = stringResource(R.string.share_view_bottom_sheet_edit_title, state.share.label), + share = state.share, + onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } + ) } - if (showAddShare) { - AddShareBottomSheet("Abc.txt",onDismiss = { showAddShare = false }) + is ShareBottomSheetState.New -> { + val state = (bottomSheetState as ShareBottomSheetState.New) + AddOrEditShareBottomSheet( + title = stringResource(R.string.share_view_bottom_sheet_new_title), + share = state.newShare, + onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } + ) } + + ShareBottomSheetState.Idle -> Unit } } // 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 -fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { +private fun AddOrEditShareBottomSheet(title: String, share: UnifiedShare, onDismiss: () -> Unit) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() - var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } - var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } + var category by remember { mutableStateOf(share.category) } + var permission by remember { mutableStateOf(share.permission ?: UnifiedSharePermission.CanView) } var searchQuery by remember { mutableStateOf("") } - var note by remember { mutableStateOf("") } + var note by remember { mutableStateOf(share.note) } // 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) } - var deleteFiles by remember { mutableStateOf(false) } + val clipboard = LocalClipboard.current + val context = LocalContext.current + val scope = rememberCoroutineScope() val availablePermissions = remember { listOf( UnifiedSharePermission.CanView, UnifiedSharePermission.CanEdit, UnifiedSharePermission.FileDrop, - UnifiedSharePermission.Custom(false, false, false, false) + UnifiedSharePermission.Custom.getFromPermission(share.permission) ) } @@ -166,7 +212,12 @@ fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { .verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - ShareBottomSheetHeader(filename) + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) ShareCategoryButtonGroup( selectedCategory = category, @@ -186,7 +237,7 @@ fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { isExpanded = showInvitedSettings, onToggle = { showInvitedSettings = !showInvitedSettings } ) { - InvitedInlineSettings() + InvitedInlineSettings(share) } } else { AnyoneShareContent( @@ -196,27 +247,39 @@ fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { ) 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 } + val customPermissions = permission as UnifiedSharePermission.Custom + + customPermissionFields.forEach { field -> + SettingsSwitchRow( + label = stringResource(field.labelRes), + checked = field.getValue(customPermissions), + onCheckedChange = { permission = field.setValue(customPermissions, it) } + ) + } } CollapsibleSettingsSection( isExpanded = showAnyoneSettings, onToggle = { showAnyoneSettings = !showAnyoneSettings } ) { - AnyoneInlineSettings() + AnyoneInlineSettings(share) } } NoteToRecipients(note = note, onNoteChange = { note = it }) - ShareActionButtons( - category = category, + share = share, isSendEnabled = searchQuery.isNotBlank(), - onCopyClick = { /* TODO */ }, + onCopyClick = { + val label = context.getString(R.string.share_view_copy_to_clipboard_label) + + scope.launch { + val clipData = + ClipData.newPlainText(label, it) + clipboard.setClipEntry(clipData.toClipEntry()) + } + }, onSendClick = { /* TODO */ } ) } @@ -224,7 +287,7 @@ fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { } @Composable -fun CollapsibleSettingsSection( +private fun CollapsibleSettingsSection( isExpanded: Boolean, onToggle: () -> Unit, content: @Composable () -> Unit @@ -239,7 +302,7 @@ fun CollapsibleSettingsSection( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Settings", + text = stringResource(R.string.share_view_advanced_settings), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary ) @@ -258,19 +321,9 @@ fun CollapsibleSettingsSection( } } -@Composable -fun ShareBottomSheetHeader(filename: String) { - Text( - text = "Share $filename", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 8.dp) - ) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ShareCategoryButtonGroup( +private fun ShareCategoryButtonGroup( selectedCategory: UnifiedShareCategory, onCategoryChange: (UnifiedShareCategory) -> Unit ) { @@ -293,7 +346,7 @@ fun ShareCategoryButtonGroup( } @Composable -fun InvitedShareContent( +private fun InvitedShareContent( searchQuery: String, onSearchChange: (String) -> Unit, permission: UnifiedSharePermission, @@ -306,14 +359,14 @@ fun InvitedShareContent( value = searchQuery, onValueChange = onSearchChange, modifier = Modifier.fillMaxWidth(), - label = { Text("Add people") }, - placeholder = { Text("Name, team, email or federated ID") }, + label = { Text(stringResource(R.string.share_view_invited_category_label)) }, + placeholder = { Text(stringResource(R.string.share_view_invited_category_placeholder)) }, singleLine = true, shape = RoundedCornerShape(8.dp) ) PermissionDropdown( - label = "Participants", + label = stringResource(R.string.share_view_invited_category_participants), selectedPermission = permission, availablePermissions = availablePermissions, onPermissionChange = onPermissionChange @@ -322,7 +375,7 @@ fun InvitedShareContent( } @Composable -fun NoteToRecipients( +private fun NoteToRecipients( note: String, onNoteChange: (String) -> Unit ) { @@ -330,20 +383,20 @@ fun NoteToRecipients( value = note, onValueChange = onNoteChange, modifier = Modifier.fillMaxWidth(), - placeholder = { Text("Note to recipients") }, + placeholder = { Text(stringResource(R.string.share_view_note_text_field_placeholder)) }, shape = RoundedCornerShape(8.dp) ) } @Composable -fun AnyoneShareContent( +private fun AnyoneShareContent( permission: UnifiedSharePermission, availablePermissions: List, onPermissionChange: (UnifiedSharePermission) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { PermissionDropdown( - label = "Anyone with the link", + label = stringResource(R.string.share_view_permission_dropdown_label), selectedPermission = permission, availablePermissions = availablePermissions, onPermissionChange = onPermissionChange @@ -353,7 +406,7 @@ fun AnyoneShareContent( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PermissionDropdown( +private fun PermissionDropdown( label: String, selectedPermission: UnifiedSharePermission, availablePermissions: List, @@ -367,7 +420,7 @@ fun PermissionDropdown( modifier = Modifier.fillMaxWidth() ) { OutlinedTextField( - value = selectedPermission.getText(), + value = stringResource(selectedPermission.getTextId()), onValueChange = {}, readOnly = true, label = { Text(label) }, @@ -383,7 +436,7 @@ fun PermissionDropdown( ) { availablePermissions.forEach { option -> DropdownMenuItem( - text = { Text(option.getText()) }, + text = { Text(stringResource(option.getTextId())) }, onClick = { onPermissionChange(option) expanded = false @@ -395,55 +448,72 @@ fun PermissionDropdown( } @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 } - } +private fun InvitedInlineSettings(share: UnifiedShare) { + var shareWithOthers by remember { mutableStateOf(share.recipients.isNotEmpty()) } + var editFile by remember { mutableStateOf((share.permission as? UnifiedSharePermission.CanEdit) != null) } + var hasExpiration by remember { mutableStateOf(false) } // TODO + var hideDownload by remember { mutableStateOf(false) } // TODO + + SettingsSwitchRow(stringResource(R.string.share_view_invited_category_share_with_others_switch), shareWithOthers) { shareWithOthers = it } + SettingsSwitchRow(stringResource(R.string.share_view_invited_category_edit_file_switch), editFile) { editFile = it } + SettingsSwitchRow(stringResource(R.string.share_view_invited_category_expiration_date_switch), hasExpiration) { hasExpiration = it } + SettingsSwitchRow(stringResource(R.string.share_view_invited_category_hide_and_download_switch), hideDownload) { hideDownload = it } } @Composable -private fun AnyoneInlineSettings() { - var hasPassword by remember { mutableStateOf(false) } +private fun AnyoneInlineSettings(share: UnifiedShare) { + var hasPassword by remember { mutableStateOf(share.password.isNotEmpty()) } var hasExpiration by remember { mutableStateOf(false) } - var limitDownloads by remember { mutableStateOf(false) } + var limitDownloads by remember { mutableStateOf(share.limit != null) } 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 - ) - - SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } - SettingsSwitchRow("Password", hasPassword) { hasPassword = it } - SettingsSwitchRow("Limit downloads", limitDownloads) { limitDownloads = it } - - SettingsSwitchRow("Hide downloads", hideDownloads) { hideDownloads = it } - SettingsSwitchRow("Video verification", videoVerification) { videoVerification = it } - SettingsSwitchRow("Show files in grid view", showFilesInGridView) { showFilesInGridView = it } + OutlinedTextField( + value = "", + onValueChange = {}, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + label = { Text(stringResource(R.string.share_view_anyone_category_label)) }, + placeholder = { Text(stringResource(R.string.share_view_anyone_category_label_placeholder)) }, + singleLine = true + ) - } + SettingsSwitchRow( + stringResource(R.string.share_view_anyone_category_expiration_date_switch), + hasExpiration + ) { hasExpiration = it } + + SettingsSwitchRow( + stringResource(R.string.share_view_anyone_category_password_switch), + hasPassword + ) { hasPassword = it } + + SettingsSwitchRow( + stringResource(R.string.share_view_anyone_category_limit_downloads_switch), + limitDownloads + ) { limitDownloads = it } + + SettingsSwitchRow( + stringResource(R.string.share_view_anyone_category_hide_downloads_switch), + hideDownloads + ) { hideDownloads = it } + + SettingsSwitchRow( + stringResource(R.string.share_view_anyone_category_video_verification_switch), + videoVerification + ) { videoVerification = it } + + SettingsSwitchRow( + stringResource(R.string.share_view_anyone_category_grid_view_switch), + showFilesInGridView + ) { showFilesInGridView = it } } @Composable -fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { +private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -457,37 +527,38 @@ fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean } @Composable -fun ShareActionButtons( - category: UnifiedShareCategory, +private fun ShareActionButtons( + share: UnifiedShare, isSendEnabled: Boolean, - onCopyClick: () -> Unit, + onCopyClick: (String) -> Unit, onSendClick: () -> Unit ) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp)) { - if (category == UnifiedShareCategory.Invited) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + if (share.category == UnifiedShareCategory.Invited) { FilledTonalButton( - onClick = onCopyClick, + onClick = { onCopyClick("TODO") }, modifier = Modifier.weight(1f) ) { - Text("Copy link") + Text(stringResource(R.string.share_view_copy_action)) } Spacer(modifier = Modifier.width(16.dp)) Button( onClick = onSendClick, modifier = Modifier.weight(1f), - enabled = isSendEnabled // Disabled if search query is empty + enabled = isSendEnabled ) { - Text("Send") + Text(stringResource(R.string.share_view_send_action)) } } else { - // For "Anyone" (Public link), usually just one big action to create/copy Button( - onClick = onCopyClick, + onClick = { onCopyClick("TODO") }, modifier = Modifier.fillMaxWidth() ) { - Text("Create public link") + Text(stringResource(R.string.share_view_create_public_link)) } } } @@ -508,9 +579,14 @@ enum class UnifiedSharesListItemType { // NOTE: To just create a public link anyone tab + just send DOES SAME THING @Composable -fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) { +private fun UnifiedSharesListItem( + share: UnifiedShare, + type: UnifiedSharesListItemType, + onSelectShare: (UnifiedShare) -> Unit, + onDeleteShare: (UnifiedShare) -> Unit, + onSendEmail: (UnifiedShare) -> Unit +) { var showContextMenu by remember { mutableStateOf(false) } - var showDetailSheet by remember { mutableStateOf(false) } val haptics = LocalHapticFeedback.current ListItem( @@ -518,7 +594,7 @@ fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) .fillMaxWidth() .clip(type.getShape()) .combinedClickable( - onClick = { showDetailSheet = true }, + onClick = { onSelectShare(share) }, onLongClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) showContextMenu = true @@ -526,14 +602,16 @@ fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), leadingContent = { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - share.type.Icon() + share.type?.let { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center + ) { + it.Icon() + } } }, headlineContent = { @@ -543,11 +621,13 @@ fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) ) }, supportingContent = { - Text( - text = share.permission.getText(), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + share.permission?.getTextId()?.let { + Text( + text = stringResource(it), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } }, trailingContent = { Box { @@ -560,23 +640,29 @@ fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) onDismissRequest = { showContextMenu = false } ) { DropdownMenuItem( - text = { Text("Edit") }, + text = { Text(stringResource(R.string.share_view_list_item_edit)) }, onClick = { showContextMenu = false - showDetailSheet = true + onSelectShare(share) } ) DropdownMenuItem( - text = { Text("Send email") }, - onClick = { showContextMenu = false } + text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, + onClick = { + onSendEmail(share) + showContextMenu = false + } ) HorizontalDivider() DropdownMenuItem( - text = { Text("Delete", color = MaterialTheme.colorScheme.error) }, - onClick = { showContextMenu = false } + text = { Text(stringResource(R.string.share_view_list_item_delete), color = MaterialTheme.colorScheme.error) }, + onClick = { + onDeleteShare(share) + showContextMenu = false + } ) } } @@ -585,13 +671,31 @@ fun UnifiedSharesListItem(share: UnifiedShare, type: UnifiedSharesListItemType) containerColor = Color.Transparent ) ) +} - // TODO: USE EXISTING SHARE DETAILS - if (showDetailSheet) { - AddShareBottomSheet( - filename = share.label, - onDismiss = { showDetailSheet = false } - ) +@Preview(name = "UnifiedShareView – light", showBackground = true) +@Composable +private fun PreviewLight() { + PreviewTheme { + ShareView(viewModel = ShareViewModel(MockShareRepository())) + } +} + +@Preview(name = "UnifiedShareView – dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewDark() { + PreviewTheme(darkTheme = true) { + ShareView(viewModel = ShareViewModel(MockShareRepository())) + } +} + +@Composable +private fun PreviewTheme( + darkTheme: Boolean = false, + content: @Composable () -> Unit +) { + MaterialTheme { + Surface(content = content) } } @@ -603,7 +707,7 @@ fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme) { MaterialTheme( colorScheme = colorScheme, content = { - UnifiedShareView(viewModel) + ShareView(viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 9df7c264..39f948e3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -9,6 +9,7 @@ package com.nextcloud.android.common.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.ApiResult import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.repository.ShareRepository @@ -28,17 +29,18 @@ class ShareViewModel( private val _loading = MutableStateFlow(false) val loading: StateFlow = _loading - private val _error = MutableStateFlow(null) - val error: StateFlow = _error + private val _errorMessageId = MutableStateFlow(null) + val errorMessageId: StateFlow = _errorMessageId init { loadShares() } + // region private methods private fun loadShares() { viewModelScope.launch(Dispatchers.IO) { _loading.value = true - _error.value = null + _errorMessageId.value = null when (val result = repository.fetchShares()) { is ApiResult.Success -> { @@ -46,11 +48,40 @@ class ShareViewModel( } is ApiResult.Error -> { - _error.value = result.error.ocs.meta.message + _errorMessageId.value = R.string.share_view_fetch_error_message } } _loading.value = false } } + // endregion + + // region public methods + fun delete(share: UnifiedShare) { + viewModelScope.launch(Dispatchers.IO) { + val id = share.id + if (id == null) { + _errorMessageId.update { + R.string.share_view_delete_error_id_not_found_message + } + return@launch + } + + val result = repository.deleteShare(share.id) + if (result is ApiResult.Error) { + _errorMessageId.update { + R.string.share_view_delete_error_message + } + return@launch + } + } + } + + fun updateErrorMessage(value: Int?) { + _errorMessageId.update { + value + } + } + // endregion } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt index 39eb1e90..1d2ec2f2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt @@ -43,8 +43,8 @@ fun ShareDataResponse.toUnifiedShare(): UnifiedShare { category = UnifiedShareCategory.Invited, // TODO map from properties permission = UnifiedSharePermission.CanView, // TODO map from properties label = primarySource?.displayName ?: "Unknown", - note = "", - password = "", - limit = null + note = "", // TODO map from properties + password = "", // TODO map from properties + limit = null // TODO map from properties ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt new file mode 100644 index 00000000..a5bfd600 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +sealed class ShareBottomSheetState { + data object Idle: ShareBottomSheetState() + data class New(val newShare: UnifiedShare): ShareBottomSheetState() + data class Edit(val share: UnifiedShare): ShareBottomSheetState() +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt index 595230fb..b6511314 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt @@ -7,6 +7,8 @@ package com.nextcloud.android.common.ui.share.model.ui +import com.nextcloud.android.common.ui.R + sealed class UnifiedSharePermission { // file drop only for folder data object FileDrop : UnifiedSharePermission() @@ -15,16 +17,63 @@ sealed class 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() + data class Custom(var read: Boolean, var edit: Boolean, var delete: Boolean, var create: Boolean) : + UnifiedSharePermission() { + companion object { + fun getFromPermission(permission: UnifiedSharePermission?): Custom { + return Custom( + permission?.customPermissionRead() == true, + permission?.customPermissionEdit() == true, + permission?.customPermissionDelete() == true, + permission?.customPermissionCreate() == true + ) + } + } + } - fun getText(): String { + fun getTextId(): Int { return when(this) { - FileDrop -> "File drop" - CanView -> "Can view" - CanEdit -> "Can edit" - is Custom -> "Custom permissions" + FileDrop -> R.string.share_permission_file_drop + CanView -> R.string.share_permission_can_view + CanEdit -> R.string.share_permission_can_edit + is Custom -> R.string.share_permission_custom } } + + fun customFlag(selector: Custom.() -> Boolean): Boolean = + (this as? Custom)?.selector() ?: false } +fun UnifiedSharePermission?.customPermissionRead(): Boolean = this?.customFlag { read } ?: false +fun UnifiedSharePermission?.customPermissionEdit(): Boolean = this?.customFlag { edit } ?: false +fun UnifiedSharePermission?.customPermissionDelete(): Boolean = this?.customFlag { delete } ?: false +fun UnifiedSharePermission?.customPermissionCreate(): Boolean = this?.customFlag { create } ?: false + +data class CustomPermissionField( + val labelRes: Int, + val getValue: (UnifiedSharePermission.Custom) -> Boolean, + val setValue: (UnifiedSharePermission.Custom, Boolean) -> UnifiedSharePermission.Custom +) + +val customPermissionFields = listOf( + CustomPermissionField( + labelRes = R.string.share_view_view_files_switch, + getValue = { it.read }, + setValue = { p, v -> p.copy(read = v) } + ), + CustomPermissionField( + labelRes = R.string.share_view_edit_files_switch, + getValue = { it.edit }, + setValue = { p, v -> p.copy(edit = v) } + ), + CustomPermissionField( + labelRes = R.string.share_view_create_files_switch, + getValue = { it.create }, + setValue = { p, v -> p.copy(create = v) } + ), + CustomPermissionField( + labelRes = R.string.share_view_delete_files_switch, + getValue = { it.delete }, + setValue = { p, v -> p.copy(delete = v) } + ), +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt index c77094bc..5690cb5f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt @@ -11,19 +11,39 @@ import com.nextcloud.android.common.ui.share.model.api.owner.Owner import com.nextcloud.android.common.ui.share.model.api.user.ShareUser data class UnifiedShare( - val id: String, + val id: String?, val sources: List, val recipients: List, val properties: Map>>, val lastUpdated: Long, - val owner: Owner, + val owner: Owner?, - val permission: UnifiedSharePermission, - val type: UnifiedShareType, + val permission: UnifiedSharePermission?, + val type: UnifiedShareType?, val category: UnifiedShareCategory, val label: String, val note: String = "", val password: String = "", val limit: UnifiedShareDownloadLimit? = null -) +) { + companion object { + fun new(): UnifiedShare { + return UnifiedShare( + id = null, + sources = listOf(), + recipients = listOf(), + properties = mapOf(), + lastUpdated = -1, + owner = null, + permission = null, + type = null, + category = UnifiedShareCategory.Invited, + label = "", + note = "", + password = "", + limit = null + ) + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt deleted file mode 100644 index 53be607c..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/previews/SharePreviews.kt +++ /dev/null @@ -1,790 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.previews - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.nextcloud.android.common.ui.share.AnyoneShareContent -import com.nextcloud.android.common.ui.share.CollapsibleSettingsSection -import com.nextcloud.android.common.ui.share.InvitedShareContent -import com.nextcloud.android.common.ui.share.NoteToRecipients -import com.nextcloud.android.common.ui.share.PermissionDropdown -import com.nextcloud.android.common.ui.share.SettingsSwitchRow -import com.nextcloud.android.common.ui.share.ShareActionButtons -import com.nextcloud.android.common.ui.share.ShareBottomSheetHeader -import com.nextcloud.android.common.ui.share.ShareCategoryButtonGroup -import com.nextcloud.android.common.ui.share.ShareViewModel -import com.nextcloud.android.common.ui.share.UnifiedShareView -import com.nextcloud.android.common.ui.share.UnifiedSharesListItem -import com.nextcloud.android.common.ui.share.UnifiedSharesListItemType -import com.nextcloud.android.common.ui.share.model.api.owner.Owner -import com.nextcloud.android.common.ui.share.model.api.user.ShareUser -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareDownloadLimit -import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType -import com.nextcloud.android.common.ui.share.repository.MockShareRepository - -@Composable -private fun PreviewTheme( - darkTheme: Boolean = false, - content: @Composable () -> Unit -) { - MaterialTheme { - Surface(content = content) - } -} - -@Preview(name = "UnifiedShareView – light", showBackground = true) -@Composable -fun Preview_UnifiedShareView_Light() { - PreviewTheme { - UnifiedShareView(viewModel = ShareViewModel(MockShareRepository())) - } -} - -@Preview(name = "UnifiedShareView – dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun Preview_UnifiedShareView_Dark() { - PreviewTheme(darkTheme = true) { - UnifiedShareView(viewModel = ShareViewModel(MockShareRepository())) - } -} - -@Preview(name = "ListItem – Top", showBackground = true, group = "List Item") -@Composable -fun Preview_UnifiedSharesListItem_Top() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewUserShare(), - type = UnifiedSharesListItemType.Top - ) - } - } -} - -@Preview(name = "ListItem – Mid", showBackground = true, group = "List Item") -@Composable -fun Preview_UnifiedSharesListItem_Mid() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewGroupShare(), - type = UnifiedSharesListItemType.Mid - ) - } - } -} - -@Preview(name = "ListItem – Bottom", showBackground = true, group = "List Item") -@Composable -fun Preview_UnifiedSharesListItem_Bottom() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewPublicLinkShare(), - type = UnifiedSharesListItemType.Bottom - ) - } - } -} - -@Preview(name = "ListItem – Single (all three stacked)", showBackground = true, group = "List Item") -@Composable -fun Preview_UnifiedSharesListItem_AllTypes() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper(previewUserShare(), UnifiedSharesListItemType.Top) - UnifiedSharesListItemPreviewHelper(previewGroupShare(), UnifiedSharesListItemType.Mid) - UnifiedSharesListItemPreviewHelper(previewPublicLinkShare(), UnifiedSharesListItemType.Bottom) - } - } -} - -@Composable -private fun UnifiedSharesListItemPreviewHelper(share: UnifiedShare, type: UnifiedSharesListItemType) { - UnifiedSharesListItem(share = share, type = type) -} - -@Preview(name = "ListItem – CanView permission", showBackground = true, group = "Permissions") -@Composable -fun Preview_ListItem_CanView() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewUserShare(permission = UnifiedSharePermission.CanView), - type = UnifiedSharesListItemType.Top - ) - } - } -} - -@Preview(name = "ListItem – CanEdit permission", showBackground = true, group = "Permissions") -@Composable -fun Preview_ListItem_CanEdit() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewUserShare(permission = UnifiedSharePermission.CanEdit), - type = UnifiedSharesListItemType.Top - ) - } - } -} - -@Preview(name = "ListItem – FileDrop permission", showBackground = true, group = "Permissions") -@Composable -fun Preview_ListItem_FileDrop() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewUserShare(permission = UnifiedSharePermission.FileDrop), - type = UnifiedSharesListItemType.Top - ) - } - } -} - -@Preview(name = "ListItem – Custom permission", showBackground = true, group = "Permissions") -@Composable -fun Preview_ListItem_CustomPermission() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemPreviewHelper( - share = previewUserShare( - permission = UnifiedSharePermission.Custom( - true, - false, - true, - false - ) - ), - type = UnifiedSharesListItemType.Top - ) - } - } -} - -@Preview( - name = "BottomSheet – Invited / default", - showBackground = true, - heightDp = 900, - group = "Bottom Sheet" -) -@Composable -fun Preview_AddShareBottomSheet_Invited() { - PreviewTheme { - AddShareBottomSheetContentPreview( - category = UnifiedShareCategory.Invited, - permission = UnifiedSharePermission.CanView, - searchQuery = "", - note = "", - showSettings = false - ) - } -} - -@Preview( - name = "BottomSheet – Invited / search filled", - showBackground = true, - heightDp = 900, - group = "Bottom Sheet" -) -@Composable -fun Preview_AddShareBottomSheet_InvitedWithSearch() { - PreviewTheme { - AddShareBottomSheetContentPreview( - category = UnifiedShareCategory.Invited, - permission = UnifiedSharePermission.CanEdit, - searchQuery = "alice@nextcloud.example", - note = "Here are the Q2 reports!", - showSettings = false - ) - } -} - -@Preview( - name = "BottomSheet – Invited / settings expanded", - showBackground = true, - heightDp = 1100, - group = "Bottom Sheet" -) -@Composable -fun Preview_AddShareBottomSheet_InvitedSettingsExpanded() { - PreviewTheme { - AddShareBottomSheetContentPreview( - category = UnifiedShareCategory.Invited, - permission = UnifiedSharePermission.CanView, - searchQuery = "bob", - note = "", - showSettings = true - ) - } -} - -@Preview( - name = "BottomSheet – Anyone / default", - showBackground = true, - heightDp = 900, - group = "Bottom Sheet" -) -@Composable -fun Preview_AddShareBottomSheet_Anyone() { - PreviewTheme { - AddShareBottomSheetContentPreview( - category = UnifiedShareCategory.Anyone, - permission = UnifiedSharePermission.CanView, - searchQuery = "", - note = "", - showSettings = false - ) - } -} - -@Preview( - name = "BottomSheet – Anyone / Custom permission (extra switches)", - showBackground = true, - heightDp = 1100, - group = "Bottom Sheet" -) -@Composable -fun Preview_AddShareBottomSheet_AnyoneCustomPermission() { - PreviewTheme { - AddShareBottomSheetContentPreview( - category = UnifiedShareCategory.Anyone, - permission = UnifiedSharePermission.Custom( - true, - false, - false, - false - ), - searchQuery = "", - note = "", - showSettings = false - ) - } -} - -@Preview( - name = "BottomSheet – Anyone / settings expanded", - showBackground = true, - heightDp = 1200, - group = "Bottom Sheet" -) -@Composable -fun Preview_AddShareBottomSheet_AnyoneSettingsExpanded() { - PreviewTheme { - AddShareBottomSheetContentPreview( - category = UnifiedShareCategory.Anyone, - permission = UnifiedSharePermission.CanView, - searchQuery = "", - note = "Public note", - showSettings = true - ) - } -} - -@Preview(name = "CategoryButtons – Invited selected", showBackground = true, group = "Category Buttons") -@Composable -fun Preview_ShareCategoryButtonGroup_Invited() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - ShareCategoryButtonGroup( - selectedCategory = UnifiedShareCategory.Invited, - onCategoryChange = {} - ) - } - } -} - -@Preview(name = "CategoryButtons – Anyone selected", showBackground = true, group = "Category Buttons") -@Composable -fun Preview_ShareCategoryButtonGroup_Anyone() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - ShareCategoryButtonGroup( - selectedCategory = UnifiedShareCategory.Anyone, - onCategoryChange = {} - ) - } - } -} - -@Preview(name = "ActionButtons – Invited / Send disabled", showBackground = true, group = "Action Buttons") -@Composable -fun Preview_ShareActionButtons_InvitedSendDisabled() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - ShareActionButtons( - category = UnifiedShareCategory.Invited, - isSendEnabled = false, - onCopyClick = {}, - onSendClick = {} - ) - } - } -} - -@Preview(name = "ActionButtons – Invited / Send enabled", showBackground = true, group = "Action Buttons") -@Composable -fun Preview_ShareActionButtons_InvitedSendEnabled() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - ShareActionButtons( - category = UnifiedShareCategory.Invited, - isSendEnabled = true, - onCopyClick = {}, - onSendClick = {} - ) - } - } -} - -@Preview(name = "ActionButtons – Anyone", showBackground = true, group = "Action Buttons") -@Composable -fun Preview_ShareActionButtons_Anyone() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - ShareActionButtons( - category = UnifiedShareCategory.Anyone, - isSendEnabled = false, - onCopyClick = {}, - onSendClick = {} - ) - } - } -} - -@Preview(name = "CollapsibleSettings – collapsed", showBackground = true, group = "Settings Section") -@Composable -fun Preview_CollapsibleSettingsSection_Collapsed() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - CollapsibleSettingsSection(isExpanded = false, onToggle = {}) { - InvitedInlineSettingsPreview() - } - } - } -} - -@Preview(name = "CollapsibleSettings – expanded (Invited)", showBackground = true, group = "Settings Section") -@Composable -fun Preview_CollapsibleSettingsSection_ExpandedInvited() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - CollapsibleSettingsSection(isExpanded = true, onToggle = {}) { - InvitedInlineSettingsPreview() - } - } - } -} - -@Preview(name = "CollapsibleSettings – expanded (Anyone)", showBackground = true, group = "Settings Section") -@Composable -fun Preview_CollapsibleSettingsSection_ExpandedAnyone() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - CollapsibleSettingsSection(isExpanded = true, onToggle = {}) { - AnyoneInlineSettingsPreview() - } - } - } -} - -private val allPermissions = listOf( - UnifiedSharePermission.CanView, - UnifiedSharePermission.CanEdit, - UnifiedSharePermission.FileDrop, - UnifiedSharePermission.Custom(false, false, false, false) -) - -@Preview(name = "PermissionDropdown – CanView", showBackground = true, group = "Permission Dropdown") -@Composable -fun Preview_PermissionDropdown_CanView() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - PermissionDropdown( - label = "Participants", - selectedPermission = UnifiedSharePermission.CanView, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "PermissionDropdown – CanEdit", showBackground = true, group = "Permission Dropdown") -@Composable -fun Preview_PermissionDropdown_CanEdit() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - PermissionDropdown( - label = "Anyone with the link", - selectedPermission = UnifiedSharePermission.CanEdit, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "PermissionDropdown – FileDrop", showBackground = true, group = "Permission Dropdown") -@Composable -fun Preview_PermissionDropdown_FileDrop() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - PermissionDropdown( - label = "Anyone with the link", - selectedPermission = UnifiedSharePermission.FileDrop, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "PermissionDropdown – Custom", showBackground = true, group = "Permission Dropdown") -@Composable -fun Preview_PermissionDropdown_Custom() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - PermissionDropdown( - label = "Participants", - selectedPermission = UnifiedSharePermission.Custom(true, false, false, false), - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "NoteToRecipients – empty", showBackground = true, group = "Note") -@Composable -fun Preview_NoteToRecipients_Empty() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - NoteToRecipients(note = "", onNoteChange = {}) - } - } -} - -@Preview(name = "NoteToRecipients – with text", showBackground = true, group = "Note") -@Composable -fun Preview_NoteToRecipients_WithText() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - NoteToRecipients(note = "Please review by end of week!", onNoteChange = {}) - } - } -} - -@Preview(name = "InvitedShareContent – empty query", showBackground = true, group = "Invited Content") -@Composable -fun Preview_InvitedShareContent_EmptyQuery() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - InvitedShareContent( - searchQuery = "", - onSearchChange = {}, - permission = UnifiedSharePermission.CanView, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "InvitedShareContent – with query", showBackground = true, group = "Invited Content") -@Composable -fun Preview_InvitedShareContent_WithQuery() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - InvitedShareContent( - searchQuery = "carol@company.org", - onSearchChange = {}, - permission = UnifiedSharePermission.CanEdit, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "AnyoneShareContent – CanView", showBackground = true, group = "Anyone Content") -@Composable -fun Preview_AnyoneShareContent_CanView() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - AnyoneShareContent( - permission = UnifiedSharePermission.CanView, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "AnyoneShareContent – FileDrop", showBackground = true, group = "Anyone Content") -@Composable -fun Preview_AnyoneShareContent_FileDrop() { - PreviewTheme { - Box(Modifier.padding(16.dp)) { - AnyoneShareContent( - permission = UnifiedSharePermission.FileDrop, - availablePermissions = allPermissions, - onPermissionChange = {} - ) - } - } -} - -@Preview(name = "SwitchRow – off", showBackground = true, group = "Switch Row") -@Composable -fun Preview_SettingsSwitchRow_Off() { - PreviewTheme { - Box(Modifier.padding(horizontal = 16.dp)) { - SettingsSwitchRow(label = "Hide download and sync options", checked = false, onCheckedChange = {}) - } - } -} - -@Preview(name = "SwitchRow – on", showBackground = true, group = "Switch Row") -@Composable -fun Preview_SettingsSwitchRow_On() { - PreviewTheme { - Box(Modifier.padding(horizontal = 16.dp)) { - SettingsSwitchRow(label = "Expiration date", checked = true, onCheckedChange = {}) - } - } -} - -@Preview(name = "Item shape – all types", showBackground = true, group = "Shape") -@Composable -fun Preview_ItemShapes_AllTypes() { - PreviewTheme { - Column(Modifier.padding(8.dp)) { - UnifiedSharesListItemType.entries.forEach { type -> - UnifiedSharesListItemPreviewHelper(share = previewUserShare(), type = type) - } - } - } -} - -@Preview( - name = "UnifiedShareView – tablet landscape", - showBackground = true, - widthDp = 840, - heightDp = 600 -) -@Composable -fun Preview_UnifiedShareView_Tablet() { - PreviewTheme { - UnifiedShareView(viewModel = ShareViewModel(MockShareRepository())) - } -} - -@Composable -private fun AddShareBottomSheetContentPreview( - category: UnifiedShareCategory, - permission: UnifiedSharePermission, - searchQuery: String, - note: String, - showSettings: Boolean -) { - val availablePermissions = listOf( - UnifiedSharePermission.CanView, - UnifiedSharePermission.CanEdit, - UnifiedSharePermission.FileDrop, - UnifiedSharePermission.Custom(false, false, false, false) - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 32.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - ShareBottomSheetHeader(filename = "Abc.txt") - - ShareCategoryButtonGroup( - selectedCategory = category, - onCategoryChange = {} - ) - - if (category == UnifiedShareCategory.Invited) { - InvitedShareContent( - searchQuery = searchQuery, - onSearchChange = {}, - permission = permission, - availablePermissions = availablePermissions, - onPermissionChange = {} - ) - CollapsibleSettingsSection( - isExpanded = showSettings, - onToggle = {} - ) { - InvitedInlineSettingsPreview() - } - } else { - AnyoneShareContent( - permission = permission, - availablePermissions = availablePermissions, - onPermissionChange = {} - ) - if (permission is UnifiedSharePermission.Custom) { - SettingsSwitchRow("View files", false) {} - SettingsSwitchRow("Edit files", false) {} - SettingsSwitchRow("Create files", false) {} - SettingsSwitchRow("Delete files", false) {} - } - CollapsibleSettingsSection( - isExpanded = showSettings, - onToggle = {} - ) { - AnyoneInlineSettingsPreview() - } - } - - NoteToRecipients(note = note, onNoteChange = {}) - - ShareActionButtons( - category = category, - isSendEnabled = searchQuery.isNotBlank(), - onCopyClick = {}, - onSendClick = {} - ) - } -} - -@Composable -private fun InvitedInlineSettingsPreview() { - Column { - SettingsSwitchRow("Share with others", false) {} - SettingsSwitchRow("Edit file", false) {} - SettingsSwitchRow("Expiration date", true) {} - SettingsSwitchRow("Hide download and sync options", false) {} - } -} - -@Composable -private fun AnyoneInlineSettingsPreview() { - Column { - OutlinedTextField( - value = "Public reports link", - onValueChange = {}, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - label = { Text("Label") }, - placeholder = { Text("Optional name for this link") }, - singleLine = true - ) - SettingsSwitchRow("Expiration date", false) {} - SettingsSwitchRow("Password", true) {} - SettingsSwitchRow("Limit downloads", false) {} - SettingsSwitchRow("Hide downloads", false) {} - SettingsSwitchRow("Video verification", false) {} - SettingsSwitchRow("Show files in grid view", true) {} - } -} - -private fun previewUserShare( - permission: UnifiedSharePermission = UnifiedSharePermission.CanView -) = UnifiedShare( - id = "1", - sources = listOf( - ShareUser( - type = "user", - value = "alice@company.com", - displayName = "Alice Smith" - ) - ), - recipients = emptyList(), - properties = emptyMap(), - lastUpdated = 0, - owner = Owner( - userId = "alice", - displayName = "Alice Smith" - ), - label = "Alice Smith", - type = UnifiedShareType.InternalUser, - permission = permission, - category = UnifiedShareCategory.Invited, - note = "", - password = "", - limit = UnifiedShareDownloadLimit(0, 0) -) - -private fun previewGroupShare( - permission: UnifiedSharePermission = UnifiedSharePermission.CanEdit -) = UnifiedShare( - id = "2", - sources = listOf( - ShareUser( - type = "group", - value = "design", - displayName = "Design Team" - ) - ), - recipients = emptyList(), - properties = emptyMap(), - lastUpdated = 0, - owner = Owner( - userId = "system", - displayName = "System" - ), - label = "Design Team", - type = UnifiedShareType.InternalGroup, - permission = permission, - category = UnifiedShareCategory.Invited, - note = "", - password = "", - limit = UnifiedShareDownloadLimit(0, 0) -) - -private fun previewPublicLinkShare( - permission: UnifiedSharePermission = UnifiedSharePermission.FileDrop -) = UnifiedShare( - id = "3", - sources = listOf( - ShareUser( - type = "link", - value = "https://nextcloud.com/s/abc123", - displayName = "Public Link" - ) - ), - recipients = emptyList(), - properties = emptyMap(), - lastUpdated = 1710000000, - owner = Owner( - userId = "system", - displayName = "System" - ), - label = "Public link", - type = UnifiedShareType.ExternalLink, - permission = permission, - category = UnifiedShareCategory.Anyone, - note = "", - password = "1234", - limit = UnifiedShareDownloadLimit(50, 5) -) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml new file mode 100644 index 00000000..f341dc87 --- /dev/null +++ b/ui/src/main/res/values/strings.xml @@ -0,0 +1,60 @@ + + + + Create a new share + Share %s + + File drop + Can view + Can edit + Custom permissions + + View files + Edit files + Create files + Delete files + + Settings + Add people + Name, team, email or federated ID + Participants + + Note to recipients + + Anyone with the link + + Share with others + Edit file + Expiration date + Hide download and sync options + + + Label + Optional name for this link + Expiration date + Password protection + Limit downloads + Hide downloads + Video verification + + Show files in grid view + Copied + Copy link + Send + Create public link + + Edit + Send email + Delete + + Failed to fetch shares + + Share not found, cannot delete + Failed to delete share + + \ No newline at end of file From 622286e886a9b7060c8bf0c9110cba1f8adfb5f7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 12:44:48 +0200 Subject: [PATCH 005/174] add translations, bind ui actions Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareView.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index caed3858..f043323d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -92,10 +92,6 @@ import com.nextcloud.android.common.ui.share.repository.MockShareRepository import kotlinx.coroutines.launch -// TODO: MOVE TO THE ANDROID: COMMON -// TODO: MAKE LAZY COLUMN -// TODO: EXPOSE ACTIONS, IMPLEMENT VIEWMODEL, REPOSITORY TO FETCH ACTUAL SHARE, INJECT NECESSARY PARAMETERS - @Composable private fun ShareView(viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() @@ -170,7 +166,8 @@ private fun ShareView(viewModel: ShareViewModel) { } } -// 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: 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 AddOrEditShareBottomSheet(title: String, share: UnifiedShare, onDismiss: () -> Unit) { From c82dda5a2adbb183e2ec8e6294e79db46f6211d0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 15:25:47 +0200 Subject: [PATCH 006/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 39 ++++++++++++++++--- .../android/common/ui/share/ShareViewModel.kt | 11 ++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index f043323d..6a2c51ea 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -149,6 +149,9 @@ private fun ShareView(viewModel: ShareViewModel) { AddOrEditShareBottomSheet( title = stringResource(R.string.share_view_bottom_sheet_edit_title, state.share.label), share = state.share, + onCreateOrEdit = { + + }, onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } ) } @@ -158,6 +161,9 @@ private fun ShareView(viewModel: ShareViewModel) { AddOrEditShareBottomSheet( title = stringResource(R.string.share_view_bottom_sheet_new_title), share = state.newShare, + onCreateOrEdit = { + + }, onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } ) } @@ -170,7 +176,12 @@ private fun ShareView(viewModel: ShareViewModel) { // should look like User 1, Group 1 etc. @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun AddOrEditShareBottomSheet(title: String, share: UnifiedShare, onDismiss: () -> Unit) { +private fun AddOrEditShareBottomSheet( + title: String, + share: UnifiedShare, + onCreateOrEdit: () -> Unit, + onDismiss: () -> Unit +) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() @@ -277,7 +288,9 @@ private fun AddOrEditShareBottomSheet(title: String, share: UnifiedShare, onDism clipboard.setClipEntry(clipData.toClipEntry()) } }, - onSendClick = { /* TODO */ } + onSendClick = { + onCreateOrEdit() + } ) } } @@ -451,10 +464,19 @@ private fun InvitedInlineSettings(share: UnifiedShare) { var hasExpiration by remember { mutableStateOf(false) } // TODO var hideDownload by remember { mutableStateOf(false) } // TODO - SettingsSwitchRow(stringResource(R.string.share_view_invited_category_share_with_others_switch), shareWithOthers) { shareWithOthers = it } + SettingsSwitchRow( + stringResource(R.string.share_view_invited_category_share_with_others_switch), + shareWithOthers + ) { shareWithOthers = it } SettingsSwitchRow(stringResource(R.string.share_view_invited_category_edit_file_switch), editFile) { editFile = it } - SettingsSwitchRow(stringResource(R.string.share_view_invited_category_expiration_date_switch), hasExpiration) { hasExpiration = it } - SettingsSwitchRow(stringResource(R.string.share_view_invited_category_hide_and_download_switch), hideDownload) { hideDownload = it } + SettingsSwitchRow( + stringResource(R.string.share_view_invited_category_expiration_date_switch), + hasExpiration + ) { hasExpiration = it } + SettingsSwitchRow( + stringResource(R.string.share_view_invited_category_hide_and_download_switch), + hideDownload + ) { hideDownload = it } } @Composable @@ -655,7 +677,12 @@ private fun UnifiedSharesListItem( HorizontalDivider() DropdownMenuItem( - text = { Text(stringResource(R.string.share_view_list_item_delete), color = MaterialTheme.colorScheme.error) }, + text = { + Text( + stringResource(R.string.share_view_list_item_delete), + color = MaterialTheme.colorScheme.error + ) + }, onClick = { onDeleteShare(share) showContextMenu = false diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 39f948e3..739bb35c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers @@ -58,6 +59,16 @@ class ShareViewModel( // endregion // region public methods + fun create(share: UnifiedShare) { + viewModelScope.launch(Dispatchers.IO) { + /* + val request = CreateShareRequest() + repository.createShare() + */ + + } + } + fun delete(share: UnifiedShare) { viewModelScope.launch(Dispatchers.IO) { val id = share.id From e91eba155738062246402a6e7c2d7ec9b2733e8a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 14:02:16 +0200 Subject: [PATCH 007/174] wip Signed-off-by: alperozturk96 # Conflicts: # gradle/verification-metadata.xml # sample/build.gradle --- .../android/common/sample/MainActivity.kt | 11 +++ .../android/common/sample/MainViewModel.kt | 32 +++++++- sample/src/main/res/layout/activity_main.xml | 61 +++++++++++++++ sample/src/main/res/values/strings.xml | 4 + ui/build.gradle | 11 ++- .../common/ui/network/NextcloudCredentials.kt | 14 ++++ .../common/ui/network/NextcloudHttpClient.kt | 74 +++++++++++++++++++ .../common/ui/network/PredefinedStatus.kt | 24 ++++++ .../common/ui/network/UserStatusService.kt | 61 +++++++++++++++ 9 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt index ed29a272..f46c2704 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt @@ -85,6 +85,17 @@ class MainActivity : AppCompatActivity() { material.colorMaterialButtonPrimaryBorderless(negativeButton) } + binding.testApiBtn.setOnClickListener { + val baseUrl = binding.baseUrl.text?.toString().orEmpty() + val username = binding.username.text?.toString().orEmpty() + val token = binding.token.text?.toString().orEmpty() + mainViewModel.testPredefinedStatuses(baseUrl, username, token) + } + + mainViewModel.apiTestResult.observe(this) { result -> + Toast.makeText(this, result, Toast.LENGTH_LONG).show() + } + setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) mainViewModel.color.observe(this) { applyTheme(it) } diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 457600c7..b69a012a 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -9,7 +9,37 @@ package com.nextcloud.android.common.sample import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.android.common.ui.network.ApiCredentials +import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.network.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.UserStatusService +import kotlinx.coroutines.launch class MainViewModel : ViewModel() { val color = MutableLiveData() -} + val apiTestResult = MutableLiveData() + + fun testPredefinedStatuses( + baseUrl: String, + username: String, + token: String + ) { + viewModelScope.launch { + val credentials = ApiCredentials(baseUrl, username, token) + val client = NextcloudHttpClient.create(credentials, enableLogging = true) + val service = UserStatusService(client) + + when (val result = service.fetchPredefinedStatuses()) { + is ApiResult.Success -> + apiTestResult.value = + "✅ Success (${result.data.size} statuses):\n" + + result.data.joinToString("\n") { "${it.icon} ${it.message}" } + + is ApiResult.Error -> + apiTestResult.value = + "❌ Error ${result.error.ocs.meta.statusCode}: ${result.error.ocs.meta.message}" + } + } + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 23447605..2e4f232b 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -193,6 +193,67 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/circular_progress_bar" /> + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 1b957aa2..f7db289e 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -19,6 +19,10 @@ Suggestion Chip Filter Chip Color + Base URL + Username + App token + Test User Status API Theming UI Module \ No newline at end of file diff --git a/ui/build.gradle b/ui/build.gradle index d04aa1d5..f81e5bac 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -7,6 +7,7 @@ plugins { id 'org.jetbrains.kotlin.plugin.compose' + id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion" apply false id 'com.android.library' id 'com.android.built-in-kotlin' id 'com.android.legacy-kapt' @@ -45,8 +46,8 @@ android { } dependencies { - implementation 'androidx.compose.ui:ui-tooling-preview:1.10.6' - debugImplementation 'androidx.compose.ui:ui-tooling:1.10.6' + implementation 'androidx.compose.ui:ui-tooling-preview:1.11.0' + debugImplementation 'androidx.compose.ui:ui-tooling:1.11.0' kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}" implementation(platform("androidx.compose:compose-bom:2026.08.00")) @@ -63,6 +64,12 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.2")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") + implementation project(':core') api project(':material-color-utilities') diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt new file mode 100644 index 00000000..4194b679 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +data class ApiCredentials( + val baseURL: String, + val username: String, + val token: String +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt new file mode 100644 index 00000000..54eb8e9b --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import java.util.concurrent.TimeUnit + +class NextcloudHttpClient private constructor( + val okHttpClient: OkHttpClient, + val credentials: ApiCredentials +) { + companion object { + private const val CONNECT_TIMEOUT_SECONDS = 30L + private const val READ_TIMEOUT_SECONDS = 30L + private const val WRITE_TIMEOUT_SECONDS = 30L + + fun create( + credentials: ApiCredentials, + enableLogging: Boolean = false + ): NextcloudHttpClient { + val authInterceptor = AuthInterceptor(credentials) + + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = if (enableLogging) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + + val okHttpClient = OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .addInterceptor(authInterceptor) + .addInterceptor(loggingInterceptor) + .build() + + return NextcloudHttpClient(okHttpClient, credentials) + } + } + + private class AuthInterceptor(private val credentials: ApiCredentials) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val basicCredentials = Credentials.basic(credentials.username, credentials.token) + + val request = chain.request() + .newBuilder() + .header("Authorization", basicCredentials) + .header("OCS-APIRequest", "true") + .url(buildUrl(chain.request().url.toString(), credentials.baseURL)) + .build() + + return chain.proceed(request) + } + + private fun buildUrl(requestUrl: String, baseUrl: String): String { + return if (requestUrl.startsWith("http://") || requestUrl.startsWith("https://")) { + requestUrl + } else { + "${baseUrl.trimEnd('/')}/${requestUrl.trimStart('/')}" + } + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt new file mode 100644 index 00000000..35918558 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +import kotlinx.serialization.Serializable + +@Serializable +data class ClearAt( + val type: String, + val time: Int +) + +@Serializable +data class PredefinedStatus( + val id: String, + val icon: String, + val message: String, + val clearAt: ClearAt? = null +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt new file mode 100644 index 00000000..7d466eef --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import okhttp3.Request + +class UserStatusService(private val client: NextcloudHttpClient) { + + private val json = Json { ignoreUnknownKeys = true } + + suspend fun fetchPredefinedStatuses(): ApiResult> = + withContext(Dispatchers.IO) { + val url = + "${client.credentials.baseURL.trimEnd('/')}" + + "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses" + + val request = + Request + .Builder() + .url(url) + .header("Accept", "application/json") + .build() + + try { + val response = client.okHttpClient.newCall(request).execute() + val body = response.body?.string().orEmpty() + + if (response.isSuccessful) { + val parsed = json.decodeFromString>>(body) + ApiResult.Success(parsed.ocs.data) + } else { + val error = json.decodeFromString>(body) + ApiResult.Error(error) + } + } catch (e: Exception) { + ApiResult.Error( + OcsResponse( + Ocs( + meta = + Meta( + status = "error", + statusCode = -1, + message = e.message ?: "Unknown error", + totalItems = "", + itemsPerPage = "" + ), + data = e.message ?: "Unknown error" + ) + ) + ) + } + } +} From ab64e412704232ebf7c3f5e935be75bda6c1de23 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Apr 2026 10:39:10 +0200 Subject: [PATCH 008/174] add test ocs call Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/sample/MainViewModel.kt | 8 ++++---- .../android/common/ui/network/UserStatusService.kt | 7 ++++++- .../{NextcloudCredentials.kt => api/ApiCredentials.kt} | 2 +- .../{NextcloudHttpClient.kt => api/ApiHttpClient.kt} | 8 ++++---- .../android/common/ui/network/{ => model}/ApiResult.kt | 2 +- .../android/common/ui/network/{ => model}/OcsResponse.kt | 2 +- .../nextcloud/android/common/ui/share/ShareViewModel.kt | 3 +-- .../common/ui/share/repository/MockShareRepository.kt | 2 +- .../common/ui/share/repository/ShareRemoteRepository.kt | 2 +- .../android/common/ui/share/repository/ShareRepository.kt | 2 +- 10 files changed, 21 insertions(+), 17 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/network/{NextcloudCredentials.kt => api/ApiCredentials.kt} (83%) rename ui/src/main/java/com/nextcloud/android/common/ui/network/{NextcloudHttpClient.kt => api/ApiHttpClient.kt} (92%) rename ui/src/main/java/com/nextcloud/android/common/ui/network/{ => model}/ApiResult.kt (85%) rename ui/src/main/java/com/nextcloud/android/common/ui/network/{ => model}/OcsResponse.kt (92%) diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index b69a012a..220effdc 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -10,9 +10,9 @@ package com.nextcloud.android.common.sample import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.android.common.ui.network.ApiCredentials -import com.nextcloud.android.common.ui.network.ApiResult -import com.nextcloud.android.common.ui.network.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.api.ApiCredentials +import com.nextcloud.android.common.ui.network.model.ApiResult +import com.nextcloud.android.common.ui.network.api.ApiHttpClient import com.nextcloud.android.common.ui.network.UserStatusService import kotlinx.coroutines.launch @@ -27,7 +27,7 @@ class MainViewModel : ViewModel() { ) { viewModelScope.launch { val credentials = ApiCredentials(baseUrl, username, token) - val client = NextcloudHttpClient.create(credentials, enableLogging = true) + val client = ApiHttpClient.create(credentials, enableLogging = true) val service = UserStatusService(client) when (val result = service.fetchPredefinedStatuses()) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt index 7d466eef..9fa314ee 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt @@ -7,12 +7,17 @@ package com.nextcloud.android.common.ui.network +import com.nextcloud.android.common.ui.network.api.ApiHttpClient +import com.nextcloud.android.common.ui.network.model.ApiResult +import com.nextcloud.android.common.ui.network.model.Meta +import com.nextcloud.android.common.ui.network.model.Ocs +import com.nextcloud.android.common.ui.network.model.OcsResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.Request -class UserStatusService(private val client: NextcloudHttpClient) { +class UserStatusService(private val client: ApiHttpClient) { private val json = Json { ignoreUnknownKeys = true } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt similarity index 83% rename from ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt index 4194b679..f6770090 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudCredentials.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.network +package com.nextcloud.android.common.ui.network.api data class ApiCredentials( val baseURL: String, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt similarity index 92% rename from ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt index 54eb8e9b..499207a8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.network +package com.nextcloud.android.common.ui.network.api import okhttp3.Credentials import okhttp3.Interceptor @@ -14,7 +14,7 @@ import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit -class NextcloudHttpClient private constructor( +class ApiHttpClient private constructor( val okHttpClient: OkHttpClient, val credentials: ApiCredentials ) { @@ -26,7 +26,7 @@ class NextcloudHttpClient private constructor( fun create( credentials: ApiCredentials, enableLogging: Boolean = false - ): NextcloudHttpClient { + ): ApiHttpClient { val authInterceptor = AuthInterceptor(credentials) val loggingInterceptor = HttpLoggingInterceptor().apply { @@ -45,7 +45,7 @@ class NextcloudHttpClient private constructor( .addInterceptor(loggingInterceptor) .build() - return NextcloudHttpClient(okHttpClient, credentials) + return ApiHttpClient(okHttpClient, credentials) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt similarity index 85% rename from ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt index 1827178f..9ab9328b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/ApiResult.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.network +package com.nextcloud.android.common.ui.network.model sealed class ApiResult { data class Success(val data: T) : ApiResult() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt similarity index 92% rename from ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt index 1dc0e7d9..d0c361e5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/OcsResponse.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.network +package com.nextcloud.android.common.ui.network.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 739bb35c..5621326e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -10,8 +10,7 @@ package com.nextcloud.android.common.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.network.ApiResult -import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest +import com.nextcloud.android.common.ui.network.model.ApiResult import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 73aa1227..053d951c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -7,7 +7,7 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.network.model.ApiResult import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.owner.Owner diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index bea010e3..6551f27f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -7,7 +7,7 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.network.model.ApiResult import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index c6bb626d..3267d9db 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -7,7 +7,7 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.network.ApiResult +import com.nextcloud.android.common.ui.network.model.ApiResult import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients From 2a2f5b8e15b54c03971a0bd6893e3f83198c2d25 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 14:11:09 +0200 Subject: [PATCH 009/174] fix test call Signed-off-by: alperozturk96 --- sample/src/main/AndroidManifest.xml | 1 + ui/build.gradle | 2 +- .../common/ui/network/PredefinedStatus.kt | 21 ++++++++++++++++++- .../common/ui/network/UserStatusService.kt | 4 ++-- .../common/ui/network/model/OcsResponse.kt | 4 ++-- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index bc5dab68..153a8ae0 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" + android:usesCleartextTraffic="true" android:theme="@style/Theme.Androidcommon"> { + override val descriptor = PrimitiveSerialDescriptor("ClearAtTime", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): String { + val jsonDecoder = decoder as? JsonDecoder ?: return decoder.decodeString() + return (jsonDecoder.decodeJsonElement() as? JsonPrimitive)?.content ?: "" + } + + override fun serialize(encoder: Encoder, value: String) = encoder.encodeString(value) +} @Serializable data class ClearAt( val type: String, - val time: Int + @Serializable(with = ClearAtTimeSerializer::class) + val time: String ) @Serializable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt index 9fa314ee..73f9a3a0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt @@ -24,7 +24,7 @@ class UserStatusService(private val client: ApiHttpClient) { suspend fun fetchPredefinedStatuses(): ApiResult> = withContext(Dispatchers.IO) { val url = - "${client.credentials.baseURL.trimEnd('/')}" + + client.credentials.baseURL.trimEnd('/') + "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses" val request = @@ -36,7 +36,7 @@ class UserStatusService(private val client: ApiHttpClient) { try { val response = client.okHttpClient.newCall(request).execute() - val body = response.body?.string().orEmpty() + val body = response.body.string() if (response.isSuccessful) { val parsed = json.decodeFromString>>(body) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt index d0c361e5..0b5c39db 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt @@ -31,8 +31,8 @@ data class Meta( val message: String, @SerialName("totalitems") - val totalItems: String, + val totalItems: String = "", @SerialName("itemsperpage") - val itemsPerPage: String + val itemsPerPage: String = "" ) From 27769a7377d2dc04d4067e41c6f617cab0502016 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 14:56:55 +0200 Subject: [PATCH 010/174] use client Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareView.kt | 9 ++++++--- .../common/ui/share/repository/ShareRemoteRepository.kt | 5 ++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 6a2c51ea..69eae020 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -88,7 +88,10 @@ import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission import com.nextcloud.android.common.ui.share.model.ui.customPermissionFields +import com.nextcloud.android.common.ui.network.api.ApiCredentials +import com.nextcloud.android.common.ui.network.api.ApiHttpClient import com.nextcloud.android.common.ui.share.repository.MockShareRepository +import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @@ -723,9 +726,9 @@ private fun PreviewTheme( } } -fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme) { - // TODO: REPLACE - val viewModel = ShareViewModel(repository = MockShareRepository()) +fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme, credentials: ApiCredentials) { + val apiHttpClient = ApiHttpClient.create(credentials) + val viewModel = ShareViewModel(repository = ShareRemoteRepository(apiHttpClient)) setContent { MaterialTheme( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 6551f27f..e90ec820 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share.repository +import com.nextcloud.android.common.ui.network.api.ApiHttpClient import com.nextcloud.android.common.ui.network.model.ApiResult import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse @@ -14,9 +15,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipient import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare -class ShareRemoteRepository: ShareRepository { - - // TODO: ALL OCS-APIRequest //boolean header +class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository { /** * Searches for recipients From ccc45ae300aa8e2cae44d136b086e0a3d7476bee Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 16:16:59 +0200 Subject: [PATCH 011/174] wip Signed-off-by: alperozturk96 --- .../android/common/sample/MainViewModel.kt | 2 +- .../common/ui/network/api/ApiHttpClient.kt | 31 ++- .../common/ui/network/api/ApiMethod.kt | 15 ++ .../share/repository/ShareRemoteRepository.kt | 185 ++++++++++++++---- 4 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 220effdc..6725e64c 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -42,4 +42,4 @@ class MainViewModel : ViewModel() { } } } -} \ No newline at end of file +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt index 499207a8..b484d09f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt @@ -10,6 +10,7 @@ package com.nextcloud.android.common.ui.network.api import okhttp3.Credentials import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.Request import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit @@ -19,9 +20,20 @@ class ApiHttpClient private constructor( val credentials: ApiCredentials ) { companion object { - private const val CONNECT_TIMEOUT_SECONDS = 30L - private const val READ_TIMEOUT_SECONDS = 30L - private const val WRITE_TIMEOUT_SECONDS = 30L + private const val CONNECT_TIMEOUT_SECONDS = 90L + private const val READ_TIMEOUT_SECONDS = 90L + private const val WRITE_TIMEOUT_SECONDS = 90L + + private const val HTTP_PREFIX = "http://" + private const val HTTPS_PREFIX = "https://" + + private const val HEADER_ACCEPT_NAME = "Accept" + private const val HEADER_ACCEPT_VALUE = "application/json" + + private const val HEADER_AUTHORIZATION_NAME = "Authorization" + + private const val HEADER_OCS_NAME = "OCS-APIRequest" + private const val HEADER_OCS_VALUE = "true" fun create( credentials: ApiCredentials, @@ -49,14 +61,21 @@ class ApiHttpClient private constructor( } } + fun buildRequest(url: String, method: ApiMethod, body: okhttp3.RequestBody? = null): Request = + Request.Builder() + .url(url) + .header(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE) + .method(method.type, body) + .build() + private class AuthInterceptor(private val credentials: ApiCredentials) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val basicCredentials = Credentials.basic(credentials.username, credentials.token) val request = chain.request() .newBuilder() - .header("Authorization", basicCredentials) - .header("OCS-APIRequest", "true") + .header(HEADER_AUTHORIZATION_NAME, basicCredentials) + .header(HEADER_OCS_NAME, HEADER_OCS_VALUE) .url(buildUrl(chain.request().url.toString(), credentials.baseURL)) .build() @@ -64,7 +83,7 @@ class ApiHttpClient private constructor( } private fun buildUrl(requestUrl: String, baseUrl: String): String { - return if (requestUrl.startsWith("http://") || requestUrl.startsWith("https://")) { + return if (requestUrl.startsWith(HTTP_PREFIX) || requestUrl.startsWith(HTTPS_PREFIX)) { requestUrl } else { "${baseUrl.trimEnd('/')}/${requestUrl.trimStart('/')}" diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt new file mode 100644 index 00000000..45e56478 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.api + +enum class ApiMethod(val type: String) { + GET("GET"), + POST("POST"), + PUT("PUT"), + DELETE("DELETE") +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index e90ec820..2d6eac2a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -8,15 +8,48 @@ package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.api.ApiHttpClient +import com.nextcloud.android.common.ui.network.api.ApiMethod import com.nextcloud.android.common.ui.network.model.ApiResult +import com.nextcloud.android.common.ui.network.model.Meta +import com.nextcloud.android.common.ui.network.model.Ocs +import com.nextcloud.android.common.ui.network.model.OcsResponse import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse +import com.nextcloud.android.common.ui.share.model.api.create.toUnifiedShare import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository { + companion object { + private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share/" + private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares" + } + + private val json = Json { ignoreUnknownKeys = true } + private val jsonMediaType = "application/json; charset=utf-8".toMediaType() + private val baseUrl get() = client.credentials.baseURL.trimEnd('/') + + private fun errorResult(e: Exception): ApiResult.Error = + ApiResult.Error( + OcsResponse( + Ocs( + meta = Meta( + status = "error", + statusCode = -1, + message = e.message ?: "Unknown error" + ), + data = e.message ?: "Unknown error" + ) + ) + ) + /** * Searches for recipients */ @@ -25,47 +58,104 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository query: String, limit: Int, offset: Int - ): ApiResult> { - /* - GET - /ocs/v2.php/apps/sharing/api/v1/recipients + ): ApiResult> = withContext(Dispatchers.IO) { + val url = "$baseUrl/ocs/v2.php/apps/sharing/api/v1/recipients" + + "?recipientType=$recipientType&query=$query&limit=$limit&offset=$offset" - */ + val request = client.buildRequest(url, ApiMethod.GET) - TODO("Not yet implemented") + try { + val response = client.okHttpClient.newCall(request).execute() + val body = response.body.string() + if (response.isSuccessful) { + val parsed = json.decodeFromString>>(body) + ApiResult.Success(parsed.ocs.data) + } else { + ApiResult.Error(json.decodeFromString>(body)) + } + } catch (e: Exception) { + errorResult(e) + } } - override suspend fun createShare(request: CreateShareRequest): ApiResult { - /* - POST - /ocs/v2.php/apps/sharing/api/v1/share - */ - TODO("Not yet implemented") - } + override suspend fun createShare(request: CreateShareRequest): ApiResult = + withContext(Dispatchers.IO) { + val url = "$baseUrl$SHARE_ENDPOINT" + val body = json.encodeToString(request).toRequestBody(jsonMediaType) - override suspend fun fetchShare(id: String): ApiResult { - /* - POST - /ocs/v2.php/apps/sharing/api/v1/share/{id} - */ - TODO("Not yet implemented") - } + val httpRequest = client.buildRequest(url, ApiMethod.POST, body) - override suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult { - /* - PUT - /ocs/v2.php/apps/sharing/api/v1/share/{id} - */ - TODO("Not yet implemented") - } + try { + val response = client.okHttpClient.newCall(httpRequest).execute() + val responseBody = response.body.string() + if (response.isSuccessful) { + val parsed = json.decodeFromString>(responseBody) + ApiResult.Success(parsed.ocs.data) + } else { + ApiResult.Error(json.decodeFromString>(responseBody)) + } + } catch (e: Exception) { + errorResult(e) + } + } - override suspend fun deleteShare(id: String): ApiResult { - /* - DELETE - /ocs/v2.php/apps/sharing/api/v1/share/{id} - */ - TODO("Not yet implemented") - } + override suspend fun fetchShare(id: String): ApiResult = + withContext(Dispatchers.IO) { + val url = "$baseUrl$SHARE_ENDPOINT$id" + val request = client.buildRequest(url, ApiMethod.GET) + + try { + val response = client.okHttpClient.newCall(request).execute() + val body = response.body.string() + if (response.isSuccessful) { + val parsed = json.decodeFromString>(body) + ApiResult.Success(parsed.ocs.data) + } else { + ApiResult.Error(json.decodeFromString>(body)) + } + } catch (e: Exception) { + errorResult(e) + } + } + + override suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult = + withContext(Dispatchers.IO) { + val url = "$baseUrl$SHARE_ENDPOINT$id" + val body = json.encodeToString(request).toRequestBody(jsonMediaType) + + val httpRequest = client.buildRequest(url, ApiMethod.PUT, body) + + try { + val response = client.okHttpClient.newCall(httpRequest).execute() + val responseBody = response.body.string() + if (response.isSuccessful) { + val parsed = json.decodeFromString>(responseBody) + ApiResult.Success(parsed.ocs.data) + } else { + ApiResult.Error(json.decodeFromString>(responseBody)) + } + } catch (e: Exception) { + errorResult(e) + } + } + + override suspend fun deleteShare(id: String): ApiResult = + withContext(Dispatchers.IO) { + val url = "$baseUrl$SHARE_ENDPOINT$id" + val request = client.buildRequest(url, ApiMethod.DELETE) + + try { + val response = client.okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + ApiResult.Success(Unit) + } else { + val body = response.body.string() + ApiResult.Error(json.decodeFromString>(body)) + } + } catch (e: Exception) { + errorResult(e) + } + } /** * @param sourceType @@ -81,11 +171,26 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository sourceType: String?, lastShareId: String?, limit: Int - ): ApiResult> { - /* - GET - /ocs/v2.php/apps/sharing/api/v1/shares - */ - TODO("Not yet implemented") + ): ApiResult> = withContext(Dispatchers.IO) { + val queryParams = buildString { + append("?limit=$limit") + sourceType?.let { append("&sourceType=$it") } + lastShareId?.let { append("&lastShareId=$it") } + } + val url = "$baseUrl$SHARES_ENDPOINT$queryParams" + val request = client.buildRequest(url, ApiMethod.GET) + + try { + val response = client.okHttpClient.newCall(request).execute() + val body = response.body.string() + if (response.isSuccessful) { + val parsed = json.decodeFromString>>(body) + ApiResult.Success(parsed.ocs.data.map { it.toUnifiedShare() }) + } else { + ApiResult.Error(json.decodeFromString>(body)) + } + } catch (e: Exception) { + errorResult(e) + } } } From 200d7369462ae0c5a97906cae53dbe659606c741 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 16:20:32 +0200 Subject: [PATCH 012/174] wip Signed-off-by: alperozturk96 --- .../share/repository/ShareRemoteRepository.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 2d6eac2a..3a894ed4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -59,6 +59,8 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository limit: Int, offset: Int ): ApiResult> = withContext(Dispatchers.IO) { + // TODO: - CHECK URL GET + // /ocs/v2.php/apps/sharing/api/v1/recipients val url = "$baseUrl/ocs/v2.php/apps/sharing/api/v1/recipients" + "?recipientType=$recipientType&query=$query&limit=$limit&offset=$offset" @@ -80,6 +82,10 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository override suspend fun createShare(request: CreateShareRequest): ApiResult = withContext(Dispatchers.IO) { + /* + POST + /ocs/v2.php/apps/sharing/api/v1/share + */ val url = "$baseUrl$SHARE_ENDPOINT" val body = json.encodeToString(request).toRequestBody(jsonMediaType) @@ -101,6 +107,10 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository override suspend fun fetchShare(id: String): ApiResult = withContext(Dispatchers.IO) { + /* + POST + /ocs/v2.php/apps/sharing/api/v1/share/{id} + */ val url = "$baseUrl$SHARE_ENDPOINT$id" val request = client.buildRequest(url, ApiMethod.GET) @@ -120,6 +130,10 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository override suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult = withContext(Dispatchers.IO) { + /* + PUT + /ocs/v2.php/apps/sharing/api/v1/share/{id} + */ val url = "$baseUrl$SHARE_ENDPOINT$id" val body = json.encodeToString(request).toRequestBody(jsonMediaType) @@ -141,6 +155,10 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository override suspend fun deleteShare(id: String): ApiResult = withContext(Dispatchers.IO) { + /* + DELETE + /ocs/v2.php/apps/sharing/api/v1/share/{id} + */ val url = "$baseUrl$SHARE_ENDPOINT$id" val request = client.buildRequest(url, ApiMethod.DELETE) @@ -172,6 +190,10 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository lastShareId: String?, limit: Int ): ApiResult> = withContext(Dispatchers.IO) { + /* + GET + /ocs/v2.php/apps/sharing/api/v1/shares + */ val queryParams = buildString { append("?limit=$limit") sourceType?.let { append("&sourceType=$it") } From 72d64042b1ca6e823d6b871a2720bab8e69d7d63 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 16:21:37 +0200 Subject: [PATCH 013/174] wip Signed-off-by: alperozturk96 --- .../nextcloud/android/common/ui/network/api/ApiHttpClient.kt | 1 + .../common/ui/share/repository/ShareRemoteRepository.kt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt index b484d09f..81278099 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt @@ -86,6 +86,7 @@ class ApiHttpClient private constructor( return if (requestUrl.startsWith(HTTP_PREFIX) || requestUrl.startsWith(HTTPS_PREFIX)) { requestUrl } else { + // TODO - remove hardcoded value? "${baseUrl.trimEnd('/')}/${requestUrl.trimStart('/')}" } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 3a894ed4..0a192145 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -33,9 +33,14 @@ class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository } private val json = Json { ignoreUnknownKeys = true } + + // TODO - move from here? private val jsonMediaType = "application/json; charset=utf-8".toMediaType() + + // TODO - trim end? private val baseUrl get() = client.credentials.baseURL.trimEnd('/') + // TODO - Move from here private fun errorResult(e: Exception): ApiResult.Error = ApiResult.Error( OcsResponse( From faf65a08b921893fda604c9b31f40f83b66e942c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 6 May 2026 08:38:51 +0200 Subject: [PATCH 014/174] wip Signed-off-by: alperozturk96 --- .../android/common/sample/MainViewModel.kt | 18 +- .../common/ui/network/UserStatusRepository.kt | 30 +++ .../common/ui/network/UserStatusService.kt | 66 ----- .../common/ui/network/api/ApiHttpClient.kt | 94 -------- .../common/ui/network/auth/AuthInterceptor.kt | 49 ++++ .../ServerCredentials.kt} | 6 +- .../{api/ApiMethod.kt => http/HttpMethod.kt} | 6 +- .../ui/network/http/NextcloudHttpClient.kt | 76 ++++++ .../common/ui/network/http/RequestBuilder.kt | 27 +++ .../common/ui/network/model/ApiResult.kt | 13 - .../common/ui/network/model/NetworkResult.kt | 22 ++ .../ui/network/serialization/OCSSerializer.kt | 14 ++ .../android/common/ui/share/ShareView.kt | 10 +- .../android/common/ui/share/ShareViewModel.kt | 8 +- .../share/repository/MockShareRepository.kt | 26 +- .../share/repository/ShareRemoteRepository.kt | 226 ++++-------------- .../ui/share/repository/ShareRepository.kt | 14 +- 17 files changed, 314 insertions(+), 391 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt rename ui/src/main/java/com/nextcloud/android/common/ui/network/{api/ApiCredentials.kt => auth/ServerCredentials.kt} (72%) rename ui/src/main/java/com/nextcloud/android/common/ui/network/{api/ApiMethod.kt => http/HttpMethod.kt} (70%) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 6725e64c..0738ab44 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -10,10 +10,10 @@ package com.nextcloud.android.common.sample import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.android.common.ui.network.api.ApiCredentials -import com.nextcloud.android.common.ui.network.model.ApiResult -import com.nextcloud.android.common.ui.network.api.ApiHttpClient -import com.nextcloud.android.common.ui.network.UserStatusService +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.UserStatusRepository import kotlinx.coroutines.launch class MainViewModel : ViewModel() { @@ -26,17 +26,17 @@ class MainViewModel : ViewModel() { token: String ) { viewModelScope.launch { - val credentials = ApiCredentials(baseUrl, username, token) - val client = ApiHttpClient.create(credentials, enableLogging = true) - val service = UserStatusService(client) + val credentials = ServerCredentials(baseUrl, username, token) + val client = NextcloudHttpClient.create(credentials, enableLogging = true) + val service = UserStatusRepository(client) when (val result = service.fetchPredefinedStatuses()) { - is ApiResult.Success -> + is NetworkResult.Success -> apiTestResult.value = "✅ Success (${result.data.size} statuses):\n" + result.data.joinToString("\n") { "${it.icon} ${it.message}" } - is ApiResult.Error -> + is NetworkResult.Error -> apiTestResult.value = "❌ Error ${result.error.ocs.meta.statusCode}: ${result.error.ocs.meta.message}" } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt new file mode 100644 index 00000000..7c36bdf9 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network + +import com.nextcloud.android.common.ui.network.http.HttpMethod +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.network.model.OcsResponse +import com.nextcloud.android.common.ui.network.serialization.OCSSerializer + +class UserStatusRepository(private val client: NextcloudHttpClient) { + + private companion object { + private const val PREDEFINED_STATUSES_ENDPOINT = + "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses" + } + + suspend fun fetchPredefinedStatuses(): NetworkResult> = + client.executeRequest( + endpoint = PREDEFINED_STATUSES_ENDPOINT, + method = HttpMethod.GET + ) { body -> + OCSSerializer.json.decodeFromString>>(body).ocs.data + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt deleted file mode 100644 index 73f9a3a0..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusService.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.network - -import com.nextcloud.android.common.ui.network.api.ApiHttpClient -import com.nextcloud.android.common.ui.network.model.ApiResult -import com.nextcloud.android.common.ui.network.model.Meta -import com.nextcloud.android.common.ui.network.model.Ocs -import com.nextcloud.android.common.ui.network.model.OcsResponse -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import okhttp3.Request - -class UserStatusService(private val client: ApiHttpClient) { - - private val json = Json { ignoreUnknownKeys = true } - - suspend fun fetchPredefinedStatuses(): ApiResult> = - withContext(Dispatchers.IO) { - val url = - client.credentials.baseURL.trimEnd('/') + - "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses" - - val request = - Request - .Builder() - .url(url) - .header("Accept", "application/json") - .build() - - try { - val response = client.okHttpClient.newCall(request).execute() - val body = response.body.string() - - if (response.isSuccessful) { - val parsed = json.decodeFromString>>(body) - ApiResult.Success(parsed.ocs.data) - } else { - val error = json.decodeFromString>(body) - ApiResult.Error(error) - } - } catch (e: Exception) { - ApiResult.Error( - OcsResponse( - Ocs( - meta = - Meta( - status = "error", - statusCode = -1, - message = e.message ?: "Unknown error", - totalItems = "", - itemsPerPage = "" - ), - data = e.message ?: "Unknown error" - ) - ) - ) - } - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt deleted file mode 100644 index 81278099..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiHttpClient.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.network.api - -import okhttp3.Credentials -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import okhttp3.logging.HttpLoggingInterceptor -import java.util.concurrent.TimeUnit - -class ApiHttpClient private constructor( - val okHttpClient: OkHttpClient, - val credentials: ApiCredentials -) { - companion object { - private const val CONNECT_TIMEOUT_SECONDS = 90L - private const val READ_TIMEOUT_SECONDS = 90L - private const val WRITE_TIMEOUT_SECONDS = 90L - - private const val HTTP_PREFIX = "http://" - private const val HTTPS_PREFIX = "https://" - - private const val HEADER_ACCEPT_NAME = "Accept" - private const val HEADER_ACCEPT_VALUE = "application/json" - - private const val HEADER_AUTHORIZATION_NAME = "Authorization" - - private const val HEADER_OCS_NAME = "OCS-APIRequest" - private const val HEADER_OCS_VALUE = "true" - - fun create( - credentials: ApiCredentials, - enableLogging: Boolean = false - ): ApiHttpClient { - val authInterceptor = AuthInterceptor(credentials) - - val loggingInterceptor = HttpLoggingInterceptor().apply { - level = if (enableLogging) { - HttpLoggingInterceptor.Level.BODY - } else { - HttpLoggingInterceptor.Level.NONE - } - } - - val okHttpClient = OkHttpClient.Builder() - .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .addInterceptor(authInterceptor) - .addInterceptor(loggingInterceptor) - .build() - - return ApiHttpClient(okHttpClient, credentials) - } - } - - fun buildRequest(url: String, method: ApiMethod, body: okhttp3.RequestBody? = null): Request = - Request.Builder() - .url(url) - .header(HEADER_ACCEPT_NAME, HEADER_ACCEPT_VALUE) - .method(method.type, body) - .build() - - private class AuthInterceptor(private val credentials: ApiCredentials) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val basicCredentials = Credentials.basic(credentials.username, credentials.token) - - val request = chain.request() - .newBuilder() - .header(HEADER_AUTHORIZATION_NAME, basicCredentials) - .header(HEADER_OCS_NAME, HEADER_OCS_VALUE) - .url(buildUrl(chain.request().url.toString(), credentials.baseURL)) - .build() - - return chain.proceed(request) - } - - private fun buildUrl(requestUrl: String, baseUrl: String): String { - return if (requestUrl.startsWith(HTTP_PREFIX) || requestUrl.startsWith(HTTPS_PREFIX)) { - requestUrl - } else { - // TODO - remove hardcoded value? - "${baseUrl.trimEnd('/')}/${requestUrl.trimStart('/')}" - } - } - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt new file mode 100644 index 00000000..07c7b485 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.auth + +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor(private val credentials: ServerCredentials) : Interceptor { + + private companion object { + private const val HTTP_PREFIX = "http://" + private const val HTTPS_PREFIX = "https://" + private const val DELIMITER = '/' + + private const val HEADER_AUTHORIZATION = "Authorization" + private const val HEADER_OCS_REQUEST = "OCS-APIRequest" + private const val HEADER_OCS_REQUEST_VALUE = "true" + } + + override fun intercept(chain: Interceptor.Chain): Response { + val basicCredentials = Credentials.basic(credentials.username, credentials.token) + + val request = chain.request() + .newBuilder() + .header(HEADER_AUTHORIZATION, basicCredentials) + .header(HEADER_OCS_REQUEST, HEADER_OCS_REQUEST_VALUE) + .url(resolveUrl(chain.request().url.toString())) + .build() + + return chain.proceed(request) + } + + /** + * Prepends [ServerCredentials.baseURL] to relative URLs. + * Absolute URLs (starting with http/https) are passed through unchanged. + */ + private fun resolveUrl(requestUrl: String): String = + if (requestUrl.startsWith(HTTP_PREFIX) || requestUrl.startsWith(HTTPS_PREFIX)) { + requestUrl + } else { + "${credentials.baseURL.trimEnd(DELIMITER)}/${requestUrl.trimStart(DELIMITER)}" + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt similarity index 72% rename from ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt index f6770090..b299353c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiCredentials.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt @@ -5,10 +5,10 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.network.api +package com.nextcloud.android.common.ui.network.auth -data class ApiCredentials( +data class ServerCredentials( val baseURL: String, val username: String, val token: String -) +) \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt similarity index 70% rename from ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt index 45e56478..f5901b8d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/api/ApiMethod.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt @@ -5,11 +5,11 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.network.api +package com.nextcloud.android.common.ui.network.http -enum class ApiMethod(val type: String) { +enum class HttpMethod(val type: String) { GET("GET"), POST("POST"), PUT("PUT"), DELETE("DELETE") -} +} \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt new file mode 100644 index 00000000..8996bfb2 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.http + +import com.nextcloud.android.common.ui.network.auth.AuthInterceptor +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.network.model.OcsResponse +import com.nextcloud.android.common.ui.network.serialization.OCSSerializer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.logging.HttpLoggingInterceptor +import java.util.concurrent.TimeUnit + +class NextcloudHttpClient private constructor( + val okHttpClient: OkHttpClient, + val credentials: ServerCredentials +) { + companion object { + private const val CONNECT_TIMEOUT_SECONDS = 90L + private const val READ_TIMEOUT_SECONDS = 90L + private const val WRITE_TIMEOUT_SECONDS = 90L + + fun create( + credentials: ServerCredentials, + enableLogging: Boolean = false + ): NextcloudHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = if (enableLogging) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + + val okHttpClient = OkHttpClient.Builder() + .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .addInterceptor(AuthInterceptor(credentials)) + .addInterceptor(loggingInterceptor) + .build() + + return NextcloudHttpClient(okHttpClient, credentials) + } + } + + private val baseUrl get() = credentials.baseURL.trimEnd('/') + + suspend fun executeRequest( + endpoint: String, + method: HttpMethod, + body: RequestBody? = null, + parse: (String) -> T + ): NetworkResult = withContext(Dispatchers.IO) { + val request = buildOcsRequest("$baseUrl$endpoint", method, body) + try { + val response = okHttpClient.newCall(request).execute() + val responseBody = response.body.string() + if (response.isSuccessful) { + NetworkResult.Success(parse(responseBody)) + } else { + NetworkResult.ServerError(OCSSerializer.json.decodeFromString>(responseBody)) + } + } catch (e: Exception) { + NetworkResult.fromException(e) + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt new file mode 100644 index 00000000..7e7560cd --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.http + +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request +import okhttp3.RequestBody + +val JSON_CONTENT_TYPE = "application/json; charset=utf-8".toMediaType() + +private const val HEADER_ACCEPT = "Accept" +private const val HEADER_ACCEPT_VALUE = "application/json" + +fun buildOcsRequest( + url: String, + method: HttpMethod, + body: RequestBody? = null +): Request = Request.Builder() + .url(url) + .header(HEADER_ACCEPT, HEADER_ACCEPT_VALUE) + .method(method.type, body) + .build() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt deleted file mode 100644 index 9ab9328b..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/ApiResult.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.network.model - -sealed class ApiResult { - data class Success(val data: T) : ApiResult() - data class Error(val error: OcsResponse) : ApiResult() -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt new file mode 100644 index 00000000..932761b6 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.model + +sealed class NetworkResult { + data class Success(val data: T) : NetworkResult() + + data class ServerError(val response: OcsResponse) : NetworkResult() + + data class NetworkException(val throwable: Throwable) : NetworkResult() + + val isSuccess: Boolean get() = this is Success + + companion object { + fun fromException(e: Throwable): NetworkException = NetworkException(e) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt new file mode 100644 index 00000000..aae4ab75 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.serialization + +import kotlinx.serialization.json.Json + +object OCSSerializer { + val json = Json { ignoreUnknownKeys = true } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 69eae020..a7dcadeb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -88,8 +88,8 @@ import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission import com.nextcloud.android.common.ui.share.model.ui.customPermissionFields -import com.nextcloud.android.common.ui.network.api.ApiCredentials -import com.nextcloud.android.common.ui.network.api.ApiHttpClient +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @@ -726,9 +726,9 @@ private fun PreviewTheme( } } -fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme, credentials: ApiCredentials) { - val apiHttpClient = ApiHttpClient.create(credentials) - val viewModel = ShareViewModel(repository = ShareRemoteRepository(apiHttpClient)) +fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme, credentials: ServerCredentials) { + val nextcloudHttpClient = NextcloudHttpClient.create(credentials) + val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) setContent { MaterialTheme( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 5621326e..0896d0dd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -10,7 +10,7 @@ package com.nextcloud.android.common.ui.share import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.network.model.ApiResult +import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers @@ -43,11 +43,11 @@ class ShareViewModel( _errorMessageId.value = null when (val result = repository.fetchShares()) { - is ApiResult.Success -> { + is NetworkResult.Success -> { _shares.update { result.data } } - is ApiResult.Error -> { + is NetworkResult.Error -> { _errorMessageId.value = R.string.share_view_fetch_error_message } } @@ -79,7 +79,7 @@ class ShareViewModel( } val result = repository.deleteShare(share.id) - if (result is ApiResult.Error) { + if (result is NetworkResult.Error) { _errorMessageId.update { R.string.share_view_delete_error_message } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 053d951c..b2f416b6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -7,7 +7,7 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.network.model.ApiResult +import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.owner.Owner @@ -22,7 +22,7 @@ class MockShareRepository : ShareRepository { query: String, limit: Int, offset: Int - ): ApiResult> { + ): NetworkResult> { val mock = listOf( ShareRecipients( @@ -53,12 +53,12 @@ class MockShareRepository : ShareRepository { ) ) - return ApiResult.Success(mock) + return NetworkResult.Success(mock) } override suspend fun createShare( request: CreateShareRequest - ): ApiResult { + ): NetworkResult { val response = ShareDataResponse( sources = request.data.sources, @@ -72,10 +72,10 @@ class MockShareRepository : ShareRepository { ) ) - return ApiResult.Success(response) + return NetworkResult.Success(response) } - override suspend fun fetchShare(id: String): ApiResult { + override suspend fun fetchShare(id: String): NetworkResult { val mock = ShareDataResponse( sources = emptyList(), @@ -95,13 +95,13 @@ class MockShareRepository : ShareRepository { ) ) - return ApiResult.Success(mock) + return NetworkResult.Success(mock) } override suspend fun updateShare( id: String, request: UpdateShareRequest - ): ApiResult { + ): NetworkResult { val updated = ShareDataResponse( sources = request.data.sources, @@ -112,18 +112,18 @@ class MockShareRepository : ShareRepository { owner = request.data.owner ) - return ApiResult.Success(updated) + return NetworkResult.Success(updated) } - override suspend fun deleteShare(id: String): ApiResult { - return ApiResult.Success(Unit) + override suspend fun deleteShare(id: String): NetworkResult { + return NetworkResult.Success(Unit) } override suspend fun fetchShares( sourceType: String?, lastShareId: String?, limit: Int - ): ApiResult> { + ): NetworkResult> { val data = listOf( UnifiedShare( id = "1", @@ -276,6 +276,6 @@ class MockShareRepository : ShareRepository { ) ) - return ApiResult.Success(data) + return NetworkResult.Success(data) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 0a192145..3c7ca0e2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -7,217 +7,95 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.network.api.ApiHttpClient -import com.nextcloud.android.common.ui.network.api.ApiMethod -import com.nextcloud.android.common.ui.network.model.ApiResult -import com.nextcloud.android.common.ui.network.model.Meta -import com.nextcloud.android.common.ui.network.model.Ocs +import com.nextcloud.android.common.ui.network.http.HttpMethod +import com.nextcloud.android.common.ui.network.http.JSON_CONTENT_TYPE +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.OcsResponse +import com.nextcloud.android.common.ui.network.serialization.OCSSerializer import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.create.toUnifiedShare import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody -class ShareRemoteRepository(private val client: ApiHttpClient) : ShareRepository { +class ShareRemoteRepository( + private val client: NextcloudHttpClient, + private val json: kotlinx.serialization.json.Json = OCSSerializer.json +) : ShareRepository { - companion object { + private companion object { + // Trailing slash intentional, share ID is appended directly: "$SHARE_ENDPOINT$id" private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share/" + private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares" + private const val RECIPIENTS_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/recipients" } - private val json = Json { ignoreUnknownKeys = true } - - // TODO - move from here? - private val jsonMediaType = "application/json; charset=utf-8".toMediaType() - - // TODO - trim end? - private val baseUrl get() = client.credentials.baseURL.trimEnd('/') - - // TODO - Move from here - private fun errorResult(e: Exception): ApiResult.Error = - ApiResult.Error( - OcsResponse( - Ocs( - meta = Meta( - status = "error", - statusCode = -1, - message = e.message ?: "Unknown error" - ), - data = e.message ?: "Unknown error" - ) - ) - ) - - /** - * Searches for recipients - */ override suspend fun fetchRecipients( recipientType: String, query: String, limit: Int, offset: Int - ): ApiResult> = withContext(Dispatchers.IO) { - // TODO: - CHECK URL GET - // /ocs/v2.php/apps/sharing/api/v1/recipients - val url = "$baseUrl/ocs/v2.php/apps/sharing/api/v1/recipients" + - "?recipientType=$recipientType&query=$query&limit=$limit&offset=$offset" - - val request = client.buildRequest(url, ApiMethod.GET) - - try { - val response = client.okHttpClient.newCall(request).execute() - val body = response.body.string() - if (response.isSuccessful) { - val parsed = json.decodeFromString>>(body) - ApiResult.Success(parsed.ocs.data) - } else { - ApiResult.Error(json.decodeFromString>(body)) - } - } catch (e: Exception) { - errorResult(e) + ): NetworkResult> = + client.executeRequest( + endpoint = "$RECIPIENTS_ENDPOINT?recipientType=$recipientType&query=$query&limit=$limit&offset=$offset", + method = HttpMethod.GET + ) { body -> + json.decodeFromString>>(body).ocs.data } - } - override suspend fun createShare(request: CreateShareRequest): ApiResult = - withContext(Dispatchers.IO) { - /* - POST - /ocs/v2.php/apps/sharing/api/v1/share - */ - val url = "$baseUrl$SHARE_ENDPOINT" - val body = json.encodeToString(request).toRequestBody(jsonMediaType) - - val httpRequest = client.buildRequest(url, ApiMethod.POST, body) - - try { - val response = client.okHttpClient.newCall(httpRequest).execute() - val responseBody = response.body.string() - if (response.isSuccessful) { - val parsed = json.decodeFromString>(responseBody) - ApiResult.Success(parsed.ocs.data) - } else { - ApiResult.Error(json.decodeFromString>(responseBody)) - } - } catch (e: Exception) { - errorResult(e) - } + override suspend fun createShare(request: CreateShareRequest): NetworkResult = + client.executeRequest( + endpoint = SHARE_ENDPOINT, + method = HttpMethod.POST, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data } - override suspend fun fetchShare(id: String): ApiResult = - withContext(Dispatchers.IO) { - /* - POST - /ocs/v2.php/apps/sharing/api/v1/share/{id} - */ - val url = "$baseUrl$SHARE_ENDPOINT$id" - val request = client.buildRequest(url, ApiMethod.GET) - - try { - val response = client.okHttpClient.newCall(request).execute() - val body = response.body.string() - if (response.isSuccessful) { - val parsed = json.decodeFromString>(body) - ApiResult.Success(parsed.ocs.data) - } else { - ApiResult.Error(json.decodeFromString>(body)) - } - } catch (e: Exception) { - errorResult(e) - } + override suspend fun fetchShare(id: String): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT$id", + method = HttpMethod.GET + ) { body -> + json.decodeFromString>(body).ocs.data } - override suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult = - withContext(Dispatchers.IO) { - /* - PUT - /ocs/v2.php/apps/sharing/api/v1/share/{id} - */ - val url = "$baseUrl$SHARE_ENDPOINT$id" - val body = json.encodeToString(request).toRequestBody(jsonMediaType) - - val httpRequest = client.buildRequest(url, ApiMethod.PUT, body) - - try { - val response = client.okHttpClient.newCall(httpRequest).execute() - val responseBody = response.body.string() - if (response.isSuccessful) { - val parsed = json.decodeFromString>(responseBody) - ApiResult.Success(parsed.ocs.data) - } else { - ApiResult.Error(json.decodeFromString>(responseBody)) - } - } catch (e: Exception) { - errorResult(e) - } + override suspend fun updateShare(id: String, request: UpdateShareRequest): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT$id", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data } - override suspend fun deleteShare(id: String): ApiResult = - withContext(Dispatchers.IO) { - /* - DELETE - /ocs/v2.php/apps/sharing/api/v1/share/{id} - */ - val url = "$baseUrl$SHARE_ENDPOINT$id" - val request = client.buildRequest(url, ApiMethod.DELETE) - - try { - val response = client.okHttpClient.newCall(request).execute() - if (response.isSuccessful) { - ApiResult.Success(Unit) - } else { - val body = response.body.string() - ApiResult.Error(json.decodeFromString>(body)) - } - } catch (e: Exception) { - errorResult(e) - } - } + override suspend fun deleteShare(id: String): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT$id", + method = HttpMethod.DELETE + ) { } - /** - * @param sourceType - * Optional filter to return only shares matching a specific source type. - * When null, shares of all source types are returned. - * - * @param lastShareId - * Pagination cursor representing the last known share ID. - * Only shares with an ID greater than this value will be returned. - * When null, results start from the first available share. - */ override suspend fun fetchShares( sourceType: String?, lastShareId: String?, limit: Int - ): ApiResult> = withContext(Dispatchers.IO) { - /* - GET - /ocs/v2.php/apps/sharing/api/v1/shares - */ + ): NetworkResult> { val queryParams = buildString { append("?limit=$limit") sourceType?.let { append("&sourceType=$it") } lastShareId?.let { append("&lastShareId=$it") } } - val url = "$baseUrl$SHARES_ENDPOINT$queryParams" - val request = client.buildRequest(url, ApiMethod.GET) - - try { - val response = client.okHttpClient.newCall(request).execute() - val body = response.body.string() - if (response.isSuccessful) { - val parsed = json.decodeFromString>>(body) - ApiResult.Success(parsed.ocs.data.map { it.toUnifiedShare() }) - } else { - ApiResult.Error(json.decodeFromString>(body)) - } - } catch (e: Exception) { - errorResult(e) + return client.executeRequest( + endpoint = "$SHARES_ENDPOINT$queryParams", + method = HttpMethod.GET + ) { body -> + json.decodeFromString>>(body) + .ocs.data + .map { it.toUnifiedShare() } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 3267d9db..db90524e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -7,7 +7,7 @@ package com.nextcloud.android.common.ui.share.repository -import com.nextcloud.android.common.ui.network.model.ApiResult +import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients @@ -20,19 +20,19 @@ interface ShareRepository { query: String, limit: Int = 10, offset: Int = 0 - ): ApiResult> + ): NetworkResult> - suspend fun createShare(request: CreateShareRequest): ApiResult + suspend fun createShare(request: CreateShareRequest): NetworkResult - suspend fun fetchShare(id: String): ApiResult + suspend fun fetchShare(id: String): NetworkResult - suspend fun updateShare(id: String, request: UpdateShareRequest): ApiResult + suspend fun updateShare(id: String, request: UpdateShareRequest): NetworkResult - suspend fun deleteShare(id: String): ApiResult + suspend fun deleteShare(id: String): NetworkResult suspend fun fetchShares( sourceType: String? = null, lastShareId: String? = null, limit: Int = 100 - ): ApiResult> + ): NetworkResult> } From 53ca0255c725fcc9f0458d52060acecd08a264a6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 6 May 2026 08:56:45 +0200 Subject: [PATCH 015/174] wip Signed-off-by: alperozturk96 --- .../android/common/sample/MainViewModel.kt | 8 +++-- .../android/common/ui/share/ShareViewModel.kt | 31 +++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 0738ab44..4744a557 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -36,9 +36,13 @@ class MainViewModel : ViewModel() { "✅ Success (${result.data.size} statuses):\n" + result.data.joinToString("\n") { "${it.icon} ${it.message}" } - is NetworkResult.Error -> + is NetworkResult.ServerError -> apiTestResult.value = - "❌ Error ${result.error.ocs.meta.statusCode}: ${result.error.ocs.meta.message}" + "❌ Error ${result.response.ocs.meta.statusCode}: ${result.response.ocs.meta.message}" + + is NetworkResult.NetworkException -> + apiTestResult.value = + "❌ Exception: ${result.throwable.message}" } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 0896d0dd..a7d65469 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -42,12 +42,14 @@ class ShareViewModel( _loading.value = true _errorMessageId.value = null - when (val result = repository.fetchShares()) { - is NetworkResult.Success -> { - _shares.update { result.data } - } - - is NetworkResult.Error -> { + when (val result = repository.fetchShares( + sourceType = null, + lastShareId = null, + limit = 50 + )) { + is NetworkResult.Success -> _shares.update { result.data } + is NetworkResult.ServerError, + is NetworkResult.NetworkException -> { _errorMessageId.value = R.string.share_view_fetch_error_message } } @@ -70,20 +72,17 @@ class ShareViewModel( fun delete(share: UnifiedShare) { viewModelScope.launch(Dispatchers.IO) { - val id = share.id - if (id == null) { - _errorMessageId.update { - R.string.share_view_delete_error_id_not_found_message - } + val id = share.id ?: run { + _errorMessageId.update { R.string.share_view_delete_error_id_not_found_message } return@launch } - val result = repository.deleteShare(share.id) - if (result is NetworkResult.Error) { - _errorMessageId.update { - R.string.share_view_delete_error_message + when (repository.deleteShare(id)) { + is NetworkResult.Success -> _shares.update { current -> current.filterNot { it.id == id } } + is NetworkResult.ServerError, + is NetworkResult.NetworkException -> { + _errorMessageId.update { R.string.share_view_delete_error_message } } - return@launch } } } From 98b651daf36e2713c46420c9827254b1dce27ed7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 27 May 2026 12:18:47 +0300 Subject: [PATCH 016/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 3 +- .../android/common/ui/share/ShareViewModel.kt | 224 +++++-- .../model/api/create/CreateShareRequest.kt | 23 - .../model/api/create/ShareDataResponse.kt | 50 -- .../common/ui/share/model/api/icon/Icon.kt | 17 + .../common/ui/share/model/api/owner/Owner.kt | 8 +- .../share/model/api/permission/Permission.kt | 24 + .../ui/share/model/api/property/Property.kt | 123 ++++ .../{ShareRecipients.kt => Recipient.kt} | 20 +- .../share/model/api/request/ShareRequests.kt | 58 ++ .../common/ui/share/model/api/share/Share.kt | 37 ++ .../{user/ShareUser.kt => source/Source.kt} | 8 +- .../ui/share/model/api/state/ShareState.kt | 23 + .../model/api/update/UpdateShareRequest.kt | 32 - .../common/ui/share/model/ui/UnifiedShares.kt | 7 +- .../share/repository/MockShareRepository.kt | 601 +++++++++++------- .../share/repository/ShareRemoteRepository.kt | 163 +++-- .../ui/share/repository/ShareRepository.kt | 78 ++- ui/src/main/res/values/strings.xml | 10 +- 19 files changed, 1061 insertions(+), 448 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/{ShareRecipients.kt => Recipient.kt} (61%) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/{user/ShareUser.kt => source/Source.kt} (72%) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index a7dcadeb..1b07956c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -94,7 +94,6 @@ import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch - @Composable private fun ShareView(viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() @@ -138,7 +137,7 @@ private fun ShareView(viewModel: ShareViewModel) { UnifiedSharesListItem(share, type, onSelectShare = { share -> bottomSheetState = ShareBottomSheetState.Edit(share) }, onDeleteShare = { - viewModel.delete(share) + viewModel.deleteShare(share.id) }, onSendEmail = { // TODO: }) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index a7d65469..ef712815 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -11,7 +11,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare +import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest +import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest +import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -23,8 +30,11 @@ class ShareViewModel( private val repository: ShareRepository ) : ViewModel() { - private val _shares = MutableStateFlow>(emptyList()) - val shares: StateFlow> = _shares + private val _shares = MutableStateFlow>(emptyList()) + val shares: StateFlow> = _shares + + private val _activeShare = MutableStateFlow(null) + val activeShare: StateFlow = _activeShare private val _loading = MutableStateFlow(false) val loading: StateFlow = _loading @@ -36,61 +46,201 @@ class ShareViewModel( loadShares() } - // region private methods - private fun loadShares() { + // region shares list + fun loadShares( + sourceClass: String? = null, + lastShareID: String? = null, + limit: Int = 50 + ) { + launchWithLoading { + handleResult( + result = repository.fetchShares( + sourceClass = sourceClass, + lastShareID = lastShareID, + limit = limit + ), + errorId = R.string.share_view_fetch_error_message + ) { _shares.update { it } } + } + } + + fun fetchShare(id: String, request: GetShareRequest = GetShareRequest()) { + launchWithLoading { + handleResult( + result = repository.fetchShare(id, request), + errorId = R.string.share_view_fetch_error_message + ) { share -> + _activeShare.update { share } + replaceInList(share) + } + } + } + // endregion + + // region create + + /** + * Creates an empty draft [Share] on the server and sets it as the [activeShare]. + * Then sources can be added later. + * + */ + fun createShare(onCreated: (Share) -> Unit = {}) { + launchWithLoading { + handleResult( + result = repository.createShare(), + errorId = R.string.share_view_create_error_message + ) { draft -> + _activeShare.update { draft } + _shares.update { current -> listOf(draft) + current } + onCreated(draft) + } + } + } + // endregion + + // region state + fun updateState(id: String, shareState: ShareState) { viewModelScope.launch(Dispatchers.IO) { - _loading.value = true - _errorMessageId.value = null - - when (val result = repository.fetchShares( - sourceType = null, - lastShareId = null, - limit = 50 - )) { - is NetworkResult.Success -> _shares.update { result.data } - is NetworkResult.ServerError, - is NetworkResult.NetworkException -> { - _errorMessageId.value = R.string.share_view_fetch_error_message - } + handleResult( + result = repository.updateShareState(id, UpdateShareStateRequest(shareState)), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) } + } + } + // endregion - _loading.value = false + // region sources + fun addSource(id: String, clazz: String, value: String) { + viewModelScope.launch(Dispatchers.IO) { + handleResult( + result = repository.addShareSource(id, AddSourceRequest(clazz, value)), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) + } + } + } + + fun removeSource(id: String, clazz: String, value: String) { + viewModelScope.launch(Dispatchers.IO) { + handleResult( + result = repository.removeShareSource(id, clazz, value), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) + } } } // endregion - // region public methods - fun create(share: UnifiedShare) { + // region recipients + fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) { + viewModelScope.launch(Dispatchers.IO) { + handleResult( + result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) + } + } + } + + fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { - /* - val request = CreateShareRequest() - repository.createShare() - */ + handleResult( + result = repository.removeShareRecipient(id, clazz, value, instance), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) + } + } + } + // endregion + // region properties + fun updateProperty(id: String, clazz: String, value: String?) { + viewModelScope.launch(Dispatchers.IO) { + handleResult( + result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value)), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) + } } } + // endregion - fun delete(share: UnifiedShare) { + // region permissions + fun updatePermission(id: String, clazz: String, enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val id = share.id ?: run { - _errorMessageId.update { R.string.share_view_delete_error_id_not_found_message } - return@launch + handleResult( + result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)), + errorId = R.string.share_view_update_error_message + ) { updated -> + _activeShare.update { updated } + replaceInList(updated) } + } + } + // endregion - when (repository.deleteShare(id)) { - is NetworkResult.Success -> _shares.update { current -> current.filterNot { it.id == id } } - is NetworkResult.ServerError, - is NetworkResult.NetworkException -> { - _errorMessageId.update { R.string.share_view_delete_error_message } - } + // region delete + fun deleteShare(id: String) { + viewModelScope.launch(Dispatchers.IO) { + handleResult( + result = repository.deleteShare(id), + errorId = R.string.share_view_delete_error_message + ) { + _shares.update { current -> current.filterNot { it.id == id } } + if (_activeShare.value?.id == id) _activeShare.update { null } } } } + // endregion + // region ui helpers fun updateErrorMessage(value: Int?) { - _errorMessageId.update { - value + _errorMessageId.update { value } + } + + fun clearActiveShare() { + _activeShare.update { null } + } + // endregion + + // region private + private fun replaceInList(updated: Share) { + _shares.update { current -> current.map { if (it.id == updated.id) updated else it } } + } + + private fun handleResult( + result: NetworkResult, + errorId: Int, + onSuccess: (T) -> Unit + ) { + when (result) { + is NetworkResult.Success -> onSuccess(result.data) + is NetworkResult.ServerError, + is NetworkResult.NetworkException -> _errorMessageId.update { errorId } + } + } + + private fun launchWithLoading(block: suspend () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + _loading.update { true } + _errorMessageId.update { null } + block() + _loading.update { false } } } + // endregion } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt deleted file mode 100644 index 54fa9da7..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/CreateShareRequest.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.create - -import com.nextcloud.android.common.ui.share.model.api.user.ShareUser -import kotlinx.serialization.Serializable - -@Serializable -data class CreateShareRequest( - val data: ShareDataRequest -) - -@Serializable -data class ShareDataRequest( - val sources: List, - val recipients: List, - val properties: Map>> -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt deleted file mode 100644 index 1d2ec2f2..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/create/ShareDataResponse.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.create - -import com.nextcloud.android.common.ui.share.model.api.owner.Owner -import com.nextcloud.android.common.ui.share.model.api.user.ShareUser -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareType -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ShareDataResponse( - val sources: List, - val recipients: List, - val properties: Map>>, - - val id: String, - - @SerialName("last_updated") - val lastUpdated: Long, - - val owner: Owner -) - -fun ShareDataResponse.toUnifiedShare(): UnifiedShare { - val primarySource = sources.firstOrNull() - return UnifiedShare( - id = id, - sources = sources, - recipients = recipients, - properties = properties, - lastUpdated = lastUpdated, - owner = owner, - type = UnifiedShareType.toUnifiedShareType(primarySource?.type), - category = UnifiedShareCategory.Invited, // TODO map from properties - permission = UnifiedSharePermission.CanView, // TODO map from properties - label = primarySource?.displayName ?: "Unknown", - note = "", // TODO map from properties - password = "", // TODO map from properties - limit = null // TODO map from properties - ) -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt new file mode 100644 index 00000000..4343042a --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.icon + +import kotlinx.serialization.Serializable + +@Serializable +data class Icon( + val svg: String? = null, + val light: String? = null, + val dark: String? = null +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt index cb75c5d7..d5646004 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt @@ -9,13 +9,17 @@ package com.nextcloud.android.common.ui.share.model.api.owner import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable - +import com.nextcloud.android.common.ui.share.model.api.icon.Icon @Serializable data class Owner( @SerialName("user_id") val userId: String, + val instance: String? = null, + @SerialName("display_name") - val displayName: String + val displayName: String, + + val icon: Icon ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt new file mode 100644 index 00000000..eae0441a --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.permission + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Permission( + @SerialName("class") + val clazz: String, + + @SerialName("display_name") + val displayName: String, + + val category: String?, + + val enabled: Boolean +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt new file mode 100644 index 00000000..1520f771 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -0,0 +1,123 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.property + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator + +@OptIn(ExperimentalSerializationApi::class) +@JsonClassDiscriminator("type") +@Serializable +sealed class Property + +@Serializable +@SerialName("boolean") +data class PropertyBoolean( + @SerialName("class") + val clazz: String, + + @SerialName("display_name") + val displayName: String, + + val hint: String? = null, + + val priority: Int, + + val required: Boolean, + + val value: String? = null +) : Property() + +@Serializable +@SerialName("date") +data class PropertyDate( + @SerialName("class") + val clazz: String, + + @SerialName("display_name") + val displayName: String, + + val hint: String? = null, + + val priority: Int, + + val required: Boolean, + + val value: String? = null, + + @SerialName("min_date") + val minDate: String? = null, + + @SerialName("max_date") + val maxDate: String? = null +) : Property() + +@Serializable +@SerialName("enum") +data class PropertyEnum( + @SerialName("class") + val clazz: String, + + @SerialName("display_name") + val displayName: String, + + val hint: String? = null, + + val priority: Int, + + val required: Boolean, + + val value: String? = null, + + @SerialName("valid_values") + val validValues: List +) : Property() + +@Serializable +@SerialName("password") +data class PropertyPassword( + @SerialName("class") + val clazz: String, + + @SerialName("display_name") + val displayName: String, + + val hint: String? = null, + + val priority: Int, + + val required: Boolean, + + val value: String? = null +) : Property() + +@Serializable +@SerialName("string") +data class PropertyString( + @SerialName("class") + val clazz: String, + + @SerialName("display_name") + val displayName: String, + + val hint: String? = null, + + val priority: Int, + + val required: Boolean, + + val value: String? = null, + + @SerialName("min_length") + val minLength: Int? = null, + + @SerialName("max_length") + val maxLength: Int? = null +) : Property() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt similarity index 61% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index a9bba350..36d876b1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/ShareRecipients.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -7,23 +7,21 @@ package com.nextcloud.android.common.ui.share.model.api.recipients +import com.nextcloud.android.common.ui.share.model.api.icon.Icon import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ShareRecipients( - val type: String, +data class Recipient( + @SerialName("class") + val clazz: String, + val value: String, + val instance: String? = null, + @SerialName("display_name") val displayName: String, - @SerialName("display_name_unique") - val displayNameUnique: String, - - @SerialName("icon_url_light") - val iconUrlLight: String, - - @SerialName("icon_url_dark") - val iconUrlDark: String -) \ No newline at end of file + val icon: Icon? = null +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt new file mode 100644 index 00000000..b0502ec6 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.request + +import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class GetShareRequest( + val secret: String? = null, + val arguments: Map = emptyMap() +) + +@Serializable +data class UpdateShareStateRequest( + val shareState: ShareState +) + +@Serializable +data class AddSourceRequest( + @SerialName("class") + val clazz: String, + + val value: String +) + +@Serializable +data class AddRecipientRequest( + @SerialName("class") + val clazz: String, + + val value: String, + + val instance: String? = null +) + +@Serializable +data class UpdateSharePropertyRequest( + @SerialName("class") + val clazz: String, + + val value: String? = null +) + +@Serializable +data class UpdateSharePermissionRequest( + @SerialName("class") + val clazz: String, + + val enabled: Boolean +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt new file mode 100644 index 00000000..8b67d24d --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.share + +import com.nextcloud.android.common.ui.share.model.api.owner.Owner +import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.property.Property +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.source.Source +import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Share( + val id: String, + + val owner: Owner, + + @SerialName("last_updated") + val lastUpdated: Long, + + val shareState: ShareState, + + val sources: List, + + val recipients: List, + + val properties: List, + + val permissions: List +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt similarity index 72% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt index c0709a89..b5dfa16f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/ShareUser.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt @@ -5,14 +5,16 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share.model.api.user +package com.nextcloud.android.common.ui.share.model.api.source import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ShareUser( - val type: String, +data class Source( + @SerialName("class") + val clazz: String, + val value: String, @SerialName("display_name") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt new file mode 100644 index 00000000..2846dc06 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.state + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class ShareState { + @SerialName("active") + ACTIVE, + + @SerialName("draft") + DRAFT, + + @SerialName("deleted") + DELETED +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt deleted file mode 100644 index 926177af..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/update/UpdateShareRequest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.update - -import com.nextcloud.android.common.ui.share.model.api.owner.Owner -import com.nextcloud.android.common.ui.share.model.api.user.ShareUser -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class UpdateShareRequest( - val data: UpdateShareData -) - -@Serializable -data class UpdateShareData( - val sources: List, - val recipients: List, - val properties: Map>>, - - val id: String, - - @SerialName("last_updated") - val lastUpdated: Long, - - val owner: Owner -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt index 5690cb5f..0e24f7e5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt @@ -8,12 +8,13 @@ package com.nextcloud.android.common.ui.share.model.ui import com.nextcloud.android.common.ui.share.model.api.owner.Owner -import com.nextcloud.android.common.ui.share.model.api.user.ShareUser +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.source.Source data class UnifiedShare( val id: String?, - val sources: List, - val recipients: List, + val sources: List, + val recipients: List, val properties: Map>>, val lastUpdated: Long, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index b2f416b6..8f7fcf94 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -8,274 +8,425 @@ package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.model.NetworkResult -import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest -import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse +import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.owner.Owner -import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients -import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest -import com.nextcloud.android.common.ui.share.model.api.user.ShareUser -import com.nextcloud.android.common.ui.share.model.ui.* +import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean +import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword +import com.nextcloud.android.common.ui.share.model.api.property.PropertyString +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest +import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest +import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.source.Source +import com.nextcloud.android.common.ui.share.model.api.state.ShareState class MockShareRepository : ShareRepository { + + private val mockOwner = Owner( + userId = "alice", + displayName = "Alice Johnson", + icon = Icon( + light = "https://mock/icons/user_light.png", + dark = "https://mock/icons/user_dark.png" + ) + ) + + private val mockPermissions = listOf( + Permission( + clazz = "read", + displayName = "Read", + category = "access", + enabled = true + ), + Permission( + clazz = "update", + displayName = "Update", + category = "access", + enabled = false + ), + Permission( + clazz = "delete", + displayName = "Delete", + category = "access", + enabled = false + ) + ) + + private val mockProperties = listOf( + PropertyString( + clazz = "note", + displayName = "Note", + priority = 10, + required = false, + value = "Design review – please check latest changes" + ), + PropertyPassword( + clazz = "password", + displayName = "Password", + priority = 20, + required = false, + value = null + ), + PropertyDate( + clazz = "expiration_date", + displayName = "Expiration date", + priority = 30, + required = false, + value = null, + minDate = "2026-01-01", + maxDate = "2027-01-01" + ), + PropertyBoolean( + clazz = "hide_download", + displayName = "Hide download", + priority = 40, + required = false, + value = "false" + ) + ) + + private fun buildShare( + id: String, + sources: List, + recipients: List, + shareState: ShareState = ShareState.ACTIVE, + lastUpdated: Long = System.currentTimeMillis(), + owner: Owner = mockOwner + ) = Share( + id = id, + owner = owner, + lastUpdated = lastUpdated, + shareState = shareState, + sources = sources, + recipients = recipients, + properties = mockProperties, + permissions = mockPermissions + ) + + private val mockShares = mutableListOf( + buildShare( + id = "1", + sources = listOf( + Source(clazz = "file", value = "/Photos/vacation.jpg", displayName = "vacation.jpg") + ), + recipients = listOf( + Recipient( + clazz = "user", + value = "alice@company.com", + displayName = "Alice Johnson", + icon = Icon( + light = "https://mock/icons/user_light.png", + dark = "https://mock/icons/user_dark.png" + ) + ) + ) + ), + buildShare( + id = "2", + sources = listOf( + Source(clazz = "file", value = "/Documents/report.pdf", displayName = "report.pdf") + ), + recipients = listOf( + Recipient( + clazz = "group", + value = "marketing", + displayName = "Marketing Team", + icon = Icon( + light = "https://mock/icons/group_light.png", + dark = "https://mock/icons/group_dark.png" + ) + ) + ), + owner = Owner( + userId = "system", + displayName = "System", + icon = Icon( + light = "https://mock/icons/system_light.png", + dark = "https://mock/icons/system_dark.png" + ) + ) + ), + buildShare( + id = "3", + sources = listOf( + Source(clazz = "link", value = "https://nextcloud.com/s/abc123", displayName = "Public Link") + ), + recipients = emptyList(), + lastUpdated = 1710000000L, + owner = Owner( + userId = "system", + displayName = "System", + icon = Icon( + light = "https://mock/icons/system_light.png", + dark = "https://mock/icons/system_dark.png" + ) + ) + ), + buildShare( + id = "4", + sources = listOf( + Source(clazz = "file", value = "/Projects/brief.docx", displayName = "brief.docx") + ), + recipients = listOf( + Recipient( + clazz = "mail", + value = "john@external.com", + displayName = "John External", + icon = Icon( + light = "https://mock/icons/external_light.png", + dark = "https://mock/icons/external_dark.png" + ) + ) + ), + owner = Owner( + userId = "john", + displayName = "John External", + icon = Icon( + light = "https://mock/icons/user_light.png", + dark = "https://mock/icons/user_dark.png" + ) + ) + ), + buildShare( + id = "5", + sources = listOf( + Source(clazz = "file", value = "/Shared/assets.zip", displayName = "assets.zip") + ), + recipients = listOf( + Recipient( + clazz = "federated", + value = "partner@nextcloud.org", + instance = "nextcloud.org", + displayName = "Partner Cloud", + icon = Icon( + light = "https://mock/icons/federated_light.png", + dark = "https://mock/icons/federated_dark.png" + ) + ) + ), + owner = Owner( + userId = "partner", + displayName = "Partner Cloud", + icon = Icon( + light = "https://mock/icons/user_light.png", + dark = "https://mock/icons/user_dark.png" + ) + ) + ) + ) + override suspend fun fetchRecipients( - recipientType: String, + recipientTypeClass: String?, query: String, limit: Int, offset: Int - ): NetworkResult> { - - val mock = listOf( - ShareRecipients( - type = recipientType, + ): NetworkResult> { + val all = listOf( + Recipient( + clazz = "user", value = "alice@company.com", displayName = "Alice Johnson", - displayNameUnique = "Alice Johnson (Company)", - iconUrlLight = "https://mock/icons/user_light.png", - iconUrlDark = "https://mock/icons/user_dark.png" + icon = Icon( + light = "https://mock/icons/user_light.png", + dark = "https://mock/icons/user_dark.png" + ) ), - - ShareRecipients( - type = recipientType, + Recipient( + clazz = "group", value = "marketing", displayName = "Marketing Team", - displayNameUnique = "Marketing Team (Group)", - iconUrlLight = "https://mock/icons/group_light.png", - iconUrlDark = "https://mock/icons/group_dark.png" + icon = Icon( + light = "https://mock/icons/group_light.png", + dark = "https://mock/icons/group_dark.png" + ) ), - - ShareRecipients( - type = recipientType, + Recipient( + clazz = "mail", value = "john@external.com", displayName = "John External", - displayNameUnique = "John External (External)", - iconUrlLight = "https://mock/icons/external_light.png", - iconUrlDark = "https://mock/icons/external_dark.png" + icon = Icon( + light = "https://mock/icons/external_light.png", + dark = "https://mock/icons/external_dark.png" + ) + ), + Recipient( + clazz = "federated", + value = "partner@nextcloud.org", + instance = "nextcloud.org", + displayName = "Partner Cloud", + icon = Icon( + light = "https://mock/icons/federated_light.png", + dark = "https://mock/icons/federated_dark.png" + ) ) ) - return NetworkResult.Success(mock) - } - - override suspend fun createShare( - request: CreateShareRequest - ): NetworkResult { - - val response = ShareDataResponse( - sources = request.data.sources, - recipients = request.data.recipients, - properties = request.data.properties, - id = "mock-share-${System.currentTimeMillis()}", - lastUpdated = System.currentTimeMillis(), - owner = Owner( - userId = "mock-user", - displayName = "Mock User" - ) - ) + val filtered = all + .filter { recipientTypeClass == null || it.clazz == recipientTypeClass } + .filter { it.displayName.contains(query, ignoreCase = true) || it.value.contains(query, ignoreCase = true) } + .drop(offset) + .take(limit) - return NetworkResult.Success(response) + return NetworkResult.Success(filtered) } - override suspend fun fetchShare(id: String): NetworkResult { - - val mock = ShareDataResponse( + override suspend fun createShare(): NetworkResult { + val share = buildShare( + id = "mock-share-${System.currentTimeMillis()}", sources = emptyList(), - recipients = listOf( - ShareUser( - type = "user", - value = "alice@company.com", - displayName = "Alice Johnson" - ) - ), - properties = emptyMap(), - id = id, - lastUpdated = 0, - owner = Owner( - userId = "alice", - displayName = "Alice Johnson" - ) + recipients = emptyList(), + shareState = ShareState.DRAFT ) - - return NetworkResult.Success(mock) + mockShares.add(share) + return NetworkResult.Success(share) } - override suspend fun updateShare( + override suspend fun fetchShare( id: String, - request: UpdateShareRequest - ): NetworkResult { - - val updated = ShareDataResponse( - sources = request.data.sources, - recipients = request.data.recipients, - properties = request.data.properties, - id = id, - lastUpdated = System.currentTimeMillis(), - owner = request.data.owner - ) - - return NetworkResult.Success(updated) + request: GetShareRequest + ): NetworkResult { + val share = mockShares.find { it.id == id } + ?: return NetworkResult.Success( + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + ) + return NetworkResult.Success(share) } override suspend fun deleteShare(id: String): NetworkResult { + mockShares.removeAll { it.id == id } return NetworkResult.Success(Unit) } override suspend fun fetchShares( - sourceType: String?, - lastShareId: String?, + sourceClass: String?, + lastShareID: String?, limit: Int - ): NetworkResult> { - val data = listOf( - UnifiedShare( - id = "1", - sources = emptyList(), - recipients = listOf( - ShareUser( - type = "user", - value = "alice@company.com", - displayName = "Alice Johnson" - ) - ), - properties = emptyMap(), - lastUpdated = 0, - owner = Owner( - userId = "alice", - displayName = "Alice Johnson" - ), - - permission = UnifiedSharePermission.CanView, - label = "Alice Johnson", - note = "Design review – please check latest changes", - password = "", - type = UnifiedShareType.InternalUser, - category = UnifiedShareCategory.Invited, - limit = UnifiedShareDownloadLimit( - limit = 100, - downloadCount = 12 - ) - ), + ): NetworkResult> { + var result = mockShares.toList() - UnifiedShare( - id = "2", - sources = emptyList(), - recipients = listOf( - ShareUser( - type = "group", - value = "marketing", - displayName = "Marketing Team" - ) - ), - properties = emptyMap(), - lastUpdated = 0, - owner = Owner( - userId = "system", - displayName = "System" - ), + if (sourceClass != null) { + result = result.filter { share -> share.sources.any { it.clazz == sourceClass } } + } - permission = UnifiedSharePermission.CanEdit, - label = "Marketing Team", - note = "", - password = "", - type = UnifiedShareType.InternalGroup, - category = UnifiedShareCategory.Invited, - limit = UnifiedShareDownloadLimit( - limit = 0, - downloadCount = 0 - ) - ), + if (lastShareID != null) { + val index = result.indexOfFirst { it.id == lastShareID } + if (index >= 0) result = result.drop(index + 1) + } - UnifiedShare( - id = "3", - sources = listOf( - ShareUser( - type = "link", - value = "https://nextcloud.com/s/abc123", - displayName = "Public Link" - ) - ), - recipients = emptyList(), - properties = emptyMap(), - lastUpdated = 1710000000, - owner = Owner( - userId = "system", - displayName = "System" - ), + return NetworkResult.Success(result.take(limit)) + } - permission = UnifiedSharePermission.Custom( - read = true, - edit = false, - delete = false, - create = false - ), - label = "Public Link", - note = "Public link for client review", - password = "1234", - type = UnifiedShareType.InternalLink, - category = UnifiedShareCategory.Anyone, - limit = UnifiedShareDownloadLimit( - limit = 50, - downloadCount = 5 - ) - ), + override suspend fun updateShareState( + id: String, + request: UpdateShareStateRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val updated = (if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())) + .copy(shareState = request.shareState, lastUpdated = System.currentTimeMillis()) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } - UnifiedShare( - id = "4", - sources = emptyList(), - recipients = listOf( - ShareUser( - type = "mail", - value = "john@external.com", - displayName = "John External" - ) - ), - properties = emptyMap(), - lastUpdated = 0, - owner = Owner( - userId = "john", - displayName = "John External" - ), + override suspend fun addShareSource( + id: String, + request: AddSourceRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val newSource = Source(clazz = request.clazz, value = request.value, displayName = request.value) + val updated = current.copy( + sources = current.sources + newSource, + lastUpdated = System.currentTimeMillis() + ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } - permission = UnifiedSharePermission.CanView, - label = "John External", - note = "External partner access", - password = "", - type = UnifiedShareType.ExternalMail, - category = UnifiedShareCategory.Anyone, - limit = UnifiedShareDownloadLimit( - limit = 20, - downloadCount = 2 - ) - ), + override suspend fun removeShareSource( + id: String, + clazz: String, + value: String + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val updated = current.copy( + sources = current.sources.filterNot { it.clazz == clazz && it.value == value }, + lastUpdated = System.currentTimeMillis() + ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } - UnifiedShare( - id = "5", - sources = emptyList(), - recipients = listOf( - ShareUser( - type = "federated", - value = "partner@nextcloud.org", - displayName = "Partner Cloud" - ) - ), - properties = emptyMap(), - lastUpdated = 0, - owner = Owner( - userId = "partner", - displayName = "Partner Cloud" - ), + override suspend fun addShareRecipient( + id: String, + request: AddRecipientRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val newRecipient = Recipient( + clazz = request.clazz, + value = request.value, + instance = request.instance, + displayName = request.value + ) + val updated = current.copy( + recipients = current.recipients + newRecipient, + lastUpdated = System.currentTimeMillis() + ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } - permission = UnifiedSharePermission.FileDrop, - label = "Partner Cloud", - note = "Federated sharing with partner instance", - password = "", - type = UnifiedShareType.ExternalFederated, - category = UnifiedShareCategory.Anyone, - limit = UnifiedShareDownloadLimit( - limit = 0, - downloadCount = 0 - ) - ) + override suspend fun removeShareRecipient( + id: String, + clazz: String, + value: String, + instance: String? + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val updated = current.copy( + recipients = current.recipients.filterNot { + it.clazz == clazz && it.value == value && it.instance == instance + }, + lastUpdated = System.currentTimeMillis() ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } - return NetworkResult.Success(data) + override suspend fun updateShareProperty( + id: String, + request: UpdateSharePropertyRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val updated = current.copy(lastUpdated = System.currentTimeMillis()) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } + + override suspend fun updateSharePermission( + id: String, + request: UpdateSharePermissionRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = + if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val updatedPermissions = current.permissions.map { + if (it.clazz == request.clazz) it.copy(enabled = request.enabled) else it + } + val updated = current.copy( + permissions = updatedPermissions, + lastUpdated = System.currentTimeMillis() + ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 3c7ca0e2..6e0a391e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -13,12 +13,14 @@ import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.OcsResponse import com.nextcloud.android.common.ui.network.serialization.OCSSerializer -import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest -import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse -import com.nextcloud.android.common.ui.share.model.api.create.toUnifiedShare -import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients -import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest +import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest +import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest +import com.nextcloud.android.common.ui.share.model.api.share.Share import okhttp3.RequestBody.Companion.toRequestBody class ShareRemoteRepository( @@ -27,75 +29,160 @@ class ShareRemoteRepository( ) : ShareRepository { private companion object { - // Trailing slash intentional, share ID is appended directly: "$SHARE_ENDPOINT$id" - private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share/" - + private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share" private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares" private const val RECIPIENTS_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/recipients" } override suspend fun fetchRecipients( - recipientType: String, + recipientTypeClass: String?, query: String, limit: Int, offset: Int - ): NetworkResult> = - client.executeRequest( - endpoint = "$RECIPIENTS_ENDPOINT?recipientType=$recipientType&query=$query&limit=$limit&offset=$offset", + ): NetworkResult> { + val queryParams = buildString { + append("?query=$query&limit=$limit&offset=$offset") + recipientTypeClass?.let { append("&recipientTypeClass=$it") } + } + return client.executeRequest( + endpoint = "$RECIPIENTS_ENDPOINT$queryParams", method = HttpMethod.GET ) { body -> - json.decodeFromString>>(body).ocs.data + json.decodeFromString>>(body).ocs.data } + } - override suspend fun createShare(request: CreateShareRequest): NetworkResult = + override suspend fun createShare(): NetworkResult = client.executeRequest( endpoint = SHARE_ENDPOINT, + method = HttpMethod.POST + ) { body -> + json.decodeFromString>(body).ocs.data + } + + override suspend fun fetchShare( + id: String, + request: GetShareRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id", method = HttpMethod.POST, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) ) { body -> - json.decodeFromString>(body).ocs.data + json.decodeFromString>(body).ocs.data } - override suspend fun fetchShare(id: String): NetworkResult = + override suspend fun deleteShare(id: String): NetworkResult = client.executeRequest( - endpoint = "$SHARE_ENDPOINT$id", + endpoint = "$SHARE_ENDPOINT/$id", + method = HttpMethod.DELETE + ) { } + + override suspend fun fetchShares( + sourceClass: String?, + lastShareID: String?, + limit: Int + ): NetworkResult> { + val queryParams = buildString { + append("?limit=$limit") + sourceClass?.let { append("&sourceClass=$it") } + lastShareID?.let { append("&lastShareID=$it") } + } + return client.executeRequest( + endpoint = "$SHARES_ENDPOINT$queryParams", method = HttpMethod.GET ) { body -> - json.decodeFromString>(body).ocs.data + json.decodeFromString>>(body).ocs.data } + } - override suspend fun updateShare(id: String, request: UpdateShareRequest): NetworkResult = + override suspend fun updateShareState( + id: String, + request: UpdateShareStateRequest + ): NetworkResult = client.executeRequest( - endpoint = "$SHARE_ENDPOINT$id", + endpoint = "$SHARE_ENDPOINT/$id/state", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) ) { body -> - json.decodeFromString>(body).ocs.data + json.decodeFromString>(body).ocs.data } - override suspend fun deleteShare(id: String): NetworkResult = + override suspend fun addShareSource( + id: String, + request: AddSourceRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/source", + method = HttpMethod.POST, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data + } + + override suspend fun removeShareSource( + id: String, + clazz: String, + value: String + ): NetworkResult = client.executeRequest( - endpoint = "$SHARE_ENDPOINT$id", + endpoint = "$SHARE_ENDPOINT/$id/source?class=$clazz&value=$value", method = HttpMethod.DELETE - ) { } + ) { body -> + json.decodeFromString>(body).ocs.data + } - override suspend fun fetchShares( - sourceType: String?, - lastShareId: String?, - limit: Int - ): NetworkResult> { + override suspend fun addShareRecipient( + id: String, + request: AddRecipientRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/recipient", + method = HttpMethod.POST, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data + } + + override suspend fun removeShareRecipient( + id: String, + clazz: String, + value: String, + instance: String? + ): NetworkResult { val queryParams = buildString { - append("?limit=$limit") - sourceType?.let { append("&sourceType=$it") } - lastShareId?.let { append("&lastShareId=$it") } + append("?class=$clazz&value=$value") + instance?.let { append("&instance=$it") } } return client.executeRequest( - endpoint = "$SHARES_ENDPOINT$queryParams", - method = HttpMethod.GET + endpoint = "$SHARE_ENDPOINT/$id/recipient$queryParams", + method = HttpMethod.DELETE ) { body -> - json.decodeFromString>>(body) - .ocs.data - .map { it.toUnifiedShare() } + json.decodeFromString>(body).ocs.data } } + + override suspend fun updateShareProperty( + id: String, + request: UpdateSharePropertyRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/property", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data + } + + override suspend fun updateSharePermission( + id: String, + request: UpdateSharePermissionRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/enabled", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index db90524e..302304bf 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -4,35 +4,77 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ - package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.model.NetworkResult -import com.nextcloud.android.common.ui.share.model.api.create.CreateShareRequest -import com.nextcloud.android.common.ui.share.model.api.create.ShareDataResponse -import com.nextcloud.android.common.ui.share.model.api.recipients.ShareRecipients -import com.nextcloud.android.common.ui.share.model.api.update.UpdateShareRequest -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest +import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest +import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest +import com.nextcloud.android.common.ui.share.model.api.share.Share interface ShareRepository { + suspend fun fetchRecipients( - recipientType: String, + recipientTypeClass: String?, query: String, - limit: Int = 10, - offset: Int = 0 - ): NetworkResult> - - suspend fun createShare(request: CreateShareRequest): NetworkResult + limit: Int, + offset: Int + ): NetworkResult> - suspend fun fetchShare(id: String): NetworkResult + suspend fun createShare(): NetworkResult - suspend fun updateShare(id: String, request: UpdateShareRequest): NetworkResult + suspend fun fetchShare( + id: String, + request: GetShareRequest = GetShareRequest() + ): NetworkResult suspend fun deleteShare(id: String): NetworkResult suspend fun fetchShares( - sourceType: String? = null, - lastShareId: String? = null, - limit: Int = 100 - ): NetworkResult> + sourceClass: String?, + lastShareID: String?, + limit: Int + ): NetworkResult> + + suspend fun updateShareState( + id: String, + request: UpdateShareStateRequest + ): NetworkResult + + suspend fun addShareSource( + id: String, + request: AddSourceRequest + ): NetworkResult + + suspend fun removeShareSource( + id: String, + clazz: String, + value: String + ): NetworkResult + + suspend fun addShareRecipient( + id: String, + request: AddRecipientRequest + ): NetworkResult + + suspend fun removeShareRecipient( + id: String, + clazz: String, + value: String, + instance: String? = null + ): NetworkResult + + suspend fun updateShareProperty( + id: String, + request: UpdateSharePropertyRequest + ): NetworkResult + + suspend fun updateSharePermission( + id: String, + request: UpdateSharePermissionRequest + ): NetworkResult } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index f341dc87..040c51d4 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -33,7 +33,6 @@ Expiration date Hide download and sync options - Label Optional name for this link Expiration date @@ -52,9 +51,12 @@ Send email Delete - Failed to fetch shares + Failed to fetch shares. + + Share not found, cannot delete. + Failed to delete share. - Share not found, cannot delete - Failed to delete share + Failed to create share. + Failed to update share. \ No newline at end of file From e1e938d50b28532c7e83037c0ebd0dfc22d9e606 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 27 May 2026 15:54:22 +0300 Subject: [PATCH 017/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 11 +-- .../share/model/ui/ShareBottomSheetState.kt | 6 +- .../model/ui/UnifiedShareDownloadLimit.kt | 13 --- .../share/model/ui/UnifiedSharePermission.kt | 79 ------------------- .../ui/share/model/ui/UnifiedShareType.kt | 44 ----------- .../common/ui/share/model/ui/UnifiedShares.kt | 50 ------------ 6 files changed, 10 insertions(+), 193 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 1b07956c..cbd0be4f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -90,6 +90,7 @@ import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission import com.nextcloud.android.common.ui.share.model.ui.customPermissionFields import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @@ -111,7 +112,7 @@ private fun ShareView(viewModel: ShareViewModel) { Scaffold(floatingActionButton = { FloatingActionButton( - onClick = { bottomSheetState = ShareBottomSheetState.New(UnifiedShare.new()) }, + onClick = { bottomSheetState = ShareBottomSheetState.New(Share()) }, ) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") } @@ -601,11 +602,11 @@ enum class UnifiedSharesListItemType { // NOTE: To just create a public link anyone tab + just send DOES SAME THING @Composable private fun UnifiedSharesListItem( - share: UnifiedShare, + share: Share, type: UnifiedSharesListItemType, - onSelectShare: (UnifiedShare) -> Unit, - onDeleteShare: (UnifiedShare) -> Unit, - onSendEmail: (UnifiedShare) -> Unit + onSelectShare: (Share) -> Unit, + onDeleteShare: (Share) -> Unit, + onSendEmail: (Share) -> Unit ) { var showContextMenu by remember { mutableStateOf(false) } val haptics = LocalHapticFeedback.current diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt index a5bfd600..a0491746 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt @@ -7,8 +7,10 @@ package com.nextcloud.android.common.ui.share.model.ui +import com.nextcloud.android.common.ui.share.model.api.share.Share + sealed class ShareBottomSheetState { data object Idle: ShareBottomSheetState() - data class New(val newShare: UnifiedShare): ShareBottomSheetState() - data class Edit(val share: UnifiedShare): ShareBottomSheetState() + data class New(val newShare: Share): ShareBottomSheetState() + data class Edit(val share: Share): ShareBottomSheetState() } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt deleted file mode 100644 index 46a600be..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareDownloadLimit.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.ui - -data class UnifiedShareDownloadLimit( - val limit: Int, - val downloadCount: Int -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt deleted file mode 100644 index b6511314..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedSharePermission.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.ui - -import com.nextcloud.android.common.ui.R - -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(var read: Boolean, var edit: Boolean, var delete: Boolean, var create: Boolean) : - UnifiedSharePermission() { - companion object { - fun getFromPermission(permission: UnifiedSharePermission?): Custom { - return Custom( - permission?.customPermissionRead() == true, - permission?.customPermissionEdit() == true, - permission?.customPermissionDelete() == true, - permission?.customPermissionCreate() == true - ) - } - } - } - - fun getTextId(): Int { - return when(this) { - FileDrop -> R.string.share_permission_file_drop - CanView -> R.string.share_permission_can_view - CanEdit -> R.string.share_permission_can_edit - is Custom -> R.string.share_permission_custom - } - } - - fun customFlag(selector: Custom.() -> Boolean): Boolean = - (this as? Custom)?.selector() ?: false -} - -fun UnifiedSharePermission?.customPermissionRead(): Boolean = this?.customFlag { read } ?: false -fun UnifiedSharePermission?.customPermissionEdit(): Boolean = this?.customFlag { edit } ?: false -fun UnifiedSharePermission?.customPermissionDelete(): Boolean = this?.customFlag { delete } ?: false -fun UnifiedSharePermission?.customPermissionCreate(): Boolean = this?.customFlag { create } ?: false - -data class CustomPermissionField( - val labelRes: Int, - val getValue: (UnifiedSharePermission.Custom) -> Boolean, - val setValue: (UnifiedSharePermission.Custom, Boolean) -> UnifiedSharePermission.Custom -) - -val customPermissionFields = listOf( - CustomPermissionField( - labelRes = R.string.share_view_view_files_switch, - getValue = { it.read }, - setValue = { p, v -> p.copy(read = v) } - ), - CustomPermissionField( - labelRes = R.string.share_view_edit_files_switch, - getValue = { it.edit }, - setValue = { p, v -> p.copy(edit = v) } - ), - CustomPermissionField( - labelRes = R.string.share_view_create_files_switch, - getValue = { it.create }, - setValue = { p, v -> p.copy(create = v) } - ), - CustomPermissionField( - labelRes = R.string.share_view_delete_files_switch, - getValue = { it.delete }, - setValue = { p, v -> p.copy(delete = v) } - ), -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt deleted file mode 100644 index ec3a8acc..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareType.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.ui - -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import com.nextcloud.android.common.ui.R - -enum class UnifiedShareType { - InternalUser, InternalGroup, InternalLink, ExternalLink, ExternalFederated, ExternalMail; - - @Composable - fun Icon() { - val iconId = when (this) { - InternalUser -> R.drawable.ic_user - InternalGroup -> R.drawable.ic_group - InternalLink -> R.drawable.ic_email - ExternalLink -> R.drawable.ic_link - ExternalFederated -> R.drawable.ic_group - ExternalMail -> R.drawable.ic_email - } - - Icon(painterResource(iconId), contentDescription = "share type icon") - } - - companion object { - fun toUnifiedShareType(value: String?): UnifiedShareType { - return when (value?.lowercase()) { - "user" -> InternalUser - "group" -> InternalGroup - "link" -> InternalLink - "federated" -> ExternalFederated - "mail" -> ExternalMail - else -> ExternalLink - } - } - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt deleted file mode 100644 index 0e24f7e5..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShares.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.ui - -import com.nextcloud.android.common.ui.share.model.api.owner.Owner -import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient -import com.nextcloud.android.common.ui.share.model.api.source.Source - -data class UnifiedShare( - val id: String?, - val sources: List, - val recipients: List, - val properties: Map>>, - - val lastUpdated: Long, - val owner: Owner?, - - val permission: UnifiedSharePermission?, - val type: UnifiedShareType?, - val category: UnifiedShareCategory, - val label: String, - val note: String = "", - val password: String = "", - val limit: UnifiedShareDownloadLimit? = null -) { - companion object { - fun new(): UnifiedShare { - return UnifiedShare( - id = null, - sources = listOf(), - recipients = listOf(), - properties = mapOf(), - lastUpdated = -1, - owner = null, - permission = null, - type = null, - category = UnifiedShareCategory.Invited, - label = "", - note = "", - password = "", - limit = null - ) - } - } -} From 7be1b5d64b297933e7f63bc3909aaf9f4d9f2014 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 27 May 2026 16:16:01 +0300 Subject: [PATCH 018/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 568 +++++------------- .../ui/share/model/api/property/Property.kt | 9 + .../share/model/ui/ShareBottomSheetState.kt | 5 +- 3 files changed, 150 insertions(+), 432 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index cbd0be4f..d01255bf 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -7,7 +7,6 @@ package com.nextcloud.android.common.ui.share -import android.content.ClipData import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -17,30 +16,22 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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 import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Button import androidx.compose.material3.ColorScheme 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 @@ -66,7 +57,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -74,26 +64,28 @@ 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.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.toClipEntry import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.model.ui.ShareBottomSheetState -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShare -import com.nextcloud.android.common.ui.share.model.ui.UnifiedShareCategory -import com.nextcloud.android.common.ui.share.model.ui.UnifiedSharePermission -import com.nextcloud.android.common.ui.share.model.ui.customPermissionFields import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.model.api.property.Property +import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean +import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum +import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword +import com.nextcloud.android.common.ui.share.model.api.property.PropertyString +import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.ShareBottomSheetState import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository -import kotlinx.coroutines.launch + @Composable private fun ShareView(viewModel: ShareViewModel) { @@ -110,105 +102,67 @@ private fun ShareView(viewModel: ShareViewModel) { } } - Scaffold(floatingActionButton = { - FloatingActionButton( - onClick = { bottomSheetState = ShareBottomSheetState.New(Share()) }, - ) { - Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") - } - }, snackbarHost = { - SnackbarHost(snackbarHostState) - }) { - LazyColumn(modifier = Modifier.padding(it)) { + Scaffold( + floatingActionButton = { + FloatingActionButton( + onClick = { + // Create an empty draft share immediately on the server, then edit it + viewModel.createShare { draft -> + bottomSheetState = ShareBottomSheetState.Edit(draft) + } + }, + ) { + Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") + } + }, + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + LazyColumn(modifier = Modifier.padding(paddingValues)) { itemsIndexed(shares) { index, share -> val type = when (index) { - 0 -> { - UnifiedSharesListItemType.Top - } - - shares.lastIndex -> { - UnifiedSharesListItemType.Bottom - } - - else -> { - UnifiedSharesListItemType.Mid - } + 0 -> UnifiedSharesListItemType.Top + shares.lastIndex -> UnifiedSharesListItemType.Bottom + else -> UnifiedSharesListItemType.Mid } - UnifiedSharesListItem(share, type, onSelectShare = { share -> - bottomSheetState = ShareBottomSheetState.Edit(share) - }, onDeleteShare = { - viewModel.deleteShare(share.id) - }, onSendEmail = { - // TODO: - }) + UnifiedSharesListItem( + share = share, + type = type, + onSelectShare = { selected -> bottomSheetState = ShareBottomSheetState.Edit(selected) }, + onDeleteShare = { viewModel.deleteShare(it.id) }, + onSendEmail = { /* TODO */ } + ) } } } - when (bottomSheetState) { - is ShareBottomSheetState.Edit -> { - val state = (bottomSheetState as ShareBottomSheetState.Edit) - AddOrEditShareBottomSheet( - title = stringResource(R.string.share_view_bottom_sheet_edit_title, state.share.label), - share = state.share, - onCreateOrEdit = { - - }, - onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } - ) - } - - is ShareBottomSheetState.New -> { - val state = (bottomSheetState as ShareBottomSheetState.New) - AddOrEditShareBottomSheet( - title = stringResource(R.string.share_view_bottom_sheet_new_title), - share = state.newShare, - onCreateOrEdit = { - - }, - onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } - ) - } - - ShareBottomSheetState.Idle -> Unit + if (bottomSheetState is ShareBottomSheetState.Edit) { + val state = bottomSheetState as ShareBottomSheetState.Edit + AddOrEditShareBottomSheet( + share = state.share, + viewModel = viewModel, + onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } + ) } } -// 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 AddOrEditShareBottomSheet( - title: String, - share: UnifiedShare, - onCreateOrEdit: () -> Unit, + share: Share, + viewModel: ShareViewModel, onDismiss: () -> Unit ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() - var category by remember { mutableStateOf(share.category) } - var permission by remember { mutableStateOf(share.permission ?: UnifiedSharePermission.CanView) } - var searchQuery by remember { mutableStateOf("") } - var note by remember { mutableStateOf(share.note) } - - // Toggle states for collapse/expand - var showInvitedSettings by remember { mutableStateOf(false) } - var showAnyoneSettings by remember { mutableStateOf(false) } - - val clipboard = LocalClipboard.current - val context = LocalContext.current - val scope = rememberCoroutineScope() - - val availablePermissions = remember { - listOf( - UnifiedSharePermission.CanView, - UnifiedSharePermission.CanEdit, - UnifiedSharePermission.FileDrop, - UnifiedSharePermission.Custom.getFromPermission(share.permission) - ) + // Group permissions dynamically by Category provided by the backend + val categories = remember(share.permissions) { + share.permissions.mapNotNull { it.category }.distinct() } + var selectedCategory by remember { mutableStateOf(categories.firstOrNull() ?: "") } + + var showAdvancedSettings by remember { mutableStateOf(false) } ModalBottomSheet( onDismissRequest = onDismiss, @@ -224,78 +178,102 @@ private fun AddOrEditShareBottomSheet( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - text = title, + text = stringResource(R.string.share_view_bottom_sheet_edit_title, share.id), style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 8.dp) ) - ShareCategoryButtonGroup( - selectedCategory = category, - onCategoryChange = { category = it } - ) - - if (category == UnifiedShareCategory.Invited) { - InvitedShareContent( - searchQuery = searchQuery, - onSearchChange = { searchQuery = it }, - permission = permission, - availablePermissions = availablePermissions, - onPermissionChange = { permission = it }, - ) - - CollapsibleSettingsSection( - isExpanded = showInvitedSettings, - onToggle = { showInvitedSettings = !showInvitedSettings } - ) { - InvitedInlineSettings(share) + // Dynamic Category Selector + if (categories.size > 1) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + categories.forEachIndexed { index, category -> + SegmentedButton( + selected = selectedCategory == category, + onClick = { selectedCategory = category }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size) + ) { + Text(category) + } + } } - } else { - AnyoneShareContent( - permission = permission, - availablePermissions = availablePermissions, - onPermissionChange = { permission = it }, - ) - - if (permission is UnifiedSharePermission.Custom) { - val customPermissions = permission as UnifiedSharePermission.Custom + } - customPermissionFields.forEach { field -> - SettingsSwitchRow( - label = stringResource(field.labelRes), - checked = field.getValue(customPermissions), - onCheckedChange = { permission = field.setValue(customPermissions, it) } - ) + // Render Permissions for Selected Category + val activePermissions = share.permissions.filter { it.category == selectedCategory } + activePermissions.forEach { permission -> + SettingsSwitchRow( + label = permission.displayName, + checked = permission.enabled, + onCheckedChange = { isChecked -> + viewModel.updatePermission(share.id, permission.clazz, isChecked) } - } + ) + } + // Render Dynamic Properties + if (share.properties.isNotEmpty()) { CollapsibleSettingsSection( - isExpanded = showAnyoneSettings, - onToggle = { showAnyoneSettings = !showAnyoneSettings } + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings } ) { - AnyoneInlineSettings(share) + // Sort by server-defined priority + share.properties.sortedBy { it.priority }.forEach { property -> + DynamicPropertyField(share.id, property, viewModel) + } } } + } + } +} - NoteToRecipients(note = note, onNoteChange = { note = it }) - - ShareActionButtons( - share = share, - isSendEnabled = searchQuery.isNotBlank(), - onCopyClick = { - val label = context.getString(R.string.share_view_copy_to_clipboard_label) - - scope.launch { - val clipData = - ClipData.newPlainText(label, it) - clipboard.setClipEntry(clipData.toClipEntry()) - } - }, - onSendClick = { - onCreateOrEdit() +@Composable +private fun DynamicPropertyField(shareId: String, property: Property, viewModel: ShareViewModel) { + when (property) { + is PropertyBoolean -> { + SettingsSwitchRow( + label = property.displayName, + checked = property.value == "true", + onCheckedChange = { isChecked -> + viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) } ) } + is PropertyString -> { + OutlinedTextField( + value = property.value ?: "", + onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + label = { Text(property.displayName) }, + placeholder = property.hint?.let { { Text(it) } }, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + singleLine = true + ) + } + is PropertyPassword -> { + OutlinedTextField( + value = property.value ?: "", + onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + label = { Text(property.displayName) }, + placeholder = property.hint?.let { { Text(it) } }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + singleLine = true + ) + } + is PropertyDate -> { + // TODO: Wrap with a DatePickerDialog. Falling back to string entry for now. + OutlinedTextField( + value = property.value ?: "", + onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + label = { Text(property.displayName + " (YYYY-MM-DD)") }, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + singleLine = true + ) + } + is PropertyEnum -> { + // TODO: Implement ExposedDropdownMenuBox using property.validValues + Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray) + } } } @@ -334,206 +312,6 @@ private fun CollapsibleSettingsSection( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ShareCategoryButtonGroup( - selectedCategory: UnifiedShareCategory, - onCategoryChange: (UnifiedShareCategory) -> Unit -) { - SingleChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - UnifiedShareCategory.entries.forEachIndexed { index, option -> - SegmentedButton( - selected = selectedCategory == option, - onClick = { onCategoryChange(option) }, - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = UnifiedShareCategory.entries.size - ) - ) { - Text(option.name) - } - } - } -} - -@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(stringResource(R.string.share_view_invited_category_label)) }, - placeholder = { Text(stringResource(R.string.share_view_invited_category_placeholder)) }, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - - PermissionDropdown( - label = stringResource(R.string.share_view_invited_category_participants), - selectedPermission = permission, - availablePermissions = availablePermissions, - onPermissionChange = onPermissionChange - ) - } -} - -@Composable -private fun NoteToRecipients( - note: String, - onNoteChange: (String) -> Unit -) { - OutlinedTextField( - value = note, - onValueChange = onNoteChange, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text(stringResource(R.string.share_view_note_text_field_placeholder)) }, - shape = RoundedCornerShape(8.dp) - ) -} - -@Composable -private fun AnyoneShareContent( - permission: UnifiedSharePermission, - availablePermissions: List, - onPermissionChange: (UnifiedSharePermission) -> Unit, -) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - PermissionDropdown( - label = stringResource(R.string.share_view_permission_dropdown_label), - 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 = stringResource(selectedPermission.getTextId()), - 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(stringResource(option.getTextId())) }, - onClick = { - onPermissionChange(option) - expanded = false - } - ) - } - } - } -} - -@Composable -private fun InvitedInlineSettings(share: UnifiedShare) { - var shareWithOthers by remember { mutableStateOf(share.recipients.isNotEmpty()) } - var editFile by remember { mutableStateOf((share.permission as? UnifiedSharePermission.CanEdit) != null) } - var hasExpiration by remember { mutableStateOf(false) } // TODO - var hideDownload by remember { mutableStateOf(false) } // TODO - - SettingsSwitchRow( - stringResource(R.string.share_view_invited_category_share_with_others_switch), - shareWithOthers - ) { shareWithOthers = it } - SettingsSwitchRow(stringResource(R.string.share_view_invited_category_edit_file_switch), editFile) { editFile = it } - SettingsSwitchRow( - stringResource(R.string.share_view_invited_category_expiration_date_switch), - hasExpiration - ) { hasExpiration = it } - SettingsSwitchRow( - stringResource(R.string.share_view_invited_category_hide_and_download_switch), - hideDownload - ) { hideDownload = it } -} - -@Composable -private fun AnyoneInlineSettings(share: UnifiedShare) { - var hasPassword by remember { mutableStateOf(share.password.isNotEmpty()) } - var hasExpiration by remember { mutableStateOf(false) } - var limitDownloads by remember { mutableStateOf(share.limit != null) } - - var hideDownloads by remember { mutableStateOf(false) } - var videoVerification by remember { mutableStateOf(false) } - var showFilesInGridView by remember { mutableStateOf(false) } - - OutlinedTextField( - value = "", - onValueChange = {}, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - label = { Text(stringResource(R.string.share_view_anyone_category_label)) }, - placeholder = { Text(stringResource(R.string.share_view_anyone_category_label_placeholder)) }, - singleLine = true - ) - - SettingsSwitchRow( - stringResource(R.string.share_view_anyone_category_expiration_date_switch), - hasExpiration - ) { hasExpiration = it } - - SettingsSwitchRow( - stringResource(R.string.share_view_anyone_category_password_switch), - hasPassword - ) { hasPassword = it } - - SettingsSwitchRow( - stringResource(R.string.share_view_anyone_category_limit_downloads_switch), - limitDownloads - ) { limitDownloads = it } - - SettingsSwitchRow( - stringResource(R.string.share_view_anyone_category_hide_downloads_switch), - hideDownloads - ) { hideDownloads = it } - - SettingsSwitchRow( - stringResource(R.string.share_view_anyone_category_video_verification_switch), - videoVerification - ) { videoVerification = it } - - SettingsSwitchRow( - stringResource(R.string.share_view_anyone_category_grid_view_switch), - showFilesInGridView - ) { showFilesInGridView = it } -} - @Composable private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { Row( @@ -548,47 +326,8 @@ private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: } } -@Composable -private fun ShareActionButtons( - share: UnifiedShare, - isSendEnabled: Boolean, - onCopyClick: (String) -> Unit, - onSendClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) { - if (share.category == UnifiedShareCategory.Invited) { - FilledTonalButton( - onClick = { onCopyClick("TODO") }, - modifier = Modifier.weight(1f) - ) { - Text(stringResource(R.string.share_view_copy_action)) - } - Spacer(modifier = Modifier.width(16.dp)) - Button( - onClick = onSendClick, - modifier = Modifier.weight(1f), - enabled = isSendEnabled - ) { - Text(stringResource(R.string.share_view_send_action)) - } - } else { - Button( - onClick = { onCopyClick("TODO") }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.share_view_create_public_link)) - } - } - } -} - enum class UnifiedSharesListItemType { Top, Mid, Bottom; - @Composable fun getShape(): RoundedCornerShape { return when (this) { @@ -599,7 +338,6 @@ enum class UnifiedSharesListItemType { } } -// NOTE: To just create a public link anyone tab + just send DOES SAME THING @Composable private fun UnifiedSharesListItem( share: Share, @@ -623,33 +361,18 @@ private fun UnifiedSharesListItem( }, ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), - leadingContent = { - share.type?.let { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - it.Icon() - } - } - }, headlineContent = { Text( - text = share.label, + text = "Share ${share.id}", style = MaterialTheme.typography.titleSmall ) }, supportingContent = { - share.permission?.getTextId()?.let { - Text( - text = stringResource(it), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + Text( + text = share.shareState.name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) }, trailingContent = { Box { @@ -657,10 +380,7 @@ private fun UnifiedSharesListItem( Icon(Icons.Default.MoreVert, contentDescription = "More options") } - DropdownMenu( - expanded = showContextMenu, - onDismissRequest = { showContextMenu = false } - ) { + DropdownMenu(expanded = showContextMenu, onDismissRequest = { showContextMenu = false }) { DropdownMenuItem( text = { Text(stringResource(R.string.share_view_list_item_edit)) }, onClick = { @@ -668,7 +388,6 @@ private fun UnifiedSharesListItem( onSelectShare(share) } ) - DropdownMenuItem( text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, onClick = { @@ -676,16 +395,9 @@ private fun UnifiedSharesListItem( showContextMenu = false } ) - HorizontalDivider() - DropdownMenuItem( - text = { - Text( - stringResource(R.string.share_view_list_item_delete), - color = MaterialTheme.colorScheme.error - ) - }, + text = { Text(stringResource(R.string.share_view_list_item_delete), color = MaterialTheme.colorScheme.error) }, onClick = { onDeleteShare(share) showContextMenu = false @@ -694,9 +406,7 @@ private fun UnifiedSharesListItem( } } }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ) + colors = ListItemDefaults.colors(containerColor = Color.Transparent) ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 1520f771..3dacf64f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -17,6 +17,15 @@ import kotlinx.serialization.json.JsonClassDiscriminator @Serializable sealed class Property +val Property.priority: Int + get() = when (this) { + is PropertyBoolean -> priority + is PropertyDate -> priority + is PropertyEnum -> priority + is PropertyPassword -> priority + is PropertyString -> priority + } + @Serializable @SerialName("boolean") data class PropertyBoolean( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt index a0491746..a2c62c28 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt @@ -10,7 +10,6 @@ package com.nextcloud.android.common.ui.share.model.ui import com.nextcloud.android.common.ui.share.model.api.share.Share sealed class ShareBottomSheetState { - data object Idle: ShareBottomSheetState() - data class New(val newShare: Share): ShareBottomSheetState() - data class Edit(val share: Share): ShareBottomSheetState() + object Idle : ShareBottomSheetState() + data class Edit(val share: Share) : ShareBottomSheetState() } From fb4dedd973545e039a656ea97fc34d2f1f1b94c0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 27 May 2026 16:49:33 +0300 Subject: [PATCH 019/174] add empty view state Signed-off-by: alperozturk96 --- .../ui/component/ContentUnavailableView.kt | 64 ++++++++++++++++++ .../android/common/ui/share/ShareView.kt | 66 +++++++++++++------ .../android/common/ui/share/ShareViewModel.kt | 4 +- .../common/ui/share/model/api/share/Share.kt | 1 + ui/src/main/res/values/strings.xml | 3 + 5 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt new file mode 100644 index 00000000..3d28ffc2 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt @@ -0,0 +1,64 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.component + +import androidx.compose.foundation.Image +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.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun ContentUnavailableView(title: String, description: String? = null, iconId: Int? = null) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + iconId?.let { + Image( + painter = painterResource(iconId), + modifier = Modifier.size(48.dp), + contentDescription = "", + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = title, + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center + ) + + description?.let { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = description, + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index d01255bf..45ab5d60 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -72,6 +72,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.model.api.property.Property @@ -86,7 +87,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareBottomSheetState import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository - @Composable private fun ShareView(viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() @@ -115,23 +115,33 @@ private fun ShareView(viewModel: ShareViewModel) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") } }, - snackbarHost = { SnackbarHost(snackbarHostState) } + snackbarHost = { SnackbarHost(snackbarHostState) }, + containerColor = Color.Transparent ) { paddingValues -> - LazyColumn(modifier = Modifier.padding(paddingValues)) { - itemsIndexed(shares) { index, share -> - val type = when (index) { - 0 -> UnifiedSharesListItemType.Top - shares.lastIndex -> UnifiedSharesListItemType.Bottom - else -> UnifiedSharesListItemType.Mid - } + if (shares.isEmpty()) { + ContentUnavailableView( + iconId = R.drawable.ic_person_add, + title = + stringResource(R.string.share_view_empty_title), + description = stringResource(R.string.share_view_empty_description) + ) + } else { + LazyColumn(modifier = Modifier.padding(paddingValues)) { + itemsIndexed(shares) { index, share -> + val type = when (index) { + 0 -> UnifiedSharesListItemType.Top + shares.lastIndex -> UnifiedSharesListItemType.Bottom + else -> UnifiedSharesListItemType.Mid + } - UnifiedSharesListItem( - share = share, - type = type, - onSelectShare = { selected -> bottomSheetState = ShareBottomSheetState.Edit(selected) }, - onDeleteShare = { viewModel.deleteShare(it.id) }, - onSendEmail = { /* TODO */ } - ) + UnifiedSharesListItem( + share = share, + type = type, + onSelectShare = { selected -> bottomSheetState = ShareBottomSheetState.Edit(selected) }, + onDeleteShare = { viewModel.deleteShare(it.id) }, + onSendEmail = { /* TODO */ } + ) + } } } } @@ -239,16 +249,20 @@ private fun DynamicPropertyField(shareId: String, property: Property, viewModel: } ) } + is PropertyString -> { OutlinedTextField( value = property.value ?: "", onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, label = { Text(property.displayName) }, placeholder = property.hint?.let { { Text(it) } }, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), singleLine = true ) } + is PropertyPassword -> { OutlinedTextField( value = property.value ?: "", @@ -256,20 +270,26 @@ private fun DynamicPropertyField(shareId: String, property: Property, viewModel: label = { Text(property.displayName) }, placeholder = property.hint?.let { { Text(it) } }, visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), singleLine = true ) } + is PropertyDate -> { // TODO: Wrap with a DatePickerDialog. Falling back to string entry for now. OutlinedTextField( value = property.value ?: "", onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, label = { Text(property.displayName + " (YYYY-MM-DD)") }, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), singleLine = true ) } + is PropertyEnum -> { // TODO: Implement ExposedDropdownMenuBox using property.validValues Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray) @@ -328,6 +348,7 @@ private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: enum class UnifiedSharesListItemType { Top, Mid, Bottom; + @Composable fun getShape(): RoundedCornerShape { return when (this) { @@ -397,7 +418,12 @@ private fun UnifiedSharesListItem( ) HorizontalDivider() DropdownMenuItem( - text = { Text(stringResource(R.string.share_view_list_item_delete), color = MaterialTheme.colorScheme.error) }, + text = { + Text( + stringResource(R.string.share_view_list_item_delete), + color = MaterialTheme.colorScheme.error + ) + }, onClick = { onDeleteShare(share) showContextMenu = false diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index ef712815..15ad093b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -43,11 +43,11 @@ class ShareViewModel( val errorMessageId: StateFlow = _errorMessageId init { - loadShares() + fetchShares() } // region shares list - fun loadShares( + fun fetchShares( sourceClass: String? = null, lastShareID: String? = null, limit: Int = 50 diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 8b67d24d..0d59ab31 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -25,6 +25,7 @@ data class Share( @SerialName("last_updated") val lastUpdated: Long, + @SerialName("state") val shareState: ShareState, val sources: List, diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 040c51d4..63441395 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -24,6 +24,9 @@ Name, team, email or federated ID Participants + No shares yet + Start sharing to see them in this list. + Note to recipients Anyone with the link From d15f768875c5eb0404237f071ac1d9fbefb58a78 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 27 May 2026 17:02:32 +0300 Subject: [PATCH 020/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 16 +++++++++++++--- .../ui/share/repository/ShareRemoteRepository.kt | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 45ab5d60..e3f369b9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -16,6 +16,7 @@ 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 @@ -126,7 +127,12 @@ private fun ShareView(viewModel: ShareViewModel) { description = stringResource(R.string.share_view_empty_description) ) } else { - LazyColumn(modifier = Modifier.padding(paddingValues)) { + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { itemsIndexed(shares) { index, share -> val type = when (index) { 0 -> UnifiedSharesListItemType.Top @@ -134,6 +140,10 @@ private fun ShareView(viewModel: ShareViewModel) { else -> UnifiedSharesListItemType.Mid } + if (index == 0) { + Spacer(modifier = Modifier.height(16.dp)) + } + UnifiedSharesListItem( share = share, type = type, @@ -372,7 +382,7 @@ private fun UnifiedSharesListItem( ListItem( modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(0.9f) .clip(type.getShape()) .combinedClickable( onClick = { onSelectShare(share) }, @@ -384,7 +394,7 @@ private fun UnifiedSharesListItem( .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), headlineContent = { Text( - text = "Share ${share.id}", + text = "Share ${share.id}", // TODO do not hardcode style = MaterialTheme.typography.titleSmall ) }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 6e0a391e..e5574bf7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -55,7 +55,8 @@ class ShareRemoteRepository( override suspend fun createShare(): NetworkResult = client.executeRequest( endpoint = SHARE_ENDPOINT, - method = HttpMethod.POST + method = HttpMethod.POST, + body = ByteArray(0).toRequestBody() ) { body -> json.decodeFromString>(body).ocs.data } From 270d8f0b3690ed1bcac712d7c6078b12fd8a36d6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 15:04:34 +0300 Subject: [PATCH 021/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 195 ++++++++++++++---- .../android/common/ui/share/ShareViewModel.kt | 45 +++- .../common/ui/share/model/api/share/Share.kt | 13 +- .../share/model/ui/ShareBottomSheetState.kt | 15 -- ...ifiedShareCategory.kt => ShareCategory.kt} | 2 +- ui/src/main/res/values/strings.xml | 5 +- 6 files changed, 207 insertions(+), 68 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt rename ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/{UnifiedShareCategory.kt => ShareCategory.kt} (87%) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index e3f369b9..218cfc10 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -12,6 +12,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -20,12 +21,14 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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.Close import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert @@ -33,10 +36,13 @@ import androidx.compose.material3.ColorScheme import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme @@ -84,17 +90,19 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword import com.nextcloud.android.common.ui.share.model.api.property.PropertyString import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share -import com.nextcloud.android.common.ui.share.model.ui.ShareBottomSheetState +import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository @Composable private fun ShareView(viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() - var bottomSheetState by remember { mutableStateOf(ShareBottomSheetState.Idle) } val shares by viewModel.shares.collectAsState() + val activeShare by viewModel.activeShare.collectAsState() val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } + val filteredShares = shares.filter { it.shareState != ShareState.DRAFT } LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -107,10 +115,7 @@ private fun ShareView(viewModel: ShareViewModel) { floatingActionButton = { FloatingActionButton( onClick = { - // Create an empty draft share immediately on the server, then edit it - viewModel.createShare { draft -> - bottomSheetState = ShareBottomSheetState.Edit(draft) - } + viewModel.createShare() }, ) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") @@ -119,12 +124,11 @@ private fun ShareView(viewModel: ShareViewModel) { snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = Color.Transparent ) { paddingValues -> - if (shares.isEmpty()) { + if (filteredShares.isEmpty()) { ContentUnavailableView( iconId = R.drawable.ic_person_add, title = stringResource(R.string.share_view_empty_title), - description = stringResource(R.string.share_view_empty_description) ) } else { LazyColumn( @@ -133,7 +137,7 @@ private fun ShareView(viewModel: ShareViewModel) { .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - itemsIndexed(shares) { index, share -> + itemsIndexed(filteredShares) { index, share -> val type = when (index) { 0 -> UnifiedSharesListItemType.Top shares.lastIndex -> UnifiedSharesListItemType.Bottom @@ -147,21 +151,21 @@ private fun ShareView(viewModel: ShareViewModel) { UnifiedSharesListItem( share = share, type = type, - onSelectShare = { selected -> bottomSheetState = ShareBottomSheetState.Edit(selected) }, + onSelectShare = { selected -> + viewModel.setActiveShare(selected) + }, onDeleteShare = { viewModel.deleteShare(it.id) }, - onSendEmail = { /* TODO */ } + onSendEmail = { } ) } } } } - if (bottomSheetState is ShareBottomSheetState.Edit) { - val state = bottomSheetState as ShareBottomSheetState.Edit + activeShare?.let { AddOrEditShareBottomSheet( - share = state.share, - viewModel = viewModel, - onDismiss = { bottomSheetState = ShareBottomSheetState.Idle } + share = it, + viewModel = viewModel ) } } @@ -170,22 +174,19 @@ private fun ShareView(viewModel: ShareViewModel) { @Composable private fun AddOrEditShareBottomSheet( share: Share, - viewModel: ShareViewModel, - onDismiss: () -> Unit + viewModel: ShareViewModel ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() - - // Group permissions dynamically by Category provided by the backend - val categories = remember(share.permissions) { - share.permissions.mapNotNull { it.category }.distinct() - } - var selectedCategory by remember { mutableStateOf(categories.firstOrNull() ?: "") } - + val context = LocalContext.current + val categories = remember { ShareCategory.entries.toList() } + var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } ModalBottomSheet( - onDismissRequest = onDismiss, + onDismissRequest = { + viewModel.setActiveShare(null) + }, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surface, ) { @@ -198,30 +199,29 @@ private fun AddOrEditShareBottomSheet( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - text = stringResource(R.string.share_view_bottom_sheet_edit_title, share.id), + text = share.title(context), style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(bottom = 8.dp) ) - // Dynamic Category Selector - if (categories.size > 1) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - categories.forEachIndexed { index, category -> - SegmentedButton( - selected = selectedCategory == category, - onClick = { selectedCategory = category }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size) - ) { - Text(category) - } + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + categories.forEachIndexed { index, category -> + SegmentedButton( + selected = selectedCategory == category, + onClick = { selectedCategory = category }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size) + ) { + Text(category.name) } } } - // Render Permissions for Selected Category - val activePermissions = share.permissions.filter { it.category == selectedCategory } - activePermissions.forEach { permission -> + if (selectedCategory == ShareCategory.Invited) { + RecipientSearchField(share, viewModel) + } + + share.permissions.forEach { permission -> SettingsSwitchRow( label = permission.displayName, checked = permission.enabled, @@ -231,7 +231,6 @@ private fun AddOrEditShareBottomSheet( ) } - // Render Dynamic Properties if (share.properties.isNotEmpty()) { CollapsibleSettingsSection( isExpanded = showAdvancedSettings, @@ -247,6 +246,110 @@ private fun AddOrEditShareBottomSheet( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RecipientSearchField( + share: Share, + viewModel: ShareViewModel +) { + var query by remember { mutableStateOf("") } + var expanded by remember { mutableStateOf(false) } + val results by viewModel.recipientSearchResults.collectAsState() + val chipScrollState = rememberScrollState() + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (share.recipients.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(chipScrollState), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + share.recipients.forEach { recipient -> + InputChip( + selected = true, + onClick = { }, + label = { Text(recipient.displayName) }, + trailingIcon = { + IconButton( + onClick = { + viewModel.removeRecipient( + id = share.id, + clazz = recipient.clazz, + value = recipient.value, + instance = recipient.instance + ) + }, + modifier = Modifier.size(16.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "remove recipient" + ) + } + } + ) + } + } + } + + ExposedDropdownMenuBox( + expanded = expanded && query.isNotBlank(), + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + value = query, + onValueChange = { + query = it + expanded = true + viewModel.onSearchQueryChanged(it) + }, + label = { Text(stringResource(R.string.share_view_invited_category_label)) }, + modifier = Modifier + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) + .fillMaxWidth(), + singleLine = true + ) + + if (query.isNotBlank()) { + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + if (results.isEmpty()) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.share_view_recipient_search_field_empty_result), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + onClick = {}, + enabled = false + ) + } else { + results.forEach { recipient -> + DropdownMenuItem( + text = { Text(recipient.displayName) }, + onClick = { + viewModel.addRecipient(share.id, recipient.clazz, recipient.value) + query = "" + expanded = false + } + ) + } + } + } + } + } + } +} + @Composable private fun DynamicPropertyField(shareId: String, property: Property, viewModel: ShareViewModel) { when (property) { @@ -393,8 +496,14 @@ private fun UnifiedSharesListItem( ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), headlineContent = { + val headline = if (share.recipients.isNotEmpty()) { + share.recipients.first().displayName + } else { + "" + } + Text( - text = "Share ${share.id}", // TODO do not hardcode + text = headline, style = MaterialTheme.typography.titleSmall ) }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 15ad093b..fae1d7b8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest @@ -21,8 +22,12 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -36,6 +41,11 @@ class ShareViewModel( private val _activeShare = MutableStateFlow(null) val activeShare: StateFlow = _activeShare + private val _searchQuery = MutableStateFlow("") + + private val _recipientSearchResults = MutableStateFlow>(emptyList()) + val recipientSearchResults: StateFlow> = _recipientSearchResults + private val _loading = MutableStateFlow(false) val loading: StateFlow = _loading @@ -44,7 +54,34 @@ class ShareViewModel( init { fetchShares() + initSearchQuery() + } + + // region search query + @OptIn(FlowPreview::class) + private fun initSearchQuery() { + viewModelScope.launch { + _searchQuery + .debounce(300L) + .distinctUntilChanged() + .filter { it.isNotBlank() } + .collect { query -> + executeSearch(query) + } + } + } + + fun onSearchQueryChanged(query: String) { + _searchQuery.value = query + } + + private suspend fun executeSearch(query: String) { + val result = repository.fetchRecipients(null, query, 10, 0) + if (result is NetworkResult.Success) { + _recipientSearchResults.value = result.data + } } + // endregion // region shares list fun fetchShares( @@ -84,7 +121,7 @@ class ShareViewModel( * Then sources can be added later. * */ - fun createShare(onCreated: (Share) -> Unit = {}) { + fun createShare() { launchWithLoading { handleResult( result = repository.createShare(), @@ -92,7 +129,6 @@ class ShareViewModel( ) { draft -> _activeShare.update { draft } _shares.update { current -> listOf(draft) + current } - onCreated(draft) } } } @@ -211,8 +247,8 @@ class ShareViewModel( _errorMessageId.update { value } } - fun clearActiveShare() { - _activeShare.update { null } + fun setActiveShare(value: Share?) { + _activeShare.update { value } } // endregion @@ -241,6 +277,5 @@ class ShareViewModel( _loading.update { false } } } - // endregion } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 0d59ab31..3d2032e4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -7,6 +7,8 @@ package com.nextcloud.android.common.ui.share.model.api.share +import android.content.Context +import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.owner.Owner import com.nextcloud.android.common.ui.share.model.api.permission.Permission import com.nextcloud.android.common.ui.share.model.api.property.Property @@ -35,4 +37,13 @@ data class Share( val properties: List, val permissions: List -) +) { + fun title(context: Context): String { + return if (shareState == ShareState.DRAFT) { + context.getString(R.string.share_view_bottom_sheet_new_title) + } else { + // TODO do not hardcode + context.getString(R.string.share_view_bottom_sheet_edit_title, "") + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt deleted file mode 100644 index a2c62c28..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareBottomSheetState.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.ui - -import com.nextcloud.android.common.ui.share.model.api.share.Share - -sealed class ShareBottomSheetState { - object Idle : ShareBottomSheetState() - data class Edit(val share: Share) : ShareBottomSheetState() -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt similarity index 87% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt index e93bd3d4..491b1b57 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/UnifiedShareCategory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt @@ -7,6 +7,6 @@ package com.nextcloud.android.common.ui.share.model.ui -enum class UnifiedShareCategory { +enum class ShareCategory { Invited, Anyone } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 63441395..0c60cec0 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -24,9 +24,8 @@ Name, team, email or federated ID Participants - No shares yet - Start sharing to see them in this list. - + Not shared + No result Note to recipients Anyone with the link From 4bee759b7b5aecd4d65d2c11b893c7b3249bfa8b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 15:48:42 +0300 Subject: [PATCH 022/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 21 ++- .../android/common/ui/share/ShareViewModel.kt | 132 ++++++++---------- .../share/repository/MockShareRepository.kt | 2 +- .../share/repository/ShareRemoteRepository.kt | 2 +- .../ui/share/repository/ShareRepository.kt | 2 +- 5 files changed, 77 insertions(+), 82 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 218cfc10..8fbe1c72 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -64,6 +64,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -94,13 +95,15 @@ import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository +import kotlinx.coroutines.launch @Composable -private fun ShareView(viewModel: ShareViewModel) { +private fun ShareView(sourceId: String, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() val shares by viewModel.shares.collectAsState() val activeShare by viewModel.activeShare.collectAsState() val context = LocalContext.current + val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val filteredShares = shares.filter { it.shareState != ShareState.DRAFT } @@ -115,7 +118,11 @@ private fun ShareView(viewModel: ShareViewModel) { floatingActionButton = { FloatingActionButton( onClick = { - viewModel.createShare() + scope.launch { + viewModel.createDraftShare()?.let { + viewModel.addSource(it.id, sourceId) + } + } }, ) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") @@ -272,7 +279,7 @@ private fun RecipientSearchField( share.recipients.forEach { recipient -> InputChip( selected = true, - onClick = { }, + onClick = { }, label = { Text(recipient.displayName) }, trailingIcon = { IconButton( @@ -559,7 +566,7 @@ private fun UnifiedSharesListItem( @Composable private fun PreviewLight() { PreviewTheme { - ShareView(viewModel = ShareViewModel(MockShareRepository())) + ShareView(sourceId = "", viewModel = ShareViewModel(MockShareRepository())) } } @@ -567,7 +574,7 @@ private fun PreviewLight() { @Composable private fun PreviewDark() { PreviewTheme(darkTheme = true) { - ShareView(viewModel = ShareViewModel(MockShareRepository())) + ShareView(sourceId = "", viewModel = ShareViewModel(MockShareRepository())) } } @@ -581,7 +588,7 @@ private fun PreviewTheme( } } -fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme, credentials: ServerCredentials) { +fun ComposeView.setupUnifiedShare(sourceId: String, credentials: ServerCredentials, colorScheme: ColorScheme) { val nextcloudHttpClient = NextcloudHttpClient.create(credentials) val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) @@ -589,7 +596,7 @@ fun ComposeView.setupUnifiedShare(colorScheme: ColorScheme, credentials: ServerC MaterialTheme( colorScheme = colorScheme, content = { - ShareView(viewModel) + ShareView(sourceId, viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index fae1d7b8..36f5502c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ShareViewModel( private val repository: ShareRepository @@ -89,47 +90,56 @@ class ShareViewModel( lastShareID: String? = null, limit: Int = 50 ) { - launchWithLoading { - handleResult( - result = repository.fetchShares( - sourceClass = sourceClass, - lastShareID = lastShareID, - limit = limit - ), - errorId = R.string.share_view_fetch_error_message - ) { _shares.update { it } } + viewModelScope.launch(Dispatchers.IO) { + _loading.update { true } + _errorMessageId.update { null } + + val result = repository.fetchShares(sourceClass, lastShareID, limit) + handleResult(result, R.string.share_view_fetch_error_message)?.let { fetchedShares -> + _shares.update { fetchedShares } + } + + _loading.update { false } } } fun fetchShare(id: String, request: GetShareRequest = GetShareRequest()) { - launchWithLoading { - handleResult( - result = repository.fetchShare(id, request), - errorId = R.string.share_view_fetch_error_message - ) { share -> + viewModelScope.launch(Dispatchers.IO) { + _loading.update { true } + _errorMessageId.update { null } + + val result = repository.fetchShare(id, request) + handleResult(result, R.string.share_view_fetch_error_message)?.let { share -> _activeShare.update { share } replaceInList(share) } + + _loading.update { false } } } // endregion // region create - /** * Creates an empty draft [Share] on the server and sets it as the [activeShare]. * Then sources can be added later. * */ - fun createShare() { - launchWithLoading { - handleResult( - result = repository.createShare(), - errorId = R.string.share_view_create_error_message - ) { draft -> - _activeShare.update { draft } - _shares.update { current -> listOf(draft) + current } - } + suspend fun createDraftShare(): Share? = withContext(Dispatchers.IO) { + _loading.update { true } + _errorMessageId.update { null } + + val result = repository.createDraftShare() + val draft = handleResult(result, R.string.share_view_create_error_message) + + if (draft != null) { + _activeShare.update { draft } + _shares.update { current -> listOf(draft) + current } + _loading.update { false } + draft + } else { + _loading.update { false } + null } } // endregion @@ -137,10 +147,8 @@ class ShareViewModel( // region state fun updateState(id: String, shareState: ShareState) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.updateShareState(id, UpdateShareStateRequest(shareState)), - errorId = R.string.share_view_update_error_message - ) { updated -> + val result = repository.updateShareState(id, UpdateShareStateRequest(shareState)) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -149,12 +157,11 @@ class ShareViewModel( // endregion // region sources - fun addSource(id: String, clazz: String, value: String) { + fun addSource(id: String, value: String) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.addShareSource(id, AddSourceRequest(clazz, value)), - errorId = R.string.share_view_update_error_message - ) { updated -> + val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" + val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -163,10 +170,8 @@ class ShareViewModel( fun removeSource(id: String, clazz: String, value: String) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.removeShareSource(id, clazz, value), - errorId = R.string.share_view_update_error_message - ) { updated -> + val result = repository.removeShareSource(id, clazz, value) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -177,10 +182,8 @@ class ShareViewModel( // region recipients fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)), - errorId = R.string.share_view_update_error_message - ) { updated -> + val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -189,10 +192,8 @@ class ShareViewModel( fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.removeShareRecipient(id, clazz, value, instance), - errorId = R.string.share_view_update_error_message - ) { updated -> + val result = repository.removeShareRecipient(id, clazz, value, instance) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -203,10 +204,8 @@ class ShareViewModel( // region properties fun updateProperty(id: String, clazz: String, value: String?) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value)), - errorId = R.string.share_view_update_error_message - ) { updated -> + val result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value)) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -217,10 +216,8 @@ class ShareViewModel( // region permissions fun updatePermission(id: String, clazz: String, enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)), - errorId = R.string.share_view_update_error_message - ) { updated -> + val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) + handleResult(result, R.string.share_view_update_error_message)?.let { updated -> _activeShare.update { updated } replaceInList(updated) } @@ -231,10 +228,8 @@ class ShareViewModel( // region delete fun deleteShare(id: String) { viewModelScope.launch(Dispatchers.IO) { - handleResult( - result = repository.deleteShare(id), - errorId = R.string.share_view_delete_error_message - ) { + val result = repository.deleteShare(id) + handleResult(result, R.string.share_view_delete_error_message)?.let { _shares.update { current -> current.filterNot { it.id == id } } if (_activeShare.value?.id == id) _activeShare.update { null } } @@ -259,22 +254,15 @@ class ShareViewModel( private fun handleResult( result: NetworkResult, - errorId: Int, - onSuccess: (T) -> Unit - ) { - when (result) { - is NetworkResult.Success -> onSuccess(result.data) + errorId: Int + ): T? { + return when (result) { + is NetworkResult.Success -> result.data is NetworkResult.ServerError, - is NetworkResult.NetworkException -> _errorMessageId.update { errorId } - } - } - - private fun launchWithLoading(block: suspend () -> Unit) { - viewModelScope.launch(Dispatchers.IO) { - _loading.update { true } - _errorMessageId.update { null } - block() - _loading.update { false } + is NetworkResult.NetworkException -> { + _errorMessageId.update { errorId } + null + } } } // endregion diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 8f7fcf94..f6a63048 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -276,7 +276,7 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(filtered) } - override suspend fun createShare(): NetworkResult { + override suspend fun createDraftShare(): NetworkResult { val share = buildShare( id = "mock-share-${System.currentTimeMillis()}", sources = emptyList(), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index e5574bf7..20ceac7c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -52,7 +52,7 @@ class ShareRemoteRepository( } } - override suspend fun createShare(): NetworkResult = + override suspend fun createDraftShare(): NetworkResult = client.executeRequest( endpoint = SHARE_ENDPOINT, method = HttpMethod.POST, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 302304bf..d10524b8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -25,7 +25,7 @@ interface ShareRepository { offset: Int ): NetworkResult> - suspend fun createShare(): NetworkResult + suspend fun createDraftShare(): NetworkResult suspend fun fetchShare( id: String, From 156e913051b9cc3a9a7793a04ebb25c4e2f2eb5f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 28 May 2026 16:02:29 +0300 Subject: [PATCH 023/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 8fbe1c72..e55c7be8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -20,8 +20,10 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState @@ -192,6 +194,9 @@ private fun AddOrEditShareBottomSheet( ModalBottomSheet( onDismissRequest = { + if (share.shareState == ShareState.DRAFT) { + viewModel.deleteShare(share.id) + } viewModel.setActiveShare(null) }, sheetState = sheetState, @@ -453,16 +458,30 @@ private fun CollapsibleSettingsSection( } @Composable -private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { +private fun SettingsSwitchRow( + label: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { Row( modifier = Modifier .fillMaxWidth() - .height(48.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + .heightIn(min = 48.dp) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically ) { - Text(text = label, style = MaterialTheme.typography.bodyLarge) - Switch(checked = checked, onCheckedChange = onCheckedChange) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) } } From 71b976a1c0922a476dd9c16b0d026bd03ae44ea5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 12:40:08 +0300 Subject: [PATCH 024/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index e55c7be8..2b00526b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -191,6 +191,8 @@ private fun AddOrEditShareBottomSheet( val categories = remember { ShareCategory.entries.toList() } var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } + var expandedCategories by remember { mutableStateOf(setOf()) } + val permissionsByCategory = share.permissions.groupBy { it.category } ModalBottomSheet( onDismissRequest = { @@ -233,22 +235,37 @@ private fun AddOrEditShareBottomSheet( RecipientSearchField(share, viewModel) } - share.permissions.forEach { permission -> - SettingsSwitchRow( - label = permission.displayName, - checked = permission.enabled, - onCheckedChange = { isChecked -> - viewModel.updatePermission(share.id, permission.clazz, isChecked) + // TODO use more better category names + permissionsByCategory.forEach { (category, permissions) -> + CollapsibleSettingsSection( + label = category ?: "", + isExpanded = category in expandedCategories, + onToggle = { + expandedCategories = if (category in expandedCategories) { + expandedCategories - category + } else { + expandedCategories + category + } + }, + ) { + permissions.forEach { permission -> + SettingsSwitchRow( + label = permission.displayName, + checked = permission.enabled, + onCheckedChange = { isChecked -> + viewModel.updatePermission(share.id, permission.clazz, isChecked) + } + ) } - ) + } } if (share.properties.isNotEmpty()) { CollapsibleSettingsSection( + label = stringResource(R.string.share_view_advanced_settings), isExpanded = showAdvancedSettings, onToggle = { showAdvancedSettings = !showAdvancedSettings } ) { - // Sort by server-defined priority share.properties.sortedBy { it.priority }.forEach { property -> DynamicPropertyField(share.id, property, viewModel) } @@ -424,6 +441,7 @@ private fun DynamicPropertyField(shareId: String, property: Property, viewModel: @Composable private fun CollapsibleSettingsSection( + label: String, isExpanded: Boolean, onToggle: () -> Unit, content: @Composable () -> Unit @@ -438,7 +456,7 @@ private fun CollapsibleSettingsSection( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = stringResource(R.string.share_view_advanced_settings), + text = label, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary ) @@ -450,9 +468,7 @@ private fun CollapsibleSettingsSection( } AnimatedVisibility(visible = isExpanded) { - Column { - content() - } + Column { content() } } } } From 4667219207d88cf3a98586a7ed12d6104e8dfe77 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 12:46:17 +0300 Subject: [PATCH 025/174] wip Signed-off-by: alperozturk96 --- ui/build.gradle | 4 ++ .../android/common/ui/share/ShareView.kt | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/ui/build.gradle b/ui/build.gradle index 3f1be988..d2a20ed9 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -56,6 +56,10 @@ dependencies { implementation("androidx.compose.material3:material3") implementation("androidx.compose.material:material-icons-core") + implementation("io.coil-kt.coil3:coil-compose:3.4.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0") + implementation("io.coil-kt.coil3:coil-svg:3.4.0") + implementation("com.vanniktech:ui:0.10.0") implementation 'androidx.core:core-ktx:1.19.0' diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 2b00526b..7082837f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -81,10 +82,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil3.ImageLoader +import coil3.compose.AsyncImage +import coil3.compose.setSingletonImageLoaderFactory +import coil3.request.ImageRequest +import coil3.svg.SvgDecoder import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate @@ -303,6 +310,14 @@ private fun RecipientSearchField( selected = true, onClick = { }, label = { Text(recipient.displayName) }, + leadingIcon = { + recipient.icon?.let { + RecipientIcon( + icon = it, + modifier = Modifier.size(18.dp) + ) + } + }, trailingIcon = { IconButton( onClick = { @@ -364,6 +379,14 @@ private fun RecipientSearchField( } else { results.forEach { recipient -> DropdownMenuItem( + leadingIcon = { + recipient.icon?.let { + RecipientIcon( + icon = it, + modifier = Modifier.size(20.dp) + ) + } + }, text = { Text(recipient.displayName) }, onClick = { viewModel.addRecipient(share.id, recipient.clazz, recipient.value) @@ -379,6 +402,23 @@ private fun RecipientSearchField( } } +@Composable +private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { + val isDark = isSystemInDarkTheme() + val url = if (isDark) icon.dark ?: icon.light else icon.light ?: icon.dark + + if (url != null) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(url) + .decoderFactory(SvgDecoder.Factory()) + .build(), + contentDescription = null, + modifier = modifier + ) + } +} + @Composable private fun DynamicPropertyField(shareId: String, property: Property, viewModel: ShareViewModel) { when (property) { @@ -628,6 +668,12 @@ fun ComposeView.setupUnifiedShare(sourceId: String, credentials: ServerCredentia val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) setContent { + setSingletonImageLoaderFactory { context -> + ImageLoader.Builder(context) + .components { add(SvgDecoder.Factory()) } + .build() + } + MaterialTheme( colorScheme = colorScheme, content = { From 904ab04cdb5a464c3d7ea6338c7d0fe2456d8d2c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 13:40:44 +0300 Subject: [PATCH 026/174] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/android/common/ui/share/ShareView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 7082837f..4c5ea5ff 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -414,7 +414,7 @@ private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { .decoderFactory(SvgDecoder.Factory()) .build(), contentDescription = null, - modifier = modifier + modifier = modifier, ) } } From 0f6aca6b1f837148c6cf21675beebc1d94b90426 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 15:27:41 +0300 Subject: [PATCH 027/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 48 +++++++++---------- .../api/capabilities/SharingCapabilities.kt | 39 +++++++++++++++ 2 files changed, 62 insertions(+), 25 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 4c5ea5ff..0a948c11 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -7,7 +7,6 @@ package com.nextcloud.android.common.ui.share -import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -80,7 +79,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.ImageLoader import coil3.compose.AsyncImage @@ -91,6 +89,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean @@ -102,12 +101,12 @@ import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory -import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json @Composable -private fun ShareView(sourceId: String, viewModel: ShareViewModel) { +private fun ShareView(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() val shares by viewModel.shares.collectAsState() val activeShare by viewModel.activeShare.collectAsState() @@ -181,6 +180,7 @@ private fun ShareView(sourceId: String, viewModel: ShareViewModel) { activeShare?.let { AddOrEditShareBottomSheet( share = it, + sharingCapabilities = sharingCapabilities, viewModel = viewModel ) } @@ -190,6 +190,7 @@ private fun ShareView(sourceId: String, viewModel: ShareViewModel) { @Composable private fun AddOrEditShareBottomSheet( share: Share, + sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -200,6 +201,11 @@ private fun AddOrEditShareBottomSheet( var showAdvancedSettings by remember { mutableStateOf(false) } var expandedCategories by remember { mutableStateOf(setOf()) } val permissionsByCategory = share.permissions.groupBy { it.category } + val categoryLabelMap = remember(sharingCapabilities) { + sharingCapabilities.permissionCategories.associate { + it.class_field to it.displayName + } + } ModalBottomSheet( onDismissRequest = { @@ -242,11 +248,11 @@ private fun AddOrEditShareBottomSheet( RecipientSearchField(share, viewModel) } - // TODO use more better category names permissionsByCategory.forEach { (category, permissions) -> CollapsibleSettingsSection( - label = category ?: "", - isExpanded = category in expandedCategories, + label = category + ?.let { categoryLabelMap[it] } ?: permissions.first().displayName, + isExpanded = category in expandedCategories, onToggle = { expandedCategories = if (category in expandedCategories) { expandedCategories - category @@ -637,22 +643,6 @@ private fun UnifiedSharesListItem( ) } -@Preview(name = "UnifiedShareView – light", showBackground = true) -@Composable -private fun PreviewLight() { - PreviewTheme { - ShareView(sourceId = "", viewModel = ShareViewModel(MockShareRepository())) - } -} - -@Preview(name = "UnifiedShareView – dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewDark() { - PreviewTheme(darkTheme = true) { - ShareView(sourceId = "", viewModel = ShareViewModel(MockShareRepository())) - } -} - @Composable private fun PreviewTheme( darkTheme: Boolean = false, @@ -663,7 +653,15 @@ private fun PreviewTheme( } } -fun ComposeView.setupUnifiedShare(sourceId: String, credentials: ServerCredentials, colorScheme: ColorScheme) { +private val json = Json { ignoreUnknownKeys = true } + +fun ComposeView.setupUnifiedShare( + sourceId: String, + sharingJson: String, + credentials: ServerCredentials, + colorScheme: ColorScheme +) { + val sharingCapabilities = json.decodeFromString(sharingJson) val nextcloudHttpClient = NextcloudHttpClient.create(credentials) val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) @@ -677,7 +675,7 @@ fun ComposeView.setupUnifiedShare(sourceId: String, credentials: ServerCredentia MaterialTheme( colorScheme = colorScheme, content = { - ShareView(sourceId, viewModel) + ShareView(sourceId, sharingCapabilities, viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt new file mode 100644 index 00000000..2f5f45fa --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.model.api.capabilities + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SharingCapabilities( + @SerialName("api_versions") + val apiVersions: List, + val legacy: Legacy, + @SerialName("permission_categories") + val permissionCategories: List, +) + +@Serializable +data class Legacy( + @SerialName("max_sources") + val maxSources: Long, + @SerialName("max_recipients") + val maxRecipients: Long, +) + +@Serializable +data class PermissionCategory( + @SerialName("class") + val class_field: String, + @SerialName("display_name") + val displayName: String, + val hint: String?, + val icon: String?, + val priority: Long, +) From fbd85234a151a7fd74ffed1d167608de73dff19c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 15:43:15 +0300 Subject: [PATCH 028/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 0a948c11..9f7b0376 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -64,6 +64,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -199,13 +200,7 @@ private fun AddOrEditShareBottomSheet( val categories = remember { ShareCategory.entries.toList() } var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } - var expandedCategories by remember { mutableStateOf(setOf()) } - val permissionsByCategory = share.permissions.groupBy { it.category } - val categoryLabelMap = remember(sharingCapabilities) { - sharingCapabilities.permissionCategories.associate { - it.class_field to it.displayName - } - } + var expandedCategories by remember { mutableStateOf(emptySet()) } ModalBottomSheet( onDismissRequest = { @@ -248,30 +243,38 @@ private fun AddOrEditShareBottomSheet( RecipientSearchField(share, viewModel) } - permissionsByCategory.forEach { (category, permissions) -> - CollapsibleSettingsSection( - label = category - ?.let { categoryLabelMap[it] } ?: permissions.first().displayName, - isExpanded = category in expandedCategories, - onToggle = { - expandedCategories = if (category in expandedCategories) { - expandedCategories - category - } else { - expandedCategories + category + sharingCapabilities.permissionCategories + .sortedBy { it.priority } + .forEach { sharingCapability -> + key(sharingCapability.class_field) { + CollapsibleSettingsSection( + label = sharingCapability.displayName, + isExpanded = sharingCapability.displayName in expandedCategories, + onToggle = { + expandedCategories = if (expandedCategories.contains(sharingCapability.displayName)) { + expandedCategories - sharingCapability.displayName + } else { + expandedCategories + sharingCapability.displayName + } + }, + ) { + share.permissions + .filter { permission -> permission.category == sharingCapability.class_field } + .sortedBy { it.displayName } + .forEach { permission -> + key(permission.clazz) { + SettingsSwitchRow( + label = permission.displayName, + checked = permission.enabled, + onCheckedChange = { isChecked -> + viewModel.updatePermission(share.id, permission.clazz, isChecked) + } + ) + } + } } - }, - ) { - permissions.forEach { permission -> - SettingsSwitchRow( - label = permission.displayName, - checked = permission.enabled, - onCheckedChange = { isChecked -> - viewModel.updatePermission(share.id, permission.clazz, isChecked) - } - ) } } - } if (share.properties.isNotEmpty()) { CollapsibleSettingsSection( From 0dd4cfac66df75e6212ea88054d83f04dc9561e4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 16:12:05 +0300 Subject: [PATCH 029/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareView.kt | 83 +++++++++++++++---- ui/src/main/res/drawable/ic_calendar.xml | 24 ++++++ ui/src/main/res/values/strings.xml | 3 + 3 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 ui/src/main/res/drawable/ic_calendar.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index 9f7b0376..da8999ea 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -35,6 +35,8 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ColorScheme +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -56,9 +58,11 @@ import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.getSelectedDate +import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -105,6 +109,7 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch import kotlinx.serialization.json.Json +import java.time.format.DateTimeFormatter @Composable private fun ShareView(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) { @@ -469,16 +474,37 @@ private fun DynamicPropertyField(shareId: String, property: Property, viewModel: } is PropertyDate -> { - // TODO: Wrap with a DatePickerDialog. Falling back to string entry for now. + var showDatePicker by remember { mutableStateOf(false) } + var dateValue by remember { mutableStateOf(property.value ?: "") } + OutlinedTextField( - value = property.value ?: "", - onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, - label = { Text(property.displayName + " (YYYY-MM-DD)") }, + value = dateValue, + onValueChange = { }, + label = { Text(property.displayName + " (MM-dd-yyyy)") }, modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), - singleLine = true + .padding(vertical = 4.dp) + .clickable { showDatePicker = true }, + trailingIcon = { + IconButton(onClick = { showDatePicker = true }) { + Icon( + painter = painterResource(R.drawable.ic_calendar), + contentDescription = "Pick date" + ) + } + }, + enabled = true, + readOnly = true ) + + if (showDatePicker) { + DatePickerModal(onDateSelected = { + dateValue = it ?: "" + viewModel.updateProperty(shareId, property.clazz, dateValue) + }, onDismiss = { + showDatePicker = false + }) + } } is PropertyEnum -> { @@ -488,6 +514,37 @@ private fun DynamicPropertyField(shareId: String, property: Property, viewModel: } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DatePickerModal( + onDateSelected: (String?) -> Unit, + onDismiss: () -> Unit +) { + val datePickerState = rememberDatePickerState() + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + val date = datePickerState.getSelectedDate() + val formatter = DateTimeFormatter.ofPattern("MM-dd-yyyy") + val formatted = date?.format(formatter) + onDateSelected(formatted) + onDismiss() + }) { + Text(stringResource(R.string.common_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.common_cancel)) + } + } + ) { + DatePicker(state = datePickerState) + } +} + @Composable private fun CollapsibleSettingsSection( label: String, @@ -646,16 +703,6 @@ private fun UnifiedSharesListItem( ) } -@Composable -private fun PreviewTheme( - darkTheme: Boolean = false, - content: @Composable () -> Unit -) { - MaterialTheme { - Surface(content = content) - } -} - private val json = Json { ignoreUnknownKeys = true } fun ComposeView.setupUnifiedShare( @@ -664,7 +711,7 @@ fun ComposeView.setupUnifiedShare( credentials: ServerCredentials, colorScheme: ColorScheme ) { - val sharingCapabilities = json.decodeFromString(sharingJson) + val sharingCapabilities = json.decodeFromString(sharingJson) val nextcloudHttpClient = NextcloudHttpClient.create(credentials) val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) diff --git a/ui/src/main/res/drawable/ic_calendar.xml b/ui/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 00000000..9e205862 --- /dev/null +++ b/ui/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,24 @@ + + + + diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 0c60cec0..f9272a52 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -6,6 +6,9 @@ --> + OK + Cancel + Create a new share Share %s From 62f6ced9c626e219cea86f543f541081bc0dbcef Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 29 May 2026 16:12:30 +0300 Subject: [PATCH 030/174] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/android/common/ui/share/ShareView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt index da8999ea..a43b2e61 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt @@ -493,7 +493,7 @@ private fun DynamicPropertyField(shareId: String, property: Property, viewModel: ) } }, - enabled = true, + enabled = false, readOnly = true ) From da3b28ad9043adf815ba72e9c6c72e761eece577 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 08:48:43 +0300 Subject: [PATCH 031/174] wip Signed-off-by: alperozturk96 --- .../ui/share/{ShareView.kt => ShareScreen.kt} | 392 +----------------- .../component/CollapsibleShareSection.kt | 60 +++ .../common/ui/share/component/PropertyView.kt | 78 ++++ .../share/component/RecipientSearchField.kt | 184 ++++++++ .../ui/share/component/ShareDateFormatter.kt | 31 ++ .../ui/share/component/ShareDatePicker.kt | 99 +++++ .../common/ui/share/component/ShareSwitch.kt | 50 +++ .../common/ui/share/model/ui/ShareItemType.kt | 25 ++ 8 files changed, 547 insertions(+), 372 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/share/{ShareView.kt => ShareScreen.kt} (50%) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt similarity index 50% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index a43b2e61..77583f21 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -1,68 +1,46 @@ /* - * Nextcloud Android Common Library + * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.nextcloud.android.common.ui.share -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.isSystemInDarkTheme 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.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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.Close -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ColorScheme -import androidx.compose.material3.DatePicker -import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuAnchorType -import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.InputChip 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.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.getSelectedDate -import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -83,36 +61,30 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import coil3.ImageLoader -import coil3.compose.AsyncImage import coil3.compose.setSingletonImageLoaderFactory -import coil3.request.ImageRequest import coil3.svg.SvgDecoder import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection +import com.nextcloud.android.common.ui.share.component.PropertyView +import com.nextcloud.android.common.ui.share.component.RecipientSearchField +import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities -import com.nextcloud.android.common.ui.share.model.api.icon.Icon -import com.nextcloud.android.common.ui.share.model.api.property.Property -import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean -import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate -import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum -import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword -import com.nextcloud.android.common.ui.share.model.api.property.PropertyString import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory +import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -import java.time.format.DateTimeFormatter @Composable -private fun ShareView(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) { +private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() val shares by viewModel.shares.collectAsState() val activeShare by viewModel.activeShare.collectAsState() @@ -160,16 +132,16 @@ private fun ShareView(sourceId: String, sharingCapabilities: SharingCapabilities ) { itemsIndexed(filteredShares) { index, share -> val type = when (index) { - 0 -> UnifiedSharesListItemType.Top - shares.lastIndex -> UnifiedSharesListItemType.Bottom - else -> UnifiedSharesListItemType.Mid + 0 -> ShareItemType.Top + shares.lastIndex -> ShareItemType.Bottom + else -> ShareItemType.Mid } if (index == 0) { Spacer(modifier = Modifier.height(16.dp)) } - UnifiedSharesListItem( + ShareItem( share = share, type = type, onSelectShare = { selected -> @@ -252,7 +224,7 @@ private fun AddOrEditShareBottomSheet( .sortedBy { it.priority } .forEach { sharingCapability -> key(sharingCapability.class_field) { - CollapsibleSettingsSection( + CollapsibleShareSection( label = sharingCapability.displayName, isExpanded = sharingCapability.displayName in expandedCategories, onToggle = { @@ -268,7 +240,7 @@ private fun AddOrEditShareBottomSheet( .sortedBy { it.displayName } .forEach { permission -> key(permission.clazz) { - SettingsSwitchRow( + ShareSwitch( label = permission.displayName, checked = permission.enabled, onCheckedChange = { isChecked -> @@ -282,13 +254,13 @@ private fun AddOrEditShareBottomSheet( } if (share.properties.isNotEmpty()) { - CollapsibleSettingsSection( + CollapsibleShareSection( label = stringResource(R.string.share_view_advanced_settings), isExpanded = showAdvancedSettings, onToggle = { showAdvancedSettings = !showAdvancedSettings } ) { share.properties.sortedBy { it.priority }.forEach { property -> - DynamicPropertyField(share.id, property, viewModel) + PropertyView(share.id, property, viewModel) } } } @@ -296,334 +268,10 @@ private fun AddOrEditShareBottomSheet( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun RecipientSearchField( - share: Share, - viewModel: ShareViewModel -) { - var query by remember { mutableStateOf("") } - var expanded by remember { mutableStateOf(false) } - val results by viewModel.recipientSearchResults.collectAsState() - val chipScrollState = rememberScrollState() - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (share.recipients.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(chipScrollState), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - share.recipients.forEach { recipient -> - InputChip( - selected = true, - onClick = { }, - label = { Text(recipient.displayName) }, - leadingIcon = { - recipient.icon?.let { - RecipientIcon( - icon = it, - modifier = Modifier.size(18.dp) - ) - } - }, - trailingIcon = { - IconButton( - onClick = { - viewModel.removeRecipient( - id = share.id, - clazz = recipient.clazz, - value = recipient.value, - instance = recipient.instance - ) - }, - modifier = Modifier.size(16.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "remove recipient" - ) - } - } - ) - } - } - } - - ExposedDropdownMenuBox( - expanded = expanded && query.isNotBlank(), - onExpandedChange = { expanded = it } - ) { - OutlinedTextField( - value = query, - onValueChange = { - query = it - expanded = true - viewModel.onSearchQueryChanged(it) - }, - label = { Text(stringResource(R.string.share_view_invited_category_label)) }, - modifier = Modifier - .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) - .fillMaxWidth(), - singleLine = true - ) - - if (query.isNotBlank()) { - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - if (results.isEmpty()) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.share_view_recipient_search_field_empty_result), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - onClick = {}, - enabled = false - ) - } else { - results.forEach { recipient -> - DropdownMenuItem( - leadingIcon = { - recipient.icon?.let { - RecipientIcon( - icon = it, - modifier = Modifier.size(20.dp) - ) - } - }, - text = { Text(recipient.displayName) }, - onClick = { - viewModel.addRecipient(share.id, recipient.clazz, recipient.value) - query = "" - expanded = false - } - ) - } - } - } - } - } - } -} - -@Composable -private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { - val isDark = isSystemInDarkTheme() - val url = if (isDark) icon.dark ?: icon.light else icon.light ?: icon.dark - - if (url != null) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(url) - .decoderFactory(SvgDecoder.Factory()) - .build(), - contentDescription = null, - modifier = modifier, - ) - } -} - -@Composable -private fun DynamicPropertyField(shareId: String, property: Property, viewModel: ShareViewModel) { - when (property) { - is PropertyBoolean -> { - SettingsSwitchRow( - label = property.displayName, - checked = property.value == "true", - onCheckedChange = { isChecked -> - viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) - } - ) - } - - is PropertyString -> { - OutlinedTextField( - value = property.value ?: "", - onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, - label = { Text(property.displayName) }, - placeholder = property.hint?.let { { Text(it) } }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - singleLine = true - ) - } - - is PropertyPassword -> { - OutlinedTextField( - value = property.value ?: "", - onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, - label = { Text(property.displayName) }, - placeholder = property.hint?.let { { Text(it) } }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - singleLine = true - ) - } - - is PropertyDate -> { - var showDatePicker by remember { mutableStateOf(false) } - var dateValue by remember { mutableStateOf(property.value ?: "") } - - OutlinedTextField( - value = dateValue, - onValueChange = { }, - label = { Text(property.displayName + " (MM-dd-yyyy)") }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clickable { showDatePicker = true }, - trailingIcon = { - IconButton(onClick = { showDatePicker = true }) { - Icon( - painter = painterResource(R.drawable.ic_calendar), - contentDescription = "Pick date" - ) - } - }, - enabled = false, - readOnly = true - ) - - if (showDatePicker) { - DatePickerModal(onDateSelected = { - dateValue = it ?: "" - viewModel.updateProperty(shareId, property.clazz, dateValue) - }, onDismiss = { - showDatePicker = false - }) - } - } - - is PropertyEnum -> { - // TODO: Implement ExposedDropdownMenuBox using property.validValues - Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun DatePickerModal( - onDateSelected: (String?) -> Unit, - onDismiss: () -> Unit -) { - val datePickerState = rememberDatePickerState() - - DatePickerDialog( - onDismissRequest = onDismiss, - confirmButton = { - TextButton(onClick = { - val date = datePickerState.getSelectedDate() - val formatter = DateTimeFormatter.ofPattern("MM-dd-yyyy") - val formatted = date?.format(formatter) - onDateSelected(formatted) - onDismiss() - }) { - Text(stringResource(R.string.common_ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.common_cancel)) - } - } - ) { - DatePicker(state = datePickerState) - } -} - -@Composable -private fun CollapsibleSettingsSection( - label: String, - 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 = label, - 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 SettingsSwitchRow( - label: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 48.dp) - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - - Spacer(modifier = Modifier.width(16.dp)) - - Switch( - checked = checked, - onCheckedChange = onCheckedChange - ) - } -} - -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( +private fun ShareItem( share: Share, - type: UnifiedSharesListItemType, + type: ShareItemType, onSelectShare: (Share) -> Unit, onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit @@ -725,7 +373,7 @@ fun ComposeView.setupUnifiedShare( MaterialTheme( colorScheme = colorScheme, content = { - ShareView(sourceId, sharingCapabilities, viewModel) + ShareScreen(sourceId, sharingCapabilities, viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt new file mode 100644 index 00000000..eaabb698 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -0,0 +1,60 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.animation.AnimatedVisibility +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun CollapsibleShareSection( + label: String, + 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 = label, + 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() } + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt new file mode 100644 index 00000000..aa8b568e --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt @@ -0,0 +1,78 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.model.api.property.Property +import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean +import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum +import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword +import com.nextcloud.android.common.ui.share.model.api.property.PropertyString + +@Composable +fun PropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { + when (property) { + is PropertyBoolean -> { + ShareSwitch( + label = property.displayName, + checked = property.value == "true", + onCheckedChange = { isChecked -> + viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) + } + ) + } + + is PropertyString -> { + OutlinedTextField( + value = property.value ?: "", + onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + label = { Text(property.displayName) }, + placeholder = property.hint?.let { { Text(it) } }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + singleLine = true + ) + } + + is PropertyPassword -> { + OutlinedTextField( + value = property.value ?: "", + onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + label = { Text(property.displayName) }, + placeholder = property.hint?.let { { Text(it) } }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + singleLine = true + ) + } + + is PropertyDate -> { + ShareDatePicker(property, onDateSelected = { dateValue -> + viewModel.updateProperty(shareId, property.clazz, dateValue) + }) + } + + is PropertyEnum -> { + // TODO: Implement ExposedDropdownMenuBox using property.validValues + Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray) + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt new file mode 100644 index 00000000..05d02d4c --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt @@ -0,0 +1,184 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.svg.SvgDecoder +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.model.api.icon.Icon +import com.nextcloud.android.common.ui.share.model.api.share.Share + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecipientSearchField( + share: Share, + viewModel: ShareViewModel +) { + var query by remember { mutableStateOf("") } + var expanded by remember { mutableStateOf(false) } + val results by viewModel.recipientSearchResults.collectAsState() + val chipScrollState = rememberScrollState() + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (share.recipients.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(chipScrollState), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + share.recipients.forEach { recipient -> + InputChip( + selected = true, + onClick = { }, + label = { Text(recipient.displayName) }, + leadingIcon = { + recipient.icon?.let { + RecipientIcon( + icon = it, + modifier = Modifier.size(18.dp) + ) + } + }, + trailingIcon = { + IconButton( + onClick = { + viewModel.removeRecipient( + id = share.id, + clazz = recipient.clazz, + value = recipient.value, + instance = recipient.instance + ) + }, + modifier = Modifier.size(16.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "remove recipient" + ) + } + } + ) + } + } + } + + ExposedDropdownMenuBox( + expanded = expanded && query.isNotBlank(), + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + value = query, + onValueChange = { + query = it + expanded = true + viewModel.onSearchQueryChanged(it) + }, + label = { Text(stringResource(R.string.share_view_invited_category_label)) }, + modifier = Modifier + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) + .fillMaxWidth(), + singleLine = true + ) + + if (query.isNotBlank()) { + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + if (results.isEmpty()) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.share_view_recipient_search_field_empty_result), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + onClick = {}, + enabled = false + ) + } else { + results.forEach { recipient -> + DropdownMenuItem( + leadingIcon = { + recipient.icon?.let { + RecipientIcon( + icon = it, + modifier = Modifier.size(20.dp) + ) + } + }, + text = { Text(recipient.displayName) }, + onClick = { + viewModel.addRecipient(share.id, recipient.clazz, recipient.value) + query = "" + expanded = false + } + ) + } + } + } + } + } + } +} + +@Composable +private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { + val isDark = isSystemInDarkTheme() + val url = if (isDark) icon.dark ?: icon.light else icon.light ?: icon.dark + + if (url != null) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(url) + .decoderFactory(SvgDecoder.Factory()) + .build(), + contentDescription = null, + modifier = modifier, + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt new file mode 100644 index 00000000..44d1aace --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.material3.DatePickerState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.getSelectedDate +import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import java.time.format.DateTimeFormatter + +class ShareDateFormatter { + companion object { + private const val PATTERN = "MM-dd-yyyy" + } + + fun getDisplayName(property: PropertyDate): String { + return property.displayName + " ($PATTERN)" + } + + @OptIn(ExperimentalMaterial3Api::class) + fun formatDate(datePickerState: DatePickerState): String? { + val date = datePickerState.getSelectedDate() + val formatter = DateTimeFormatter.ofPattern(PATTERN) + return date?.format(formatter) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt new file mode 100644 index 00000000..9b02e7eb --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt @@ -0,0 +1,99 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.getSelectedDate +import androidx.compose.material3.rememberDatePickerState +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.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import java.time.format.DateTimeFormatter + +@Composable +fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { + val formatter = ShareDateFormatter() + var showDatePicker by remember { mutableStateOf(false) } + var dateValue by remember { mutableStateOf(property.value ?: "") } + + OutlinedTextField( + value = dateValue, + onValueChange = { }, + label = { Text(formatter.getDisplayName(property)) }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { showDatePicker = true }, + trailingIcon = { + IconButton(onClick = { showDatePicker = true }) { + Icon( + painter = painterResource(R.drawable.ic_calendar), + contentDescription = "Pick date" + ) + } + }, + enabled = false, + readOnly = true + ) + + if (showDatePicker) { + DatePickerModal(formatter, onDateSelected = { + dateValue = it ?: "" + onDateSelected(dateValue) + }, onDismiss = { + showDatePicker = false + }) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DatePickerModal( + formatter: ShareDateFormatter, + onDateSelected: (String?) -> Unit, + onDismiss: () -> Unit +) { + val datePickerState = rememberDatePickerState() + + DatePickerDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { + onDateSelected(formatter.formatDate(datePickerState)) + onDismiss() + }) { + Text(stringResource(R.string.common_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.common_cancel)) + } + } + ) { + DatePicker(state = datePickerState) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt new file mode 100644 index 00000000..61499f2a --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ShareSwitch( + label: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 48.dp) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt new file mode 100644 index 00000000..20205442 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +enum class ShareItemType { + 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) + } + } +} From 5823536095ecf8afa257c0e4c9f96630e69b5764 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 08:49:41 +0300 Subject: [PATCH 032/174] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/android/common/ui/share/ShareScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 77583f21..54215f71 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -353,7 +353,7 @@ private fun ShareItem( private val json = Json { ignoreUnknownKeys = true } -fun ComposeView.setupUnifiedShare( +fun ComposeView.initShareScreen( sourceId: String, sharingJson: String, credentials: ServerCredentials, From adb0ad8299f3150b732305c4fe9f0d5bd25423ce Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 08:59:44 +0300 Subject: [PATCH 033/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 122 +--------- .../component/AddOrEditShareBottomSheet.kt | 195 +++++++++++++++ .../share/component/RecipientSearchField.kt | 224 +++++++++++------- .../SharePropertyView.kt} | 6 +- .../datepicker}/ShareDatePicker.kt | 7 +- .../datepicker/util}/ShareDateFormatter.kt | 4 +- 6 files changed, 339 insertions(+), 219 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{PropertyView.kt => property/SharePropertyView.kt} (90%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{ => property/datepicker}/ShareDatePicker.kt (92%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{ => property/datepicker/util}/ShareDateFormatter.kt (91%) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 54215f71..5b040368 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -9,23 +9,18 @@ package com.nextcloud.android.common.ui.share import androidx.compose.foundation.background 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.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ColorScheme import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -33,20 +28,14 @@ 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.Scaffold -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -69,15 +58,10 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient -import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection -import com.nextcloud.android.common.ui.share.component.PropertyView -import com.nextcloud.android.common.ui.share.component.RecipientSearchField -import com.nextcloud.android.common.ui.share.component.ShareSwitch +import com.nextcloud.android.common.ui.share.component.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities -import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState -import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @@ -164,110 +148,6 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AddOrEditShareBottomSheet( - share: Share, - sharingCapabilities: SharingCapabilities, - viewModel: ShareViewModel -) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val scrollState = rememberScrollState() - val context = LocalContext.current - val categories = remember { ShareCategory.entries.toList() } - var selectedCategory by remember { mutableStateOf(categories.first()) } - var showAdvancedSettings by remember { mutableStateOf(false) } - var expandedCategories by remember { mutableStateOf(emptySet()) } - - ModalBottomSheet( - onDismissRequest = { - if (share.shareState == ShareState.DRAFT) { - viewModel.deleteShare(share.id) - } - viewModel.setActiveShare(null) - }, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 32.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = share.title(context), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 8.dp) - ) - - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - categories.forEachIndexed { index, category -> - SegmentedButton( - selected = selectedCategory == category, - onClick = { selectedCategory = category }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size) - ) { - Text(category.name) - } - } - } - - if (selectedCategory == ShareCategory.Invited) { - RecipientSearchField(share, viewModel) - } - - sharingCapabilities.permissionCategories - .sortedBy { it.priority } - .forEach { sharingCapability -> - key(sharingCapability.class_field) { - CollapsibleShareSection( - label = sharingCapability.displayName, - isExpanded = sharingCapability.displayName in expandedCategories, - onToggle = { - expandedCategories = if (expandedCategories.contains(sharingCapability.displayName)) { - expandedCategories - sharingCapability.displayName - } else { - expandedCategories + sharingCapability.displayName - } - }, - ) { - share.permissions - .filter { permission -> permission.category == sharingCapability.class_field } - .sortedBy { it.displayName } - .forEach { permission -> - key(permission.clazz) { - ShareSwitch( - label = permission.displayName, - checked = permission.enabled, - onCheckedChange = { isChecked -> - viewModel.updatePermission(share.id, permission.clazz, isChecked) - } - ) - } - } - } - } - } - - if (share.properties.isNotEmpty()) { - CollapsibleShareSection( - label = stringResource(R.string.share_view_advanced_settings), - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings } - ) { - share.properties.sortedBy { it.priority }.forEach { property -> - PropertyView(share.id, property, viewModel) - } - } - } - } - } -} - @Composable private fun ShareItem( share: Share, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt new file mode 100644 index 00000000..d42ba258 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt @@ -0,0 +1,195 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.component.property.SharePropertyView +import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities +import com.nextcloud.android.common.ui.share.model.api.property.priority +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.ui.ShareCategory + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddOrEditShareBottomSheet( + share: Share, + sharingCapabilities: SharingCapabilities, + viewModel: ShareViewModel +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scrollState = rememberScrollState() + val categories = remember { ShareCategory.entries.toList() } + var selectedCategory by remember { mutableStateOf(categories.first()) } + var showAdvancedSettings by remember { mutableStateOf(false) } + var expandedCategories by remember { mutableStateOf(emptySet()) } + + ModalBottomSheet( + onDismissRequest = { + if (share.shareState == ShareState.DRAFT) { + viewModel.deleteShare(share.id) + } + viewModel.setActiveShare(null) + }, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ShareTitle(share) + + CategorySelector( + categories = categories, + selectedCategory = selectedCategory, + onCategorySelected = { selectedCategory = it } + ) + + if (selectedCategory == ShareCategory.Invited) { + RecipientSearchField(share, viewModel) + } + + PermissionCategories( + share = share, + sharingCapabilities = sharingCapabilities, + expandedCategories = expandedCategories, + onToggleCategory = { categoryName -> + expandedCategories = if (expandedCategories.contains(categoryName)) { + expandedCategories - categoryName + } else { + expandedCategories + categoryName + } + }, + viewModel = viewModel + ) + + if (share.properties.isNotEmpty()) { + AdvancedSettingsSection( + share = share, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + viewModel = viewModel + ) + } + } + } +} + +@Composable +private fun ShareTitle(share: Share) { + val context = LocalContext.current + Text( + text = share.title(context), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) +} + +@Composable +private fun CategorySelector( + categories: List, + selectedCategory: ShareCategory, + onCategorySelected: (ShareCategory) -> Unit +) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + categories.forEachIndexed { index, category -> + SegmentedButton( + selected = selectedCategory == category, + onClick = { onCategorySelected(category) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size) + ) { + Text(category.name) + } + } + } +} + +@Composable +private fun PermissionCategories( + share: Share, + sharingCapabilities: SharingCapabilities, + expandedCategories: Set, + onToggleCategory: (String) -> Unit, + viewModel: ShareViewModel +) { + sharingCapabilities.permissionCategories + .sortedBy { it.priority } + .forEach { sharingCapability -> + key(sharingCapability.class_field) { + CollapsibleShareSection( + label = sharingCapability.displayName, + isExpanded = sharingCapability.displayName in expandedCategories, + onToggle = { onToggleCategory(sharingCapability.displayName) }, + ) { + share.permissions + .filter { permission -> permission.category == sharingCapability.class_field } + .sortedBy { it.displayName } + .forEach { permission -> + key(permission.clazz) { + ShareSwitch( + label = permission.displayName, + checked = permission.enabled, + onCheckedChange = { isChecked -> + viewModel.updatePermission(share.id, permission.clazz, isChecked) + } + ) + } + } + } + } + } +} + +@Composable +private fun AdvancedSettingsSection( + share: Share, + isExpanded: Boolean, + onToggle: () -> Unit, + viewModel: ShareViewModel +) { + CollapsibleShareSection( + label = stringResource(R.string.share_view_advanced_settings), + isExpanded = isExpanded, + onToggle = onToggle + ) { + share.properties.sortedBy { it.priority }.forEach { property -> + SharePropertyView(share.id, property, viewModel) + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt index 05d02d4c..4e14adf4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt @@ -44,6 +44,7 @@ import coil3.svg.SvgDecoder import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.model.api.icon.Icon +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share @OptIn(ExperimentalMaterial3Api::class) @@ -62,110 +63,155 @@ fun RecipientSearchField( verticalArrangement = Arrangement.spacedBy(8.dp) ) { if (share.recipients.isNotEmpty()) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(chipScrollState), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - share.recipients.forEach { recipient -> - InputChip( - selected = true, - onClick = { }, - label = { Text(recipient.displayName) }, - leadingIcon = { - recipient.icon?.let { - RecipientIcon( - icon = it, - modifier = Modifier.size(18.dp) - ) - } - }, - trailingIcon = { - IconButton( - onClick = { - viewModel.removeRecipient( - id = share.id, - clazz = recipient.clazz, - value = recipient.value, - instance = recipient.instance - ) - }, - modifier = Modifier.size(16.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "remove recipient" - ) - } - } + RecipientChipRow( + recipients = share.recipients, + chipScrollState = chipScrollState, + onRemove = { recipient -> + viewModel.removeRecipient( + id = share.id, + clazz = recipient.clazz, + value = recipient.value, + instance = recipient.instance ) } - } + ) } - ExposedDropdownMenuBox( - expanded = expanded && query.isNotBlank(), - onExpandedChange = { expanded = it } + RecipientSearchDropdown( + query = query, + expanded = expanded, + onQueryChange = { + query = it + expanded = true + viewModel.onSearchQueryChanged(it) + }, + onExpandedChange = { expanded = it }, + onDismiss = { expanded = false } ) { - OutlinedTextField( - value = query, - onValueChange = { - query = it - expanded = true - viewModel.onSearchQueryChanged(it) - }, - label = { Text(stringResource(R.string.share_view_invited_category_label)) }, - modifier = Modifier - .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) - .fillMaxWidth(), - singleLine = true + RecipientDropdownContent( + results = results, + onSelect = { recipient -> + viewModel.addRecipient(share.id, recipient.clazz, recipient.value) + query = "" + expanded = false + } ) + } + } +} - if (query.isNotBlank()) { - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - if (results.isEmpty()) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.share_view_recipient_search_field_empty_result), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - onClick = {}, - enabled = false +@Composable +private fun RecipientChipRow( + recipients: List, + chipScrollState: androidx.compose.foundation.ScrollState, + onRemove: (Recipient) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(chipScrollState), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + recipients.forEach { recipient -> + InputChip( + selected = true, + onClick = { }, + label = { Text(recipient.displayName) }, + leadingIcon = { + recipient.icon?.let { + RecipientIcon( + icon = it, + modifier = Modifier.size(18.dp) + ) + } + }, + trailingIcon = { + IconButton( + onClick = { onRemove(recipient) }, + modifier = Modifier.size(16.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "remove recipient" ) - } else { - results.forEach { recipient -> - DropdownMenuItem( - leadingIcon = { - recipient.icon?.let { - RecipientIcon( - icon = it, - modifier = Modifier.size(20.dp) - ) - } - }, - text = { Text(recipient.displayName) }, - onClick = { - viewModel.addRecipient(share.id, recipient.clazz, recipient.value) - query = "" - expanded = false - } - ) - } } } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RecipientSearchDropdown( + query: String, + expanded: Boolean, + onQueryChange: (String) -> Unit, + onExpandedChange: (Boolean) -> Unit, + onDismiss: () -> Unit, + content: @Composable () -> Unit +) { + ExposedDropdownMenuBox( + expanded = expanded && query.isNotBlank(), + onExpandedChange = onExpandedChange + ) { + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + label = { Text(stringResource(R.string.share_view_invited_category_label)) }, + modifier = Modifier + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) + .fillMaxWidth(), + singleLine = true + ) + + if (query.isNotBlank()) { + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = onDismiss + ) { + content() } } } } +@Composable +private fun RecipientDropdownContent( + results: List, + onSelect: (Recipient) -> Unit +) { + if (results.isEmpty()) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.share_view_recipient_search_field_empty_result), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + onClick = {}, + enabled = false + ) + } else { + results.forEach { recipient -> + DropdownMenuItem( + leadingIcon = { + recipient.icon?.let { + RecipientIcon( + icon = it, + modifier = Modifier.size(20.dp) + ) + } + }, + text = { Text(recipient.displayName) }, + onClick = { onSelect(recipient) } + ) + } + } +} + @Composable private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { val isDark = isSystemInDarkTheme() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt similarity index 90% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index aa8b568e..28059ad2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/PropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.android.common.ui.share.component +package com.nextcloud.android.common.ui.share.component.property import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -17,6 +17,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.component.property.datepicker.ShareDatePicker +import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate @@ -25,7 +27,7 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword import com.nextcloud.android.common.ui.share.model.api.property.PropertyString @Composable -fun PropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { +fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { when (property) { is PropertyBoolean -> { ShareSwitch( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt similarity index 92% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 9b02e7eb..1b0325e0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -5,20 +5,18 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.android.common.ui.share.component +package com.nextcloud.android.common.ui.share.component.property.datepicker import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.getSelectedDate import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -30,8 +28,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate -import java.time.format.DateTimeFormatter @Composable fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { @@ -69,7 +67,6 @@ fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun DatePickerModal( formatter: ShareDateFormatter, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt similarity index 91% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt index 44d1aace..d24880db 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareDateFormatter.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.android.common.ui.share.component +package com.nextcloud.android.common.ui.share.component.property.datepicker.util import androidx.compose.material3.DatePickerState import androidx.compose.material3.ExperimentalMaterial3Api @@ -28,4 +28,4 @@ class ShareDateFormatter { val formatter = DateTimeFormatter.ofPattern(PATTERN) return date?.format(formatter) } -} +} \ No newline at end of file From e62d5d74ab9d570707f5afe92413b37728b41c77 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 09:11:06 +0300 Subject: [PATCH 034/174] wip Signed-off-by: alperozturk96 --- .../property/datepicker/ShareDatePicker.kt | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 1b0325e0..dbe6e8a1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -8,12 +8,12 @@ package com.nextcloud.android.common.ui.share.component.property.datepicker import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -26,7 +26,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate @@ -37,25 +37,29 @@ fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { var showDatePicker by remember { mutableStateOf(false) } var dateValue by remember { mutableStateOf(property.value ?: "") } - OutlinedTextField( - value = dateValue, - onValueChange = { }, - label = { Text(formatter.getDisplayName(property)) }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clickable { showDatePicker = true }, - trailingIcon = { - IconButton(onClick = { showDatePicker = true }) { + + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedTextField( + value = dateValue, + onValueChange = {}, + readOnly = true, + enabled = true, + modifier = Modifier.fillMaxWidth(), + label = { Text(formatter.getDisplayName(property)) }, + trailingIcon = { Icon( painter = painterResource(R.drawable.ic_calendar), - contentDescription = "Pick date" + contentDescription = null ) } - }, - enabled = false, - readOnly = true - ) + ) + + Box( + modifier = Modifier + .matchParentSize() + .clickable { showDatePicker = true } + ) + } if (showDatePicker) { DatePickerModal(formatter, onDateSelected = { @@ -94,3 +98,22 @@ private fun DatePickerModal( DatePicker(state = datePickerState) } } + +@Preview(showBackground = true) +@Composable +private fun ShareDatePickerPreview() { + MaterialTheme { + ShareDatePicker( + property = PropertyDate( + clazz = "expiry", + displayName = "Expiration Date", + priority = 1, + required = false, + value = "2026-12-31", + minDate = "2026-01-01", + maxDate = "2027-12-31" + ), + onDateSelected = {} + ) + } +} From 3171edb8c1ae13e4ab56113329e55c60d579de3d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 09:16:03 +0300 Subject: [PATCH 035/174] wip Signed-off-by: alperozturk96 --- .../common/ui/share/model/api/property/Property.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 3dacf64f..2aeb3d8c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -26,6 +26,15 @@ val Property.priority: Int is PropertyString -> priority } +val Property.clazz: String + get() = when (this) { + is PropertyBoolean -> clazz + is PropertyDate -> clazz + is PropertyEnum -> clazz + is PropertyPassword -> clazz + is PropertyString -> clazz + } + @Serializable @SerialName("boolean") data class PropertyBoolean( @@ -42,7 +51,9 @@ data class PropertyBoolean( val required: Boolean, val value: String? = null -) : Property() +) : Property() { + fun isTrue(): Boolean = (value == "true") +} @Serializable @SerialName("date") From d887659829c6999d92cd567187745b5e207a3137 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 09:52:27 +0300 Subject: [PATCH 036/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 18 ++---- .../android/common/ui/share/ShareViewModel.kt | 17 +++++ .../component/AddOrEditShareBottomSheet.kt | 10 ++- .../component/CollapsibleShareSection.kt | 6 +- .../share/component/RecipientSearchField.kt | 6 +- .../common/ui/share/component/ShareSwitch.kt | 6 +- .../component/property/SharePropertyView.kt | 64 +++++++++++++++---- .../property/datepicker/ShareDatePicker.kt | 6 +- .../datepicker/util/ShareDateFormatter.kt | 6 +- .../api/capabilities/SharingCapabilities.kt | 6 +- .../common/ui/share/model/ui/ShareItemType.kt | 16 ++++- 11 files changed, 114 insertions(+), 47 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 5b040368..cb271ba2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share @@ -61,7 +61,6 @@ import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.component.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share -import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @@ -75,7 +74,6 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti val context = LocalContext.current val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - val filteredShares = shares.filter { it.shareState != ShareState.DRAFT } LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -101,7 +99,7 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = Color.Transparent ) { paddingValues -> - if (filteredShares.isEmpty()) { + if (shares.isEmpty()) { ContentUnavailableView( iconId = R.drawable.ic_person_add, title = @@ -114,12 +112,8 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - itemsIndexed(filteredShares) { index, share -> - val type = when (index) { - 0 -> ShareItemType.Top - shares.lastIndex -> ShareItemType.Bottom - else -> ShareItemType.Mid - } + itemsIndexed(shares, key = { _, share -> share.id }) { index, share -> + val type = ShareItemType.type(index, shares.lastIndex) if (index == 0) { Spacer(modifier = Modifier.height(16.dp)) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 36f5502c..afc91946 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -53,6 +53,8 @@ class ShareViewModel( private val _errorMessageId = MutableStateFlow(null) val errorMessageId: StateFlow = _errorMessageId + private val pendingProperties = mutableMapOf() + init { fetchShares() initSearchQuery() @@ -211,6 +213,21 @@ class ShareViewModel( } } } + + fun updatePropertyLocally(clazz: String, value: String) { + pendingProperties[clazz] = value + } + + fun commitPendingProperties(shareId: String) { + if (pendingProperties.isEmpty()) return + + viewModelScope.launch(Dispatchers.IO) { + pendingProperties.forEach { (clazz, value) -> + repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value)) + } + pendingProperties.clear() + } + } // endregion // region permissions diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt index d42ba258..0ca202f1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt @@ -35,6 +35,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.property.SharePropertyView import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities +import com.nextcloud.android.common.ui.share.model.api.property.clazz import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState @@ -56,6 +57,7 @@ fun AddOrEditShareBottomSheet( ModalBottomSheet( onDismissRequest = { + viewModel.commitPendingProperties(share.id) if (share.shareState == ShareState.DRAFT) { viewModel.deleteShare(share.id) } @@ -189,7 +191,13 @@ private fun AdvancedSettingsSection( onToggle = onToggle ) { share.properties.sortedBy { it.priority }.forEach { property -> - SharePropertyView(share.id, property, viewModel) + key(property.clazz) { + SharePropertyView( + shareId = share.id, + property = property, + viewModel = viewModel + ) + } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt index eaabb698..affe1da5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.component diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt index 4e14adf4..9dfd0a6f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.component diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index 61499f2a..8af52f43 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.component diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 28059ad2..974e381e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.component.property @@ -12,6 +12,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -25,37 +30,54 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword import com.nextcloud.android.common.ui.share.model.api.property.PropertyString +import kotlinx.coroutines.delay @Composable fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { when (property) { is PropertyBoolean -> { + var checkedValue by remember(property.clazz) { mutableStateOf(property.isTrue()) } + ShareSwitch( label = property.displayName, - checked = property.value == "true", + checked = checkedValue, onCheckedChange = { isChecked -> - viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) + checkedValue = isChecked + viewModel.updatePropertyLocally(property.clazz, isChecked.toString()) } ) } is PropertyString -> { + var textValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } + + DebouncedPropertyUpdater( + value = textValue, + originalValue = property.value ?: "", + onDebounceComplete = { viewModel.updatePropertyLocally(property.clazz, it) } + ) + OutlinedTextField( - value = property.value ?: "", - onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + value = textValue, + onValueChange = { textValue = it }, label = { Text(property.displayName) }, - placeholder = property.hint?.let { { Text(it) } }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), singleLine = true ) } is PropertyPassword -> { + var passwordValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } + + DebouncedPropertyUpdater( + value = passwordValue, + originalValue = property.value ?: "", + onDebounceComplete = { viewModel.updatePropertyLocally(property.clazz, it) } + ) + OutlinedTextField( - value = property.value ?: "", - onValueChange = { viewModel.updateProperty(shareId, property.clazz, it) }, + value = passwordValue, + onValueChange = { passwordValue = it }, label = { Text(property.displayName) }, placeholder = property.hint?.let { { Text(it) } }, visualTransformation = PasswordVisualTransformation(), @@ -78,3 +100,19 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM } } } + +@Composable +private fun DebouncedPropertyUpdater( + value: String, + originalValue: String, + delayMillis: Long = 400L, + onDebounceComplete: (String) -> Unit +) { + LaunchedEffect(value) { + if (value != originalValue) { + delay(delayMillis) + onDebounceComplete(value) + } + } +} + diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index dbe6e8a1..a99fae18 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.component.property.datepicker diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt index d24880db..7caca750 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.component.property.datepicker.util diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 2f5f45fa..545f925c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.model.api.capabilities diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt index 20205442..f3c08a5f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ package com.nextcloud.android.common.ui.share.model.ui @@ -22,4 +22,14 @@ enum class ShareItemType { Bottom -> RoundedCornerShape(4.dp, 4.dp, 12.dp, 12.dp) } } + + companion object { + fun type(index: Int, lastIndex: Int): ShareItemType { + return when (index) { + 0 -> Top + lastIndex -> Bottom + else -> Mid + } + } + } } From 4d80ce161b9ec8f624e23f9db44531ee8c9d005f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 10:10:20 +0300 Subject: [PATCH 037/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 71 +++++++++++-------- .../android/common/ui/share/ShareViewModel.kt | 58 +++++++-------- .../component/AddOrEditShareBottomSheet.kt | 30 ++++++-- .../component/DiscardDraftShareDialog.kt | 37 ++++++++++ .../ui/share/model/ui/ShareScreenState.kt | 16 +++++ ui/src/main/res/values/strings.xml | 5 ++ 6 files changed, 149 insertions(+), 68 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index cb271ba2..924b83f4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -18,6 +19,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -62,6 +64,7 @@ import com.nextcloud.android.common.ui.share.component.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.ShareItemType +import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -69,7 +72,7 @@ import kotlinx.serialization.json.Json @Composable private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() - val shares by viewModel.shares.collectAsState() + val screenState by viewModel.state.collectAsState() val activeShare by viewModel.activeShare.collectAsState() val context = LocalContext.current val scope = rememberCoroutineScope() @@ -99,35 +102,47 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti snackbarHost = { SnackbarHost(snackbarHostState) }, containerColor = Color.Transparent ) { paddingValues -> - if (shares.isEmpty()) { - ContentUnavailableView( - iconId = R.drawable.ic_person_add, - title = - stringResource(R.string.share_view_empty_title), - ) - } else { - LazyColumn( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - itemsIndexed(shares, key = { _, share -> share.id }) { index, share -> - val type = ShareItemType.type(index, shares.lastIndex) + when (val state = screenState) { + is ShareScreenState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + is ShareScreenState.Empty -> { + ContentUnavailableView( + iconId = R.drawable.ic_person_add, + title = stringResource(R.string.share_view_empty_title), + ) + } + is ShareScreenState.Loaded -> { + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + itemsIndexed(state.shares, key = { _, share -> share.id }) { index, share -> + val type = ShareItemType.type(index, state.shares.lastIndex) - if (index == 0) { - Spacer(modifier = Modifier.height(16.dp)) - } + if (index == 0) { + Spacer(modifier = Modifier.height(16.dp)) + } else { + Spacer(modifier = Modifier.height(2.dp)) + } - ShareItem( - share = share, - type = type, - onSelectShare = { selected -> - viewModel.setActiveShare(selected) - }, - onDeleteShare = { viewModel.deleteShare(it.id) }, - onSendEmail = { } - ) + ShareItem( + share = share, + type = type, + onSelectShare = { selected -> viewModel.setActiveShare(selected) }, + onDeleteShare = { viewModel.deleteShare(it.id) }, + onSendEmail = { } + ) + } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index afc91946..42298b3e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -20,6 +20,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareProper import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -36,8 +37,8 @@ class ShareViewModel( private val repository: ShareRepository ) : ViewModel() { - private val _shares = MutableStateFlow>(emptyList()) - val shares: StateFlow> = _shares + private val _state = MutableStateFlow(ShareScreenState.Loading) + val state: StateFlow = _state private val _activeShare = MutableStateFlow(null) val activeShare: StateFlow = _activeShare @@ -47,14 +48,14 @@ class ShareViewModel( private val _recipientSearchResults = MutableStateFlow>(emptyList()) val recipientSearchResults: StateFlow> = _recipientSearchResults - private val _loading = MutableStateFlow(false) - val loading: StateFlow = _loading - private val _errorMessageId = MutableStateFlow(null) val errorMessageId: StateFlow = _errorMessageId private val pendingProperties = mutableMapOf() + private val currentShares: List + get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() + init { fetchShares() initSearchQuery() @@ -68,9 +69,7 @@ class ShareViewModel( .debounce(300L) .distinctUntilChanged() .filter { it.isNotBlank() } - .collect { query -> - executeSearch(query) - } + .collect { query -> executeSearch(query) } } } @@ -93,21 +92,21 @@ class ShareViewModel( limit: Int = 50 ) { viewModelScope.launch(Dispatchers.IO) { - _loading.update { true } + _state.update { ShareScreenState.Loading } _errorMessageId.update { null } val result = repository.fetchShares(sourceClass, lastShareID, limit) - handleResult(result, R.string.share_view_fetch_error_message)?.let { fetchedShares -> - _shares.update { fetchedShares } + handleResult(result, R.string.share_view_fetch_error_message)?.let { fetched -> + _state.update { + if (fetched.isEmpty()) ShareScreenState.Empty + else ShareScreenState.Loaded(fetched) + } } - - _loading.update { false } } } fun fetchShare(id: String, request: GetShareRequest = GetShareRequest()) { viewModelScope.launch(Dispatchers.IO) { - _loading.update { true } _errorMessageId.update { null } val result = repository.fetchShare(id, request) @@ -115,20 +114,12 @@ class ShareViewModel( _activeShare.update { share } replaceInList(share) } - - _loading.update { false } } } // endregion // region create - /** - * Creates an empty draft [Share] on the server and sets it as the [activeShare]. - * Then sources can be added later. - * - */ suspend fun createDraftShare(): Share? = withContext(Dispatchers.IO) { - _loading.update { true } _errorMessageId.update { null } val result = repository.createDraftShare() @@ -136,13 +127,10 @@ class ShareViewModel( if (draft != null) { _activeShare.update { draft } - _shares.update { current -> listOf(draft) + current } - _loading.update { false } - draft - } else { - _loading.update { false } - null + _state.update { ShareScreenState.Loaded(listOf(draft) + currentShares) } } + + draft } // endregion @@ -247,7 +235,11 @@ class ShareViewModel( viewModelScope.launch(Dispatchers.IO) { val result = repository.deleteShare(id) handleResult(result, R.string.share_view_delete_error_message)?.let { - _shares.update { current -> current.filterNot { it.id == id } } + val remaining = currentShares.filterNot { it.id == id } + _state.update { + if (remaining.isEmpty()) ShareScreenState.Empty + else ShareScreenState.Loaded(remaining) + } if (_activeShare.value?.id == id) _activeShare.update { null } } } @@ -266,13 +258,11 @@ class ShareViewModel( // region private private fun replaceInList(updated: Share) { - _shares.update { current -> current.map { if (it.id == updated.id) updated else it } } + val shares = currentShares.ifEmpty { return } + _state.update { ShareScreenState.Loaded(shares.map { if (it.id == updated.id) updated else it }) } } - private fun handleResult( - result: NetworkResult, - errorId: Int - ): T? { + private fun handleResult(result: NetworkResult, errorId: Int): T? { return when (result) { is NetworkResult.Success -> result.data is NetworkResult.ServerError, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt index 0ca202f1..3a34eff6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt @@ -1,10 +1,11 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT */ + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.layout.Arrangement @@ -26,11 +27,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.property.SharePropertyView @@ -50,18 +53,33 @@ fun AddOrEditShareBottomSheet( ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() val categories = remember { ShareCategory.entries.toList() } var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } var expandedCategories by remember { mutableStateOf(emptySet()) } + var showDiscardDialog by remember { mutableStateOf(false) } + + if (showDiscardDialog) { + DiscardDraftShareDialog( + onKeep = { showDiscardDialog = false }, + onDiscard = { + showDiscardDialog = false + viewModel.deleteShare(share.id) + viewModel.setActiveShare(null) + } + ) + } ModalBottomSheet( onDismissRequest = { - viewModel.commitPendingProperties(share.id) if (share.shareState == ShareState.DRAFT) { - viewModel.deleteShare(share.id) + showDiscardDialog = true + scope.launch { sheetState.expand() } + } else { + viewModel.commitPendingProperties(share.id) + viewModel.setActiveShare(null) } - viewModel.setActiveShare(null) }, sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surface, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt new file mode 100644 index 00000000..b8a5c5a5 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.nextcloud.android.common.ui.R + +@Composable +fun DiscardDraftShareDialog( + onKeep: () -> Unit, + onDiscard: () -> Unit +) { + AlertDialog( + onDismissRequest = onKeep, + title = { Text(stringResource(R.string.share_view_discard_draft_title)) }, + text = { Text(stringResource(R.string.share_view_discard_draft_message)) }, + confirmButton = { + TextButton(onClick = onDiscard) { + Text(stringResource(R.string.share_view_discard_draft_delete)) + } + }, + dismissButton = { + TextButton(onClick = onKeep) { + Text(stringResource(R.string.share_view_discard_draft_keep)) + } + } + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt new file mode 100644 index 00000000..aae0f0dd --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import com.nextcloud.android.common.ui.share.model.api.share.Share + +sealed class ShareScreenState { + data object Empty: ShareScreenState() + data object Loading: ShareScreenState() + data class Loaded(val shares: List): ShareScreenState() +} diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index f9272a52..adbbd303 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -58,6 +58,11 @@ Failed to fetch shares. + Discard new share? + This share hasn\'t been saved yet. Do you want to discard it? + Keep + Discard + Share not found, cannot delete. Failed to delete share. From 9b92629a9a276e4a3702881da52dbc6d61236667 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 10:22:06 +0300 Subject: [PATCH 038/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 14 ++++++- .../DeleteShareConfirmationDialog.kt | 41 +++++++++++++++++++ ui/src/main/res/values/strings.xml | 2 + 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 924b83f4..b79646ee 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -61,6 +61,7 @@ import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.component.AddOrEditShareBottomSheet +import com.nextcloud.android.common.ui.share.component.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.ShareItemType @@ -166,8 +167,19 @@ private fun ShareItem( onSendEmail: (Share) -> Unit ) { var showContextMenu by remember { mutableStateOf(false) } + var showDeleteDialog by remember { mutableStateOf(false) } val haptics = LocalHapticFeedback.current + if (showDeleteDialog) { + DeleteShareConfirmationDialog( + onConfirm = { + showDeleteDialog = false + onDeleteShare(share) + }, + onDismiss = { showDeleteDialog = false } + ) + } + ListItem( modifier = Modifier .fillMaxWidth(0.9f) @@ -229,8 +241,8 @@ private fun ShareItem( ) }, onClick = { - onDeleteShare(share) showContextMenu = false + showDeleteDialog = true } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt new file mode 100644 index 00000000..88a741b3 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.nextcloud.android.common.ui.R + +@Composable +fun DeleteShareConfirmationDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.share_view_delete_confirm_title)) }, + text = { Text(stringResource(R.string.share_view_delete_confirm_message)) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text( + text = stringResource(R.string.common_ok), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.common_cancel)) + } + } + ) +} diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index adbbd303..540a7009 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -55,6 +55,8 @@ Edit Send email Delete + Delete share? + This share will be permanently removed. Failed to fetch shares. From 4970fdd132496febbae27d41507f44df50636eb0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 10:29:27 +0300 Subject: [PATCH 039/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 42 +++++++++++++------ .../component/AddOrEditShareBottomSheet.kt | 21 ++-------- .../DeleteShareConfirmationDialog.kt | 17 +++++--- .../component/DiscardDraftShareDialog.kt | 13 +++--- .../share/model/ui/ShareItemOverlayState.kt | 14 +++++++ 5 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index b79646ee..8b535433 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -62,8 +62,10 @@ import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.component.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.DeleteShareConfirmationDialog +import com.nextcloud.android.common.ui.share.component.DiscardDraftShareDialog import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository @@ -78,6 +80,18 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti val context = LocalContext.current val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + var showDiscardDraftDialog by remember { mutableStateOf(false) } + + if (showDiscardDraftDialog) { + DiscardDraftShareDialog( + onKeep = { showDiscardDraftDialog = false }, + onDiscard = { + showDiscardDraftDialog = false + activeShare?.let { viewModel.deleteShare(it.id) } + viewModel.setActiveShare(null) + } + ) + } LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -153,7 +167,8 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti AddOrEditShareBottomSheet( share = it, sharingCapabilities = sharingCapabilities, - viewModel = viewModel + viewModel = viewModel, + onDismissDraft = { showDiscardDraftDialog = true } ) } } @@ -166,17 +181,16 @@ private fun ShareItem( onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit ) { - var showContextMenu by remember { mutableStateOf(false) } - var showDeleteDialog by remember { mutableStateOf(false) } + var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) } val haptics = LocalHapticFeedback.current - if (showDeleteDialog) { + if (overlayState == ShareItemOverlayState.DeleteConfirmation) { DeleteShareConfirmationDialog( onConfirm = { - showDeleteDialog = false + overlayState = ShareItemOverlayState.None onDeleteShare(share) }, - onDismiss = { showDeleteDialog = false } + onDismiss = { overlayState = ShareItemOverlayState.None } ) } @@ -188,7 +202,7 @@ private fun ShareItem( onClick = { onSelectShare(share) }, onLongClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) - showContextMenu = true + overlayState = ShareItemOverlayState.ContextMenu }, ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), @@ -213,15 +227,18 @@ private fun ShareItem( }, trailingContent = { Box { - IconButton(onClick = { showContextMenu = true }) { + IconButton(onClick = { overlayState = ShareItemOverlayState.ContextMenu }) { Icon(Icons.Default.MoreVert, contentDescription = "More options") } - DropdownMenu(expanded = showContextMenu, onDismissRequest = { showContextMenu = false }) { + DropdownMenu( + expanded = overlayState == ShareItemOverlayState.ContextMenu, + onDismissRequest = { overlayState = ShareItemOverlayState.None } + ) { DropdownMenuItem( text = { Text(stringResource(R.string.share_view_list_item_edit)) }, onClick = { - showContextMenu = false + overlayState = ShareItemOverlayState.None onSelectShare(share) } ) @@ -229,7 +246,7 @@ private fun ShareItem( text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, onClick = { onSendEmail(share) - showContextMenu = false + overlayState = ShareItemOverlayState.None } ) HorizontalDivider() @@ -241,8 +258,7 @@ private fun ShareItem( ) }, onClick = { - showContextMenu = false - showDeleteDialog = true + overlayState = ShareItemOverlayState.DeleteConfirmation } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt index 3a34eff6..4ee145aa 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt @@ -27,13 +27,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.property.SharePropertyView @@ -49,33 +47,20 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareCategory fun AddOrEditShareBottomSheet( share: Share, sharingCapabilities: SharingCapabilities, - viewModel: ShareViewModel + viewModel: ShareViewModel, + onDismissDraft: () -> Unit = {} ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() - val scope = rememberCoroutineScope() val categories = remember { ShareCategory.entries.toList() } var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } var expandedCategories by remember { mutableStateOf(emptySet()) } - var showDiscardDialog by remember { mutableStateOf(false) } - - if (showDiscardDialog) { - DiscardDraftShareDialog( - onKeep = { showDiscardDialog = false }, - onDiscard = { - showDiscardDialog = false - viewModel.deleteShare(share.id) - viewModel.setActiveShare(null) - } - ) - } ModalBottomSheet( onDismissRequest = { if (share.shareState == ShareState.DRAFT) { - showDiscardDialog = true - scope.launch { sheetState.expand() } + onDismissDraft() } else { viewModel.commitPendingProperties(share.id) viewModel.setActiveShare(null) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt index 88a741b3..fb10108e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt @@ -2,12 +2,14 @@ * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.nextcloud.android.common.ui.share.component import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -25,11 +27,14 @@ fun DeleteShareConfirmationDialog( title = { Text(stringResource(R.string.share_view_delete_confirm_title)) }, text = { Text(stringResource(R.string.share_view_delete_confirm_message)) }, confirmButton = { - TextButton(onClick = onConfirm) { - Text( - text = stringResource(R.string.common_ok), - color = MaterialTheme.colorScheme.error + FilledTonalButton( + onClick = onConfirm, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer ) + ) { + Text(stringResource(R.string.common_ok)) } }, dismissButton = { @@ -38,4 +43,4 @@ fun DeleteShareConfirmationDialog( } } ) -} +} \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt index b8a5c5a5..3ae7faf7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt @@ -2,12 +2,13 @@ * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.nextcloud.android.common.ui.share.component import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -24,14 +25,14 @@ fun DiscardDraftShareDialog( title = { Text(stringResource(R.string.share_view_discard_draft_title)) }, text = { Text(stringResource(R.string.share_view_discard_draft_message)) }, confirmButton = { - TextButton(onClick = onDiscard) { - Text(stringResource(R.string.share_view_discard_draft_delete)) + FilledTonalButton(onClick = onKeep) { + Text(stringResource(R.string.share_view_discard_draft_keep)) } }, dismissButton = { - TextButton(onClick = onKeep) { - Text(stringResource(R.string.share_view_discard_draft_keep)) + TextButton(onClick = onDiscard) { + Text(stringResource(R.string.share_view_discard_draft_delete)) } } ) -} +} \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt new file mode 100644 index 00000000..0e50bb29 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.model.ui + +sealed class ShareItemOverlayState { + data object None : ShareItemOverlayState() + data object ContextMenu : ShareItemOverlayState() + data object DeleteConfirmation : ShareItemOverlayState() +} From 214848782aea487702a050500603997001cc546e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 1 Jun 2026 10:30:12 +0300 Subject: [PATCH 040/174] wip Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareScreen.kt | 6 +++--- .../{ => bottomsheet}/AddOrEditShareBottomSheet.kt | 7 +++++-- .../{ => dialog}/DeleteShareConfirmationDialog.kt | 2 +- .../component/{ => dialog}/DiscardDraftShareDialog.kt | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{ => bottomsheet}/AddOrEditShareBottomSheet.kt (95%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{ => dialog}/DeleteShareConfirmationDialog.kt (95%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{ => dialog}/DiscardDraftShareDialog.kt (94%) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 8b535433..2fd80eef 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -60,9 +60,9 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient -import com.nextcloud.android.common.ui.share.component.AddOrEditShareBottomSheet -import com.nextcloud.android.common.ui.share.component.DeleteShareConfirmationDialog -import com.nextcloud.android.common.ui.share.component.DiscardDraftShareDialog +import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet +import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog +import com.nextcloud.android.common.ui.share.component.dialog.DiscardDraftShareDialog import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt similarity index 95% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 4ee145aa..9897bdba 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -2,11 +2,11 @@ * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.android.common.ui.share.component +package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,6 +34,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection +import com.nextcloud.android.common.ui.share.component.RecipientSearchField +import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.property.clazz diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt similarity index 95% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt index fb10108e..cf16a865 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareConfirmationDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.android.common.ui.share.component +package com.nextcloud.android.common.ui.share.component.dialog import androidx.compose.material3.AlertDialog import androidx.compose.material3.ButtonDefaults diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt similarity index 94% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt index 3ae7faf7..85031c3c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DiscardDraftShareDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.nextcloud.android.common.ui.share.component +package com.nextcloud.android.common.ui.share.component.dialog import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilledTonalButton From 007f5c79a04c430f266b15e235405b30f1281263 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 11:31:18 +0200 Subject: [PATCH 041/174] wip Signed-off-by: alperozturk96 --- .../ui/share/model/api/capabilities/Legacy.kt | 19 ++++++++++++++++ .../capabilities/PermissionCategoryType.kt | 22 +++++++++++++++++++ .../model/api/capabilities/SourceType.kt | 17 ++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt new file mode 100644 index 00000000..cef0c5b9 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.capabilities + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Legacy( + @SerialName("max_sources") + val maxSources: Long, + @SerialName("max_recipients") + val maxRecipients: Long, +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt new file mode 100644 index 00000000..2a797fe8 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.capabilities + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PermissionCategoryType( + @SerialName("class") + val classField: String, + @SerialName("display_name") + val displayName: String, + val hint: String?, + val icon: String?, + val priority: Long, +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt new file mode 100644 index 00000000..dbdfe0a6 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.capabilities + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SourceType( + @SerialName("class") + val classField: String, +) From e16cf632fe6ff01359b60373dcff8d21a7d31f8d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 11:31:37 +0200 Subject: [PATCH 042/174] wip Signed-off-by: alperozturk96 --- gradle/verification-metadata.xml | 9 +++++ .../android/common/ui/share/ShareScreen.kt | 21 +++------- .../android/common/ui/share/ShareViewModel.kt | 5 ++- .../bottomsheet/AddOrEditShareBottomSheet.kt | 10 ++--- .../dialog/DiscardDraftShareDialog.kt | 38 ------------------- .../api/capabilities/SharingCapabilities.kt | 25 ++---------- .../ui/share/model/ui/ShareScreenState.kt | 3 ++ 7 files changed, 30 insertions(+), 81 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7208210f..ee463678 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -75,6 +75,7 @@ + @@ -103,6 +104,11 @@ + + + + + @@ -175,6 +181,7 @@ + @@ -282,6 +289,7 @@ + @@ -297,6 +305,7 @@ + diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 2fd80eef..912d8800 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -62,12 +62,12 @@ import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog -import com.nextcloud.android.common.ui.share.component.dialog.DiscardDraftShareDialog import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState +import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -80,18 +80,6 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti val context = LocalContext.current val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - var showDiscardDraftDialog by remember { mutableStateOf(false) } - - if (showDiscardDraftDialog) { - DiscardDraftShareDialog( - onKeep = { showDiscardDraftDialog = false }, - onDiscard = { - showDiscardDraftDialog = false - activeShare?.let { viewModel.deleteShare(it.id) } - viewModel.setActiveShare(null) - } - ) - } LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -141,7 +129,7 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - itemsIndexed(state.shares, key = { _, share -> share.id }) { index, share -> + itemsIndexed(state.shares.filtered(), key = { _, share -> share.id }) { index, share -> val type = ShareItemType.type(index, state.shares.lastIndex) if (index == 0) { @@ -168,7 +156,10 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti share = it, sharingCapabilities = sharingCapabilities, viewModel = viewModel, - onDismissDraft = { showDiscardDraftDialog = true } + onDismissDraft = { draftShare -> + activeShare?.let { viewModel.deleteShare(draftShare.id) } + viewModel.setActiveShare(null) + } ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 42298b3e..7120f697 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -21,6 +21,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateR import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState +import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -98,7 +99,7 @@ class ShareViewModel( val result = repository.fetchShares(sourceClass, lastShareID, limit) handleResult(result, R.string.share_view_fetch_error_message)?.let { fetched -> _state.update { - if (fetched.isEmpty()) ShareScreenState.Empty + if (fetched.filtered().isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(fetched) } } @@ -237,7 +238,7 @@ class ShareViewModel( handleResult(result, R.string.share_view_delete_error_message)?.let { val remaining = currentShares.filterNot { it.id == id } _state.update { - if (remaining.isEmpty()) ShareScreenState.Empty + if (remaining.filtered().isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(remaining) } if (_activeShare.value?.id == id) _activeShare.update { null } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 9897bdba..89044d8f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -51,7 +51,7 @@ fun AddOrEditShareBottomSheet( share: Share, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel, - onDismissDraft: () -> Unit = {} + onDismissDraft: (Share) -> Unit = {} ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scrollState = rememberScrollState() @@ -63,7 +63,7 @@ fun AddOrEditShareBottomSheet( ModalBottomSheet( onDismissRequest = { if (share.shareState == ShareState.DRAFT) { - onDismissDraft() + onDismissDraft(share) } else { viewModel.commitPendingProperties(share.id) viewModel.setActiveShare(null) @@ -156,17 +156,17 @@ private fun PermissionCategories( onToggleCategory: (String) -> Unit, viewModel: ShareViewModel ) { - sharingCapabilities.permissionCategories + sharingCapabilities.permissionCategoryTypes .sortedBy { it.priority } .forEach { sharingCapability -> - key(sharingCapability.class_field) { + key(sharingCapability.classField) { CollapsibleShareSection( label = sharingCapability.displayName, isExpanded = sharingCapability.displayName in expandedCategories, onToggle = { onToggleCategory(sharingCapability.displayName) }, ) { share.permissions - .filter { permission -> permission.category == sharingCapability.class_field } + .filter { permission -> permission.category == sharingCapability.classField } .sortedBy { it.displayName } .forEach { permission -> key(permission.clazz) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt deleted file mode 100644 index 85031c3c..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardDraftShareDialog.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.android.common.ui.share.component.dialog - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.nextcloud.android.common.ui.R - -@Composable -fun DiscardDraftShareDialog( - onKeep: () -> Unit, - onDiscard: () -> Unit -) { - AlertDialog( - onDismissRequest = onKeep, - title = { Text(stringResource(R.string.share_view_discard_draft_title)) }, - text = { Text(stringResource(R.string.share_view_discard_draft_message)) }, - confirmButton = { - FilledTonalButton(onClick = onKeep) { - Text(stringResource(R.string.share_view_discard_draft_keep)) - } - }, - dismissButton = { - TextButton(onClick = onDiscard) { - Text(stringResource(R.string.share_view_discard_draft_delete)) - } - } - ) -} \ No newline at end of file diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 545f925c..304d3592 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -15,25 +15,8 @@ data class SharingCapabilities( @SerialName("api_versions") val apiVersions: List, val legacy: Legacy, - @SerialName("permission_categories") - val permissionCategories: List, -) - -@Serializable -data class Legacy( - @SerialName("max_sources") - val maxSources: Long, - @SerialName("max_recipients") - val maxRecipients: Long, -) - -@Serializable -data class PermissionCategory( - @SerialName("class") - val class_field: String, - @SerialName("display_name") - val displayName: String, - val hint: String?, - val icon: String?, - val priority: Long, + @SerialName("source_types") + val sourceTypes: List, + @SerialName("permission_category_types") + val permissionCategoryTypes: List, ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index aae0f0dd..1b912e17 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -8,9 +8,12 @@ package com.nextcloud.android.common.ui.share.model.ui import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.state.ShareState sealed class ShareScreenState { data object Empty: ShareScreenState() data object Loading: ShareScreenState() data class Loaded(val shares: List): ShareScreenState() } + +fun List.filtered(): List = filter { it.shareState != ShareState.DRAFT } From 64cbc6b8f5d60de141033e22ffa31aa99a8c7fc5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 12:03:53 +0200 Subject: [PATCH 043/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 89044d8f..15c0dfc0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -148,6 +150,21 @@ private fun CategorySelector( } } +// TODO ADD send button +// TODO disable multi receipients +// TODO implement Anyone --> /ocs/v2.php/apps/sharing/api/v1/share/{id}/recipient +// --> Add a recipient to a share use correspongind class value --> doesnt matter use uuid string : "value": "string", "instance": "string" + +// TODO discuss with laura for note to recepipent expiraitaion date proeprties etc should be collabls + +// TODO send ISO 8601 as string to backend for expiration date + +// TODO for COPY link use Recepient.Secret.URL side note: if value is updatable user can edit the TOKEN directly which is value + + + +// TODO: Backend will change and provide us bundled permissions or list of custom permissions. +// USE THIS FOR BOTH OPTION @Composable private fun PermissionCategories( share: Share, @@ -156,6 +173,27 @@ private fun PermissionCategories( onToggleCategory: (String) -> Unit, viewModel: ShareViewModel ) { + var expanded by remember { mutableStateOf(false) } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + share.permissions.forEach { + key(it.displayName) { + DropdownMenuItem( + text = { Text(it.displayName) }, + onClick = { /* Do something... */ } + ) + } + } + + DropdownMenuItem( + text = { Text("Option 2") }, + onClick = { /* Do something... */ } + ) + } + sharingCapabilities.permissionCategoryTypes .sortedBy { it.priority } .forEach { sharingCapability -> From ebc121dfcb4fb02e8e98b89245ce5cd293ca79b9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 12:14:28 +0200 Subject: [PATCH 044/174] wip Signed-off-by: alperozturk96 --- .../share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 15c0dfc0..946a1e73 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -161,7 +161,11 @@ private fun CategorySelector( // TODO for COPY link use Recepient.Secret.URL side note: if value is updatable user can edit the TOKEN directly which is value +// TODO display error messages and handle them via debounce mechanism +// TODO fetch shares currently returns all shares SOURCE ID will be implemented so that u can only show related shares + +// TODO show share icon from first recipient but check with laura // TODO: Backend will change and provide us bundled permissions or list of custom permissions. // USE THIS FOR BOTH OPTION From cdd4513350aa74188945b442da53b6531916d2b4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 15:56:33 +0200 Subject: [PATCH 045/174] wip Signed-off-by: alperozturk96 --- .../share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 946a1e73..bd08b55b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -177,7 +177,8 @@ private fun PermissionCategories( onToggleCategory: (String) -> Unit, viewModel: ShareViewModel ) { - var expanded by remember { mutableStateOf(false) } + /* + var expanded by remember { mutableStateOf(false) } DropdownMenu( expanded = expanded, @@ -197,6 +198,8 @@ private fun PermissionCategories( onClick = { /* Do something... */ } ) } + */ + sharingCapabilities.permissionCategoryTypes .sortedBy { it.priority } From c8c1bf08d997ef2dff1b5360b9286b53af9b223f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 12:31:06 +0200 Subject: [PATCH 046/174] disable multi recipients Signed-off-by: alperozturk96 --- .../android/common/ui/share/component/RecipientSearchField.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt index 9dfd0a6f..41ed063d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt @@ -78,6 +78,7 @@ fun RecipientSearchField( } RecipientSearchDropdown( + enabled = share.recipients.isEmpty(), query = query, expanded = expanded, onQueryChange = { @@ -145,6 +146,7 @@ private fun RecipientChipRow( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RecipientSearchDropdown( + enabled: Boolean, query: String, expanded: Boolean, onQueryChange: (String) -> Unit, @@ -160,6 +162,7 @@ private fun RecipientSearchDropdown( value = query, onValueChange = onQueryChange, label = { Text(stringResource(R.string.share_view_invited_category_label)) }, + enabled = enabled, modifier = Modifier .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) .fillMaxWidth(), From 72a39f40107ba836265786b0383af2e368e729bb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 12:31:12 +0200 Subject: [PATCH 047/174] disable multi recipients Signed-off-by: alperozturk96 --- .../ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index bd08b55b..916d03a5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -151,7 +151,6 @@ private fun CategorySelector( } // TODO ADD send button -// TODO disable multi receipients // TODO implement Anyone --> /ocs/v2.php/apps/sharing/api/v1/share/{id}/recipient // --> Add a recipient to a share use correspongind class value --> doesnt matter use uuid string : "value": "string", "instance": "string" From 934e92d463e09a3e9ab7297d54d1f7a72dad2cd1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 14:07:09 +0200 Subject: [PATCH 048/174] add secret and buttons Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 82 ++++++++++++++++++- .../share/model/api/recipients/Recipient.kt | 5 +- .../ui/share/model/api/secret/Secret.kt | 17 ++++ .../common/ui/share/model/api/share/Share.kt | 18 ++++ .../share/repository/MockShareRepository.kt | 28 +++++-- 5 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 916d03a5..05d8b936 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -10,13 +10,19 @@ package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SegmentedButton @@ -29,9 +35,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R @@ -46,6 +55,7 @@ import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -116,6 +126,74 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) } + + if (share.readyToSend()) { + val localClipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + + Buttons(onCopyLink = { + scope.launch { + share.getClipEntry()?.let { clipEntry -> + localClipboard.setClipEntry(clipEntry) + } + } + }, onSend = { + viewModel.updateState(share.id, ShareState.ACTIVE) + }) + } + } + } +} + +@Composable +private fun Buttons( + onCopyLink: () -> Unit = {}, + onSend: () -> Unit = {}, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Button( + onClick = onCopyLink, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Icon( + painter = painterResource(R.drawable.ic_link), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.share_view_copy_action), + modifier = Modifier.padding(start = 8.dp), + ) + } + + Button( + onClick = onSend, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.share_view_send_action), + modifier = Modifier.padding(start = 8.dp), + ) } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index 36d876b1..5f900004 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -8,6 +8,7 @@ package com.nextcloud.android.common.ui.share.model.api.recipients import com.nextcloud.android.common.ui.share.model.api.icon.Icon +import com.nextcloud.android.common.ui.share.model.api.secret.Secret import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -23,5 +24,7 @@ data class Recipient( @SerialName("display_name") val displayName: String, - val icon: Icon? = null + val icon: Icon? = null, + + val secret: Secret ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt new file mode 100644 index 00000000..a924e83d --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.secret + +import kotlinx.serialization.Serializable + +@Serializable +data class Secret( + val updatable: Boolean, + val value: String?, + val url: String? +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 3d2032e4..51bf0067 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -7,7 +7,10 @@ package com.nextcloud.android.common.ui.share.model.api.share +import android.content.ClipData import android.content.Context +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.toClipEntry import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.owner.Owner import com.nextcloud.android.common.ui.share.model.api.permission.Permission @@ -38,6 +41,21 @@ data class Share( val permissions: List ) { + fun getClipEntry(): ClipEntry? { + val label = recipients.first().displayName + val link = recipients.first().secret.url + + return if (link != null) { + ClipData.newPlainText(label, link).toClipEntry() + } else { + null + } + } + + fun readyToSend(): Boolean { + return sources.isNotEmpty() && recipients.isNotEmpty() && permissions.isNotEmpty() + } + fun title(context: Context): String { return if (shareState == ShareState.DRAFT) { context.getString(R.string.share_view_bottom_sheet_new_title) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index f6a63048..4c3fe4be 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -22,6 +22,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest +import com.nextcloud.android.common.ui.share.model.api.secret.Secret import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState @@ -123,7 +124,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/user_light.png", dark = "https://mock/icons/user_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ) ) ), @@ -140,7 +142,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/group_light.png", dark = "https://mock/icons/group_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ) ), owner = Owner( @@ -181,7 +184,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/external_light.png", dark = "https://mock/icons/external_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ) ), owner = Owner( @@ -207,7 +211,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/federated_light.png", dark = "https://mock/icons/federated_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ) ), owner = Owner( @@ -235,7 +240,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/user_light.png", dark = "https://mock/icons/user_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ), Recipient( clazz = "group", @@ -244,7 +250,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/group_light.png", dark = "https://mock/icons/group_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ), Recipient( clazz = "mail", @@ -253,7 +260,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/external_light.png", dark = "https://mock/icons/external_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ), Recipient( clazz = "federated", @@ -263,7 +271,8 @@ class MockShareRepository : ShareRepository { icon = Icon( light = "https://mock/icons/federated_light.png", dark = "https://mock/icons/federated_dark.png" - ) + ), + secret = Secret(false, value = "", url = "") ) ) @@ -373,7 +382,8 @@ class MockShareRepository : ShareRepository { clazz = request.clazz, value = request.value, instance = request.instance, - displayName = request.value + displayName = request.value, + secret = Secret(false, value = "", url = "") ) val updated = current.copy( recipients = current.recipients + newRecipient, From ea4ae0bed6a2e65502a5ad2f7f6692534cdb256b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 14:12:46 +0200 Subject: [PATCH 049/174] add secret and buttons Signed-off-by: alperozturk96 --- .../android/common/ui/share/model/api/secret/Secret.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt index a924e83d..b951e6d0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt @@ -12,6 +12,6 @@ import kotlinx.serialization.Serializable @Serializable data class Secret( val updatable: Boolean, - val value: String?, - val url: String? + val value: String? = null, + val url: String? = null ) From f504b6c1fb9243fdf8fafdb877597a52ad6c2e84 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 14:14:25 +0200 Subject: [PATCH 050/174] fix enabled Signed-off-by: alperozturk96 --- .../nextcloud/android/common/ui/share/model/api/share/Share.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 51bf0067..1d8b473e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -53,7 +53,7 @@ data class Share( } fun readyToSend(): Boolean { - return sources.isNotEmpty() && recipients.isNotEmpty() && permissions.isNotEmpty() + return sources.isNotEmpty() && recipients.isNotEmpty() && permissions.any { it.enabled } } fun title(context: Context): String { From b323e0edbd4002c3c68f4c30a5f62168fc0b7052 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 14:38:44 +0200 Subject: [PATCH 051/174] use ISO8601 Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 3 --- .../property/datepicker/ShareDatePicker.kt | 22 ++++++++------- .../datepicker/util/ShareDateFormatter.kt | 27 ++++++++++--------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 05d8b936..e9df46a1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -228,14 +228,11 @@ private fun CategorySelector( } } -// TODO ADD send button // TODO implement Anyone --> /ocs/v2.php/apps/sharing/api/v1/share/{id}/recipient // --> Add a recipient to a share use correspongind class value --> doesnt matter use uuid string : "value": "string", "instance": "string" // TODO discuss with laura for note to recepipent expiraitaion date proeprties etc should be collabls -// TODO send ISO 8601 as string to backend for expiration date - // TODO for COPY link use Recepient.Secret.URL side note: if value is updatable user can edit the TOKEN directly which is value // TODO display error messages and handle them via debounce mechanism diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index a99fae18..186b9238 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -37,7 +37,6 @@ fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { var showDatePicker by remember { mutableStateOf(false) } var dateValue by remember { mutableStateOf(property.value ?: "") } - Box(modifier = Modifier.fillMaxWidth()) { OutlinedTextField( value = dateValue, @@ -62,19 +61,21 @@ fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { } if (showDatePicker) { - DatePickerModal(formatter, onDateSelected = { - dateValue = it ?: "" - onDateSelected(dateValue) - }, onDismiss = { - showDatePicker = false - }) + DatePickerModal( + formatter = formatter, + onDateSelected = { displayDate, isoDate -> + dateValue = displayDate ?: "" + onDateSelected(isoDate ?: "") + }, + onDismiss = { showDatePicker = false } + ) } } @Composable private fun DatePickerModal( formatter: ShareDateFormatter, - onDateSelected: (String?) -> Unit, + onDateSelected: (displayDate: String?, isoDate: String?) -> Unit, onDismiss: () -> Unit ) { val datePickerState = rememberDatePickerState() @@ -83,7 +84,10 @@ private fun DatePickerModal( onDismissRequest = onDismiss, confirmButton = { TextButton(onClick = { - onDateSelected(formatter.formatDate(datePickerState)) + onDateSelected( + formatter.formatDisplayDate(datePickerState), + formatter.formatIso8601Date(datePickerState) + ) onDismiss() }) { Text(stringResource(R.string.common_ok)) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt index 7caca750..b63e69f9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt @@ -11,21 +11,24 @@ import androidx.compose.material3.DatePickerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.getSelectedDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale class ShareDateFormatter { - companion object { - private const val PATTERN = "MM-dd-yyyy" - } - fun getDisplayName(property: PropertyDate): String { - return property.displayName + " ($PATTERN)" - } + fun getDisplayName(property: PropertyDate): String = property.displayName @OptIn(ExperimentalMaterial3Api::class) - fun formatDate(datePickerState: DatePickerState): String? { - val date = datePickerState.getSelectedDate() - val formatter = DateTimeFormatter.ofPattern(PATTERN) - return date?.format(formatter) - } -} \ No newline at end of file + fun formatDisplayDate(datePickerState: DatePickerState): String? = + datePickerState.getSelectedDate()?.format( + DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.getDefault()) + ) + + @OptIn(ExperimentalMaterial3Api::class) + fun formatIso8601Date(datePickerState: DatePickerState): String? = + datePickerState.getSelectedDate() + ?.atStartOfDay(ZoneId.systemDefault()) + ?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) +} From 0a8d0e1841f8ef56bc5154362d5004bff6f1c62e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 15:09:25 +0200 Subject: [PATCH 052/174] adopt latest open api Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 10 +++++++--- .../ui/share/component/RecipientSearchField.kt | 2 +- .../api/capabilities/PermissionCategoryType.kt | 5 +++-- .../api/capabilities/SharingCapabilities.kt | 2 +- .../ui/share/model/api/permission/Permission.kt | 7 ++++++- .../ui/share/model/api/request/ShareRequests.kt | 13 +++++++++++++ .../common/ui/share/model/api/source/Source.kt | 5 ++++- .../ui/share/repository/MockShareRepository.kt | 14 ++++++++++++-- .../share/repository/ShareRemoteRepository.kt | 17 +++++++++++++++-- .../ui/share/repository/ShareRepository.kt | 8 +++++++- 10 files changed, 69 insertions(+), 14 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 912d8800..d49523c0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -48,8 +48,8 @@ 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.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -60,6 +60,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.component.RecipientIcon import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities @@ -77,13 +78,13 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti val errorMessageId by viewModel.errorMessageId.collectAsState() val screenState by viewModel.state.collectAsState() val activeShare by viewModel.activeShare.collectAsState() - val context = LocalContext.current val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + val resources = LocalResources.current LaunchedEffect(errorMessageId) { errorMessageId?.let { - snackbarHostState.showSnackbar(context.getString(it)) + snackbarHostState.showSnackbar(resources.getString(it)) viewModel.updateErrorMessage(null) } } @@ -209,6 +210,9 @@ private fun ShareItem( style = MaterialTheme.typography.titleSmall ) }, + leadingContent = { + share.recipients.first().icon?.let { RecipientIcon(icon = it) } + }, supportingContent = { Text( text = share.shareState.name, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt index 41ed063d..ade91d16 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt @@ -216,7 +216,7 @@ private fun RecipientDropdownContent( } @Composable -private fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { +fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { val isDark = isSystemInDarkTheme() val url = if (isDark) icon.dark ?: icon.light else icon.light ?: icon.dark diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt index 2a797fe8..17fd74cc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share.model.api.capabilities +import com.nextcloud.android.common.ui.share.model.api.icon.Icon import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,7 +17,7 @@ data class PermissionCategoryType( val classField: String, @SerialName("display_name") val displayName: String, - val hint: String?, - val icon: String?, + val hint: String? = null, + val icon: Icon? = null, val priority: Long, ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 304d3592..13452ea9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -14,7 +14,7 @@ import kotlinx.serialization.Serializable data class SharingCapabilities( @SerialName("api_versions") val apiVersions: List, - val legacy: Legacy, + val legacy: Legacy? = null, @SerialName("source_types") val sourceTypes: List, @SerialName("permission_category_types") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt index eae0441a..1e8c62f9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -15,10 +15,15 @@ data class Permission( @SerialName("class") val clazz: String, + @SerialName("source_class") + val sourceClass: String? = null, + @SerialName("display_name") val displayName: String, - val category: String?, + val hint: String? = null, + + val category: String? = null, val enabled: Boolean ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt index b0502ec6..8565a604 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt @@ -20,6 +20,7 @@ data class GetShareRequest( @Serializable data class UpdateShareStateRequest( + @SerialName("state") val shareState: ShareState ) @@ -56,3 +57,15 @@ data class UpdateSharePermissionRequest( val enabled: Boolean ) + +@Serializable +data class UpdateShareRecipientSecretRequest( + @SerialName("class") + val clazz: String, + + val value: String, + + val instance: String? = null, + + val secret: String +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt index b5dfa16f..0635a4bd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share.model.api.source +import com.nextcloud.android.common.ui.share.model.api.icon.Icon import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -18,5 +19,7 @@ data class Source( val value: String, @SerialName("display_name") - val displayName: String + val displayName: String, + + val icon: Icon? = null ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 4c3fe4be..2edf350e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -21,6 +21,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.secret.Secret import com.nextcloud.android.common.ui.share.model.api.share.Share @@ -227,7 +228,7 @@ class MockShareRepository : ShareRepository { ) override suspend fun fetchRecipients( - recipientTypeClass: String?, + recipientTypeClasses: List?, query: String, limit: Int, offset: Int @@ -277,7 +278,7 @@ class MockShareRepository : ShareRepository { ) val filtered = all - .filter { recipientTypeClass == null || it.clazz == recipientTypeClass } + .filter { recipientTypeClasses.isNullOrEmpty() || it.clazz in recipientTypeClasses } .filter { it.displayName.contains(query, ignoreCase = true) || it.value.contains(query, ignoreCase = true) } .drop(offset) .take(limit) @@ -422,6 +423,15 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(updated) } + override suspend fun updateShareRecipientSecret( + id: String, + request: UpdateShareRecipientSecretRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + return NetworkResult.Success(current) + } + override suspend fun updateSharePermission( id: String, request: UpdateSharePermissionRequest diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 20ceac7c..6498564e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -19,6 +19,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share import okhttp3.RequestBody.Companion.toRequestBody @@ -35,14 +36,14 @@ class ShareRemoteRepository( } override suspend fun fetchRecipients( - recipientTypeClass: String?, + recipientTypeClasses: List?, query: String, limit: Int, offset: Int ): NetworkResult> { val queryParams = buildString { append("?query=$query&limit=$limit&offset=$offset") - recipientTypeClass?.let { append("&recipientTypeClass=$it") } + recipientTypeClasses?.forEach { append("&recipientTypeClasses[]=$it") } } return client.executeRequest( endpoint = "$RECIPIENTS_ENDPOINT$queryParams", @@ -186,4 +187,16 @@ class ShareRemoteRepository( ) { body -> json.decodeFromString>(body).ocs.data } + + override suspend fun updateShareRecipientSecret( + id: String, + request: UpdateShareRecipientSecretRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/recipient/secret", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index d10524b8..8ce580f3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -13,13 +13,14 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share interface ShareRepository { suspend fun fetchRecipients( - recipientTypeClass: String?, + recipientTypeClasses: List?, query: String, limit: Int, offset: Int @@ -77,4 +78,9 @@ interface ShareRepository { id: String, request: UpdateSharePermissionRequest ): NetworkResult + + suspend fun updateShareRecipientSecret( + id: String, + request: UpdateShareRecipientSecretRequest + ): NetworkResult } From 83532ae69a0f0734bb4cd8542163e16a2ef18e05 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 15:34:28 +0200 Subject: [PATCH 053/174] property updates Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 143 +++++++++++------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 1 - .../component/property/SharePropertyView.kt | 88 ++++++----- 3 files changed, 137 insertions(+), 95 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 7120f697..31376752 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -25,6 +25,11 @@ import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce @@ -52,7 +57,12 @@ class ShareViewModel( private val _errorMessageId = MutableStateFlow(null) val errorMessageId: StateFlow = _errorMessageId - private val pendingProperties = mutableMapOf() + private data class PendingPropertyUpdate(val job: Job, val shareId: String, val value: String) + + private val pendingPropertyJobs = mutableMapOf() + + private val _propertyErrors = MutableStateFlow>(emptyMap()) + val propertyErrors: StateFlow> = _propertyErrors private val currentShares: List get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() @@ -97,11 +107,10 @@ class ShareViewModel( _errorMessageId.update { null } val result = repository.fetchShares(sourceClass, lastShareID, limit) - handleResult(result, R.string.share_view_fetch_error_message)?.let { fetched -> - _state.update { - if (fetched.filtered().isEmpty()) ShareScreenState.Empty - else ShareScreenState.Loaded(fetched) - } + val fetched = handleResult(result, R.string.share_view_fetch_error_message) ?: return@launch + _state.update { + if (fetched.filtered().isEmpty()) ShareScreenState.Empty + else ShareScreenState.Loaded(fetched) } } } @@ -111,10 +120,10 @@ class ShareViewModel( _errorMessageId.update { null } val result = repository.fetchShare(id, request) - handleResult(result, R.string.share_view_fetch_error_message)?.let { share -> - _activeShare.update { share } - replaceInList(share) - } + val share = handleResult(result, R.string.share_view_fetch_error_message) ?: return@launch + + _activeShare.update { share } + replaceInList(share) } } // endregion @@ -137,12 +146,18 @@ class ShareViewModel( // region state fun updateState(id: String, shareState: ShareState) { - viewModelScope.launch(Dispatchers.IO) { - val result = repository.updateShareState(id, UpdateShareStateRequest(shareState)) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> + viewModelScope.launch { + flushPendingProperties(id) + val result = withContext(Dispatchers.IO) { + repository.updateShareState(id, UpdateShareStateRequest(shareState)) + } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + if (shareState == ShareState.ACTIVE) { + _activeShare.update { null } + } else { _activeShare.update { updated } - replaceInList(updated) } + replaceInList(updated) } } // endregion @@ -152,20 +167,18 @@ class ShareViewModel( viewModelScope.launch(Dispatchers.IO) { val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> - _activeShare.update { updated } - replaceInList(updated) - } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + _activeShare.update { updated } + replaceInList(updated) } } fun removeSource(id: String, clazz: String, value: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareSource(id, clazz, value) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> - _activeShare.update { updated } - replaceInList(updated) - } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + _activeShare.update { updated } + replaceInList(updated) } } // endregion @@ -174,20 +187,18 @@ class ShareViewModel( fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> - _activeShare.update { updated } - replaceInList(updated) - } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + _activeShare.update { updated } + replaceInList(updated) } } fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareRecipient(id, clazz, value, instance) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> - _activeShare.update { updated } - replaceInList(updated) - } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + _activeShare.update { updated } + replaceInList(updated) } } // endregion @@ -196,25 +207,47 @@ class ShareViewModel( fun updateProperty(id: String, clazz: String, value: String?) { viewModelScope.launch(Dispatchers.IO) { val result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value)) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> - _activeShare.update { updated } - replaceInList(updated) - } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + _activeShare.update { updated } + replaceInList(updated) } } - fun updatePropertyLocally(clazz: String, value: String) { - pendingProperties[clazz] = value + fun updatePropertyDebounced(shareId: String, clazz: String, value: String) { + pendingPropertyJobs[clazz]?.job?.cancel() + val job = viewModelScope.launch { + delay(PROPERTY_DEBOUNCE_DELAY_MS) + pendingPropertyJobs.remove(clazz) + withContext(Dispatchers.IO) { executePropertyUpdate(shareId, clazz, value) } + } + pendingPropertyJobs[clazz] = PendingPropertyUpdate(job, shareId, value) } - fun commitPendingProperties(shareId: String) { - if (pendingProperties.isEmpty()) return + private suspend fun flushPendingProperties(shareId: String) { + val snapshot = pendingPropertyJobs.toMap() + snapshot.values.forEach { it.job.cancel() } + pendingPropertyJobs.clear() + if (snapshot.isEmpty()) return + withContext(Dispatchers.IO) { + coroutineScope { + snapshot.entries.map { (clazz, pending) -> + async { executePropertyUpdate(shareId, clazz, pending.value) } + }.awaitAll() + } + } + } - viewModelScope.launch(Dispatchers.IO) { - pendingProperties.forEach { (clazz, value) -> - repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value)) + private suspend fun executePropertyUpdate(shareId: String, clazz: String, value: String) { + when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { + is NetworkResult.Success -> { + _propertyErrors.update { it - clazz } + _activeShare.update { result.data } + replaceInList(result.data) + } + is NetworkResult.ServerError, + is NetworkResult.NetworkException -> { + _propertyErrors.update { it + (clazz to R.string.share_view_update_error_message) } } - pendingProperties.clear() } } // endregion @@ -223,10 +256,9 @@ class ShareViewModel( fun updatePermission(id: String, clazz: String, enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) - handleResult(result, R.string.share_view_update_error_message)?.let { updated -> - _activeShare.update { updated } - replaceInList(updated) - } + val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + _activeShare.update { updated } + replaceInList(updated) } } // endregion @@ -235,14 +267,14 @@ class ShareViewModel( fun deleteShare(id: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.deleteShare(id) - handleResult(result, R.string.share_view_delete_error_message)?.let { - val remaining = currentShares.filterNot { it.id == id } - _state.update { - if (remaining.filtered().isEmpty()) ShareScreenState.Empty - else ShareScreenState.Loaded(remaining) - } - if (_activeShare.value?.id == id) _activeShare.update { null } + handleResult(result, R.string.share_view_delete_error_message) ?: return@launch + + val remaining = currentShares.filterNot { it.id == id } + _state.update { + if (remaining.filtered().isEmpty()) ShareScreenState.Empty + else ShareScreenState.Loaded(remaining) } + if (_activeShare.value?.id == id) _activeShare.update { null } } } // endregion @@ -253,11 +285,16 @@ class ShareViewModel( } fun setActiveShare(value: Share?) { + _propertyErrors.update { emptyMap() } _activeShare.update { value } } // endregion // region private + companion object { + private const val PROPERTY_DEBOUNCE_DELAY_MS = 1000L + } + private fun replaceInList(updated: Share) { val shares = currentShares.ifEmpty { return } _state.update { ShareScreenState.Loaded(shares.map { if (it.id == updated.id) updated else it }) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index e9df46a1..09b2b6b6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -77,7 +77,6 @@ fun AddOrEditShareBottomSheet( if (share.shareState == ShareState.DRAFT) { onDismissDraft(share) } else { - viewModel.commitPendingProperties(share.id) viewModel.setActiveShare(null) } }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 974e381e..0ad0c739 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -7,60 +7,80 @@ package com.nextcloud.android.common.ui.share.component.property +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel -import com.nextcloud.android.common.ui.share.component.property.datepicker.ShareDatePicker import com.nextcloud.android.common.ui.share.component.ShareSwitch +import com.nextcloud.android.common.ui.share.component.property.datepicker.ShareDatePicker import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword import com.nextcloud.android.common.ui.share.model.api.property.PropertyString -import kotlinx.coroutines.delay +import com.nextcloud.android.common.ui.share.model.api.property.clazz @Composable fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { + val propertyErrors by viewModel.propertyErrors.collectAsState() + val hasError = propertyErrors[property.clazz] != null + when (property) { is PropertyBoolean -> { var checkedValue by remember(property.clazz) { mutableStateOf(property.isTrue()) } - ShareSwitch( - label = property.displayName, - checked = checkedValue, - onCheckedChange = { isChecked -> - checkedValue = isChecked - viewModel.updatePropertyLocally(property.clazz, isChecked.toString()) + Column { + ShareSwitch( + label = property.displayName, + checked = checkedValue, + onCheckedChange = { isChecked -> + checkedValue = isChecked + viewModel.updatePropertyDebounced(shareId, property.clazz, isChecked.toString()) + } + ) + if (hasError) { + Text( + text = stringResource(R.string.share_view_update_error_message), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 4.dp, bottom = 4.dp) + ) } - ) + } } is PropertyString -> { var textValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } - DebouncedPropertyUpdater( - value = textValue, - originalValue = property.value ?: "", - onDebounceComplete = { viewModel.updatePropertyLocally(property.clazz, it) } - ) - OutlinedTextField( value = textValue, - onValueChange = { textValue = it }, + onValueChange = { + textValue = it + viewModel.updatePropertyDebounced(shareId, property.clazz, it) + }, label = { Text(property.displayName) }, + isError = hasError, + supportingText = if (hasError) { + { Text(stringResource(R.string.share_view_update_error_message)) } + } else { + null + }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), singleLine = true ) @@ -69,18 +89,21 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM is PropertyPassword -> { var passwordValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } - DebouncedPropertyUpdater( - value = passwordValue, - originalValue = property.value ?: "", - onDebounceComplete = { viewModel.updatePropertyLocally(property.clazz, it) } - ) - OutlinedTextField( value = passwordValue, - onValueChange = { passwordValue = it }, + onValueChange = { + passwordValue = it + viewModel.updatePropertyDebounced(shareId, property.clazz, it) + }, label = { Text(property.displayName) }, placeholder = property.hint?.let { { Text(it) } }, visualTransformation = PasswordVisualTransformation(), + isError = hasError, + supportingText = if (hasError) { + { Text(stringResource(R.string.share_view_update_error_message)) } + } else { + null + }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp), @@ -95,24 +118,7 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM } is PropertyEnum -> { - // TODO: Implement ExposedDropdownMenuBox using property.validValues Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray) } } } - -@Composable -private fun DebouncedPropertyUpdater( - value: String, - originalValue: String, - delayMillis: Long = 400L, - onDebounceComplete: (String) -> Unit -) { - LaunchedEffect(value) { - if (value != originalValue) { - delay(delayMillis) - onDebounceComplete(value) - } - } -} - From 6fec46a493798a4f8065057e9405f4829e9536f7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 15:47:01 +0200 Subject: [PATCH 054/174] add anyone recipient Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 20 +++++++++++++++++++ .../bottomsheet/AddOrEditShareBottomSheet.kt | 9 ++++----- .../share/model/api/recipients/Recipient.kt | 6 +++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 31376752..b7d9b633 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -20,6 +20,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareProper import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.repository.ShareRepository @@ -38,6 +39,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.UUID class ShareViewModel( private val repository: ShareRepository @@ -193,6 +195,24 @@ class ShareViewModel( } } + fun addAnyoneRecipient(category: ShareCategory, share: Share) { + if (category != ShareCategory.Anyone) { + return + } + + val alreadyAdded = share.recipients.any { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } + if (alreadyAdded) { + return + } + + addRecipient( + id = share.id, + clazz = Recipient.TOKEN_RECIPIENT_CLASS, + value = UUID.randomUUID().toString(), + instance = UUID.randomUUID().toString() + ) + } + fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareRecipient(id, clazz, value, instance) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 09b2b6b6..9929ddc7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -96,7 +96,10 @@ fun AddOrEditShareBottomSheet( CategorySelector( categories = categories, selectedCategory = selectedCategory, - onCategorySelected = { selectedCategory = it } + onCategorySelected = { category -> + selectedCategory = category + viewModel.addAnyoneRecipient(category, share) + } ) if (selectedCategory == ShareCategory.Invited) { @@ -227,15 +230,11 @@ private fun CategorySelector( } } -// TODO implement Anyone --> /ocs/v2.php/apps/sharing/api/v1/share/{id}/recipient -// --> Add a recipient to a share use correspongind class value --> doesnt matter use uuid string : "value": "string", "instance": "string" // TODO discuss with laura for note to recepipent expiraitaion date proeprties etc should be collabls // TODO for COPY link use Recepient.Secret.URL side note: if value is updatable user can edit the TOKEN directly which is value -// TODO display error messages and handle them via debounce mechanism - // TODO fetch shares currently returns all shares SOURCE ID will be implemented so that u can only show related shares // TODO show share icon from first recipient but check with laura diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index 5f900004..a8e21e35 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -27,4 +27,8 @@ data class Recipient( val icon: Icon? = null, val secret: Secret -) +) { + companion object { + const val TOKEN_RECIPIENT_CLASS = "OC\\CoreSharing\\Recipient\\TokenShareRecipientType" + } +} From d83bc342645c51c3eba5ca9de07c55c72bfbf70a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 15:59:00 +0200 Subject: [PATCH 055/174] better error handling Signed-off-by: alperozturk96 --- .../ui/network/http/NextcloudHttpClient.kt | 9 ++- .../common/ui/network/model/NetworkResult.kt | 10 ++++ .../android/common/ui/share/ShareViewModel.kt | 59 ++++++++++--------- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 8996bfb2..5316ea0b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -10,10 +10,12 @@ package com.nextcloud.android.common.ui.network.http import com.nextcloud.android.common.ui.network.auth.AuthInterceptor import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.network.model.Ocs import com.nextcloud.android.common.ui.network.model.OcsResponse import com.nextcloud.android.common.ui.network.serialization.OCSSerializer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonElement import okhttp3.OkHttpClient import okhttp3.RequestBody import okhttp3.logging.HttpLoggingInterceptor @@ -65,7 +67,12 @@ class NextcloudHttpClient private constructor( val response = okHttpClient.newCall(request).execute() val responseBody = response.body.string() if (response.isSuccessful) { - NetworkResult.Success(parse(responseBody)) + val envelope = OCSSerializer.json.decodeFromString>(responseBody) + if (envelope.ocs.meta.status != "ok") { + NetworkResult.ServerError(OcsResponse(Ocs(envelope.ocs.meta, envelope.ocs.meta.message))) + } else { + NetworkResult.Success(parse(responseBody)) + } } else { NetworkResult.ServerError(OCSSerializer.json.decodeFromString>(responseBody)) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt index 932761b6..ef05856b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt @@ -20,3 +20,13 @@ sealed class NetworkResult { fun fromException(e: Throwable): NetworkException = NetworkException(e) } } + +inline fun NetworkResult.dataOrElse(onError: () -> Unit): T? = + when (this) { + is NetworkResult.Success -> data + is NetworkResult.ServerError, + is NetworkResult.NetworkException -> { + onError() + null + } + } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index b7d9b633..29ad7c4f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.network.model.dataOrElse import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest @@ -40,11 +41,17 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.UUID +import kotlin.time.Duration.Companion.milliseconds class ShareViewModel( private val repository: ShareRepository ) : ViewModel() { + companion object { + private const val SEARCH_DEBOUNCE_DELAY_MS = 300L + private const val PROPERTY_DEBOUNCE_DELAY_MS = 1000L + } + private val _state = MutableStateFlow(ShareScreenState.Loading) val state: StateFlow = _state @@ -79,7 +86,7 @@ class ShareViewModel( private fun initSearchQuery() { viewModelScope.launch { _searchQuery - .debounce(300L) + .debounce(SEARCH_DEBOUNCE_DELAY_MS.milliseconds) .distinctUntilChanged() .filter { it.isNotBlank() } .collect { query -> executeSearch(query) } @@ -109,7 +116,8 @@ class ShareViewModel( _errorMessageId.update { null } val result = repository.fetchShares(sourceClass, lastShareID, limit) - val fetched = handleResult(result, R.string.share_view_fetch_error_message) ?: return@launch + val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } + ?: return@launch _state.update { if (fetched.filtered().isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(fetched) @@ -122,7 +130,8 @@ class ShareViewModel( _errorMessageId.update { null } val result = repository.fetchShare(id, request) - val share = handleResult(result, R.string.share_view_fetch_error_message) ?: return@launch + val share = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } + ?: return@launch _activeShare.update { share } replaceInList(share) @@ -135,7 +144,8 @@ class ShareViewModel( _errorMessageId.update { null } val result = repository.createDraftShare() - val draft = handleResult(result, R.string.share_view_create_error_message) + + val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } if (draft != null) { _activeShare.update { draft } @@ -153,7 +163,8 @@ class ShareViewModel( val result = withContext(Dispatchers.IO) { repository.updateShareState(id, UpdateShareStateRequest(shareState)) } - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch if (shareState == ShareState.ACTIVE) { _activeShare.update { null } } else { @@ -169,7 +180,8 @@ class ShareViewModel( viewModelScope.launch(Dispatchers.IO) { val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch _activeShare.update { updated } replaceInList(updated) } @@ -178,7 +190,8 @@ class ShareViewModel( fun removeSource(id: String, clazz: String, value: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareSource(id, clazz, value) - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch _activeShare.update { updated } replaceInList(updated) } @@ -189,7 +202,8 @@ class ShareViewModel( fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch _activeShare.update { updated } replaceInList(updated) } @@ -216,7 +230,8 @@ class ShareViewModel( fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareRecipient(id, clazz, value, instance) - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch _activeShare.update { updated } replaceInList(updated) } @@ -227,7 +242,8 @@ class ShareViewModel( fun updateProperty(id: String, clazz: String, value: String?) { viewModelScope.launch(Dispatchers.IO) { val result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value)) - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch _activeShare.update { updated } replaceInList(updated) } @@ -236,7 +252,7 @@ class ShareViewModel( fun updatePropertyDebounced(shareId: String, clazz: String, value: String) { pendingPropertyJobs[clazz]?.job?.cancel() val job = viewModelScope.launch { - delay(PROPERTY_DEBOUNCE_DELAY_MS) + delay(PROPERTY_DEBOUNCE_DELAY_MS.milliseconds) pendingPropertyJobs.remove(clazz) withContext(Dispatchers.IO) { executePropertyUpdate(shareId, clazz, value) } } @@ -264,6 +280,7 @@ class ShareViewModel( _activeShare.update { result.data } replaceInList(result.data) } + is NetworkResult.ServerError, is NetworkResult.NetworkException -> { _propertyErrors.update { it + (clazz to R.string.share_view_update_error_message) } @@ -276,7 +293,8 @@ class ShareViewModel( fun updatePermission(id: String, clazz: String, enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) - val updated = handleResult(result, R.string.share_view_update_error_message) ?: return@launch + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch _activeShare.update { updated } replaceInList(updated) } @@ -287,7 +305,7 @@ class ShareViewModel( fun deleteShare(id: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.deleteShare(id) - handleResult(result, R.string.share_view_delete_error_message) ?: return@launch + result.dataOrElse { _errorMessageId.update { R.string.share_view_delete_error_message } } ?: return@launch val remaining = currentShares.filterNot { it.id == id } _state.update { @@ -311,24 +329,9 @@ class ShareViewModel( // endregion // region private - companion object { - private const val PROPERTY_DEBOUNCE_DELAY_MS = 1000L - } - private fun replaceInList(updated: Share) { val shares = currentShares.ifEmpty { return } _state.update { ShareScreenState.Loaded(shares.map { if (it.id == updated.id) updated else it }) } } - - private fun handleResult(result: NetworkResult, errorId: Int): T? { - return when (result) { - is NetworkResult.Success -> result.data - is NetworkResult.ServerError, - is NetworkResult.NetworkException -> { - _errorMessageId.update { errorId } - null - } - } - } // endregion } From 7a901452150fd2ee8968756ad9da4fdc0645efed Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 16:11:43 +0200 Subject: [PATCH 056/174] better error handling Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareViewModel.kt | 4 +++- .../android/common/ui/share/model/api/recipients/Recipient.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 29ad7c4f..b3ec6b95 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -219,11 +219,13 @@ class ShareViewModel( return } + // TODO: other recipients if user switch to anyone. + // THIS WILL FAIL for now UUID.randomUUID().toString() will be supported addRecipient( id = share.id, clazz = Recipient.TOKEN_RECIPIENT_CLASS, value = UUID.randomUUID().toString(), - instance = UUID.randomUUID().toString() + instance = null ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index a8e21e35..be1ce60a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -29,6 +29,6 @@ data class Recipient( val secret: Secret ) { companion object { - const val TOKEN_RECIPIENT_CLASS = "OC\\CoreSharing\\Recipient\\TokenShareRecipientType" + const val TOKEN_RECIPIENT_CLASS = "OC\\Core\\Sharing\\Recipient\\TokenShareRecipientType" } } From 931fea17ac1c547e96ee394833221c43df0628ff Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 16:47:17 +0200 Subject: [PATCH 057/174] fix issues Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 1 + .../bottomsheet/AddOrEditShareBottomSheet.kt | 64 ++++---- .../component/property/SharePropertyView.kt | 1 - .../ui/share/model/api/property/Property.kt | 144 ++++++------------ .../common/ui/share/model/api/share/Share.kt | 10 +- 5 files changed, 84 insertions(+), 136 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index b3ec6b95..c12c1e9d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -178,6 +178,7 @@ class ShareViewModel( // region sources fun addSource(id: String, value: String) { viewModelScope.launch(Dispatchers.IO) { + // TODO pass from the clients this may vary depends on the client so notes and files for now uses this val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 9929ddc7..8d476ea1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -50,8 +50,6 @@ import com.nextcloud.android.common.ui.share.component.RecipientSearchField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities -import com.nextcloud.android.common.ui.share.model.api.property.clazz -import com.nextcloud.android.common.ui.share.model.api.property.priority import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory @@ -130,16 +128,7 @@ fun AddOrEditShareBottomSheet( } if (share.readyToSend()) { - val localClipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - - Buttons(onCopyLink = { - scope.launch { - share.getClipEntry()?.let { clipEntry -> - localClipboard.setClipEntry(clipEntry) - } - } - }, onSend = { + Buttons(share, selectedCategory, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) }) } @@ -149,7 +138,8 @@ fun AddOrEditShareBottomSheet( @Composable private fun Buttons( - onCopyLink: () -> Unit = {}, + share: Share, + category: ShareCategory, onSend: () -> Unit = {}, ) { Row( @@ -158,26 +148,37 @@ private fun Buttons( .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Button( - onClick = onCopyLink, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - painter = painterResource(R.drawable.ic_link), - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource(R.string.share_view_copy_action), - modifier = Modifier.padding(start = 8.dp), - ) + val clipEntry = share.getClipEntry() + if (category == ShareCategory.Anyone && clipEntry != null) { + val localClipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + + Button( + onClick = { + scope.launch { + localClipboard.setClipEntry(clipEntry) + } + }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Icon( + painter = painterResource(R.drawable.ic_link), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.share_view_copy_action), + modifier = Modifier.padding(start = 8.dp), + ) + } } + Button( onClick = onSend, modifier = Modifier.weight(1f), @@ -230,6 +231,7 @@ private fun CategorySelector( } } +// TODO copy link must be only for ANYONE // TODO discuss with laura for note to recepipent expiraitaion date proeprties etc should be collabls diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 0ad0c739..ca482a15 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -34,7 +34,6 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword import com.nextcloud.android.common.ui.share.model.api.property.PropertyString -import com.nextcloud.android.common.ui.share.model.api.property.clazz @Composable fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 2aeb3d8c..27522128 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -15,129 +15,73 @@ import kotlinx.serialization.json.JsonClassDiscriminator @OptIn(ExperimentalSerializationApi::class) @JsonClassDiscriminator("type") @Serializable -sealed class Property - -val Property.priority: Int - get() = when (this) { - is PropertyBoolean -> priority - is PropertyDate -> priority - is PropertyEnum -> priority - is PropertyPassword -> priority - is PropertyString -> priority - } - -val Property.clazz: String - get() = when (this) { - is PropertyBoolean -> clazz - is PropertyDate -> clazz - is PropertyEnum -> clazz - is PropertyPassword -> clazz - is PropertyString -> clazz - } +sealed class Property { + abstract val clazz: String + abstract val displayName: String + abstract val hint: String? + abstract val priority: Int + abstract val required: Boolean + abstract val value: String? +} @Serializable @SerialName("boolean") data class PropertyBoolean( - @SerialName("class") - val clazz: String, - - @SerialName("display_name") - val displayName: String, - - val hint: String? = null, - - val priority: Int, - - val required: Boolean, - - val value: String? = null + @SerialName("class") override val clazz: String, + @SerialName("display_name") override val displayName: String, + override val hint: String? = null, + override val priority: Int, + override val required: Boolean, + override val value: String? = null ) : Property() { - fun isTrue(): Boolean = (value == "true") + fun isTrue(): Boolean = value == "true" } @Serializable @SerialName("date") data class PropertyDate( - @SerialName("class") - val clazz: String, - - @SerialName("display_name") - val displayName: String, - - val hint: String? = null, - - val priority: Int, - - val required: Boolean, - - val value: String? = null, - - @SerialName("min_date") - val minDate: String? = null, - - @SerialName("max_date") - val maxDate: String? = null + @SerialName("class") override val clazz: String, + @SerialName("display_name") override val displayName: String, + override val hint: String? = null, + override val priority: Int, + override val required: Boolean, + override val value: String? = null, + @SerialName("min_date") val minDate: String? = null, + @SerialName("max_date") val maxDate: String? = null ) : Property() @Serializable @SerialName("enum") data class PropertyEnum( - @SerialName("class") - val clazz: String, - - @SerialName("display_name") - val displayName: String, - - val hint: String? = null, - - val priority: Int, - - val required: Boolean, - - val value: String? = null, - - @SerialName("valid_values") - val validValues: List + @SerialName("class") override val clazz: String, + @SerialName("display_name") override val displayName: String, + override val hint: String? = null, + override val priority: Int, + override val required: Boolean, + override val value: String? = null, + @SerialName("valid_values") val validValues: List ) : Property() @Serializable @SerialName("password") data class PropertyPassword( - @SerialName("class") - val clazz: String, - - @SerialName("display_name") - val displayName: String, - - val hint: String? = null, - - val priority: Int, - - val required: Boolean, - - val value: String? = null + @SerialName("class") override val clazz: String, + @SerialName("display_name") override val displayName: String, + override val hint: String? = null, + override val priority: Int, + override val required: Boolean, + override val value: String? = null ) : Property() @Serializable @SerialName("string") data class PropertyString( - @SerialName("class") - val clazz: String, - - @SerialName("display_name") - val displayName: String, - - val hint: String? = null, - - val priority: Int, - - val required: Boolean, - - val value: String? = null, - - @SerialName("min_length") - val minLength: Int? = null, - - @SerialName("max_length") - val maxLength: Int? = null + @SerialName("class") override val clazz: String, + @SerialName("display_name") override val displayName: String, + override val hint: String? = null, + override val priority: Int, + override val required: Boolean, + override val value: String? = null, + @SerialName("min_length") val minLength: Int? = null, + @SerialName("max_length") val maxLength: Int? = null ) : Property() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 1d8b473e..9da0e6d6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -43,7 +43,7 @@ data class Share( ) { fun getClipEntry(): ClipEntry? { val label = recipients.first().displayName - val link = recipients.first().secret.url + val link = recipients.firstOrNull()?.secret?.url return if (link != null) { ClipData.newPlainText(label, link).toClipEntry() @@ -52,9 +52,11 @@ data class Share( } } - fun readyToSend(): Boolean { - return sources.isNotEmpty() && recipients.isNotEmpty() && permissions.any { it.enabled } - } + fun readyToSend(): Boolean = + sources.isNotEmpty() && + recipients.isNotEmpty() && + permissions.any { it.enabled } && + properties.none { it.required && it.value.isNullOrEmpty() } fun title(context: Context): String { return if (shareState == ShareState.DRAFT) { From 46122ab0563718bc2c70da3bfe38e002057f30e4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 10 Jun 2026 16:52:21 +0200 Subject: [PATCH 058/174] fix issues Signed-off-by: alperozturk96 --- .../ui/network/http/NextcloudHttpClient.kt | 3 +- .../android/common/ui/share/ShareViewModel.kt | 29 +++++++++---------- .../share/model/api/recipients/Recipient.kt | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 5316ea0b..5294b8fd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -29,6 +29,7 @@ class NextcloudHttpClient private constructor( private const val CONNECT_TIMEOUT_SECONDS = 90L private const val READ_TIMEOUT_SECONDS = 90L private const val WRITE_TIMEOUT_SECONDS = 90L + private const val OCS_OK = "ok" fun create( credentials: ServerCredentials, @@ -68,7 +69,7 @@ class NextcloudHttpClient private constructor( val responseBody = response.body.string() if (response.isSuccessful) { val envelope = OCSSerializer.json.decodeFromString>(responseBody) - if (envelope.ocs.meta.status != "ok") { + if (envelope.ocs.meta.status != OCS_OK) { NetworkResult.ServerError(OcsResponse(Ocs(envelope.ocs.meta, envelope.ocs.meta.message))) } else { NetworkResult.Success(parse(responseBody)) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index c12c1e9d..91d38556 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -211,23 +211,22 @@ class ShareViewModel( } fun addAnyoneRecipient(category: ShareCategory, share: Share) { - if (category != ShareCategory.Anyone) { - return - } + if (category != ShareCategory.Anyone) return - val alreadyAdded = share.recipients.any { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } - if (alreadyAdded) { - return - } + share.recipients + .filter { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } + .forEach { recipient -> + removeRecipient(share.id, recipient.clazz, recipient.value, recipient.instance) + } - // TODO: other recipients if user switch to anyone. - // THIS WILL FAIL for now UUID.randomUUID().toString() will be supported - addRecipient( - id = share.id, - clazz = Recipient.TOKEN_RECIPIENT_CLASS, - value = UUID.randomUUID().toString(), - instance = null - ) + if (share.recipients.none { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS }) { + addRecipient( + id = share.id, + clazz = Recipient.TOKEN_RECIPIENT_CLASS, + value = UUID.randomUUID().toString(), + instance = null + ) + } } fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index be1ce60a..a8e21e35 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -29,6 +29,6 @@ data class Recipient( val secret: Secret ) { companion object { - const val TOKEN_RECIPIENT_CLASS = "OC\\Core\\Sharing\\Recipient\\TokenShareRecipientType" + const val TOKEN_RECIPIENT_CLASS = "OC\\CoreSharing\\Recipient\\TokenShareRecipientType" } } From 54246d3157c933eac0423717407ee16cb6b5e295 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 22 Jun 2026 11:55:00 +0200 Subject: [PATCH 059/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 8d476ea1..7a9e16f8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -231,18 +231,7 @@ private fun CategorySelector( } } -// TODO copy link must be only for ANYONE -// TODO discuss with laura for note to recepipent expiraitaion date proeprties etc should be collabls - -// TODO for COPY link use Recepient.Secret.URL side note: if value is updatable user can edit the TOKEN directly which is value - -// TODO fetch shares currently returns all shares SOURCE ID will be implemented so that u can only show related shares - -// TODO show share icon from first recipient but check with laura - -// TODO: Backend will change and provide us bundled permissions or list of custom permissions. -// USE THIS FOR BOTH OPTION @Composable private fun PermissionCategories( share: Share, From a7dbd194622f709ad219adda8f160c0591934b4e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 13:51:52 +0200 Subject: [PATCH 060/174] wip Signed-off-by: alperozturk96 # Conflicts: # gradle/verification-metadata.xml --- sample/build.gradle | 1 + ui/build.gradle | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index edbcce1c..6c26adc3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.2.2' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.11.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' implementation project(path: ':ui') testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.3.0' diff --git a/ui/build.gradle b/ui/build.gradle index d2a20ed9..bf833022 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -16,7 +16,7 @@ plugins { android { defaultConfig { - minSdk = 21 + minSdk = 26 compileSdk = 37 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -46,8 +46,9 @@ android { } dependencies { - implementation 'androidx.compose.ui:ui-tooling-preview:1.11.0' - debugImplementation 'androidx.compose.ui:ui-tooling:1.11.0' + // region Jetpack Compose + implementation 'androidx.compose.ui:ui-tooling-preview:1.11.4' + debugImplementation 'androidx.compose.ui:ui-tooling:1.11.4' kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}" implementation(platform("androidx.compose:compose-bom:2026.08.00")) @@ -55,10 +56,11 @@ dependencies { implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.material3:material3") implementation("androidx.compose.material:material-icons-core") + // endregion - implementation("io.coil-kt.coil3:coil-compose:3.4.0") - implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0") - implementation("io.coil-kt.coil3:coil-svg:3.4.0") + implementation("io.coil-kt.coil3:coil-compose:3.5.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.5.0") + implementation("io.coil-kt.coil3:coil-svg:3.5.0") implementation("com.vanniktech:ui:0.10.0") @@ -68,9 +70,11 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' - implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.2")) + // region okhttp + implementation(platform("com.squareup.okhttp3:okhttp-bom:5.4.0")) implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.okhttp3:logging-interceptor") + // endregion implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") From 3d3037c722190c56529c8599f69971bbe7b4bb63 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:00:14 +0200 Subject: [PATCH 061/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 58 ++++--------------- .../capabilities/PermissionCategoryType.kt | 23 -------- .../api/capabilities/SharingCapabilities.kt | 2 - .../common/ui/share/model/api/owner/Owner.kt | 25 -------- .../share/model/api/permission/Permission.kt | 2 +- .../share/model/api/recipients/Recipient.kt | 5 +- .../common/ui/share/model/api/share/Share.kt | 10 +++- .../share/repository/MockShareRepository.kt | 21 +++---- 8 files changed, 35 insertions(+), 111 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 7a9e16f8..d6ff084e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -240,56 +240,22 @@ private fun PermissionCategories( onToggleCategory: (String) -> Unit, viewModel: ShareViewModel ) { - /* - var expanded by remember { mutableStateOf(false) } + // We have 3 options in dropdown null, edit, view + // If user selects null ("Can...") just show all permissions without hiding behind collabsection section + // If user selects edit or view dont show any toggle -> immediately update permission - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - share.permissions.forEach { - key(it.displayName) { - DropdownMenuItem( - text = { Text(it.displayName) }, - onClick = { /* Do something... */ } + share.permissions + .forEach { permission -> + key(permission.clazz) { + ShareSwitch( + label = permission.displayName, + checked = permission.enabled, + onCheckedChange = { isChecked -> + viewModel.updatePermission(share.id, permission.clazz, isChecked) + } ) } } - - DropdownMenuItem( - text = { Text("Option 2") }, - onClick = { /* Do something... */ } - ) - } - */ - - - sharingCapabilities.permissionCategoryTypes - .sortedBy { it.priority } - .forEach { sharingCapability -> - key(sharingCapability.classField) { - CollapsibleShareSection( - label = sharingCapability.displayName, - isExpanded = sharingCapability.displayName in expandedCategories, - onToggle = { onToggleCategory(sharingCapability.displayName) }, - ) { - share.permissions - .filter { permission -> permission.category == sharingCapability.classField } - .sortedBy { it.displayName } - .forEach { permission -> - key(permission.clazz) { - ShareSwitch( - label = permission.displayName, - checked = permission.enabled, - onCheckedChange = { isChecked -> - viewModel.updatePermission(share.id, permission.clazz, isChecked) - } - ) - } - } - } - } - } } @Composable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt deleted file mode 100644 index 17fd74cc..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/PermissionCategoryType.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.capabilities - -import com.nextcloud.android.common.ui.share.model.api.icon.Icon -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class PermissionCategoryType( - @SerialName("class") - val classField: String, - @SerialName("display_name") - val displayName: String, - val hint: String? = null, - val icon: Icon? = null, - val priority: Long, -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 13452ea9..04eb864d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -17,6 +17,4 @@ data class SharingCapabilities( val legacy: Legacy? = null, @SerialName("source_types") val sourceTypes: List, - @SerialName("permission_category_types") - val permissionCategoryTypes: List, ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt deleted file mode 100644 index d5646004..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/owner/Owner.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.owner - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import com.nextcloud.android.common.ui.share.model.api.icon.Icon - -@Serializable -data class Owner( - @SerialName("user_id") - val userId: String, - - val instance: String? = null, - - @SerialName("display_name") - val displayName: String, - - val icon: Icon -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt index 1e8c62f9..d568ae08 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -23,7 +23,7 @@ data class Permission( val hint: String? = null, - val category: String? = null, + val presets: List, val enabled: Boolean ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index a8e21e35..5d250b31 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -9,6 +9,7 @@ package com.nextcloud.android.common.ui.share.model.api.recipients import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.secret.Secret +import com.nextcloud.android.common.ui.share.model.api.user.User import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -26,7 +27,9 @@ data class Recipient( val icon: Icon? = null, - val secret: Secret + val secret: Secret, + + val initiator: User? = null ) { companion object { const val TOKEN_RECIPIENT_CLASS = "OC\\CoreSharing\\Recipient\\TokenShareRecipientType" diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 9da0e6d6..435c20f5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -12,12 +12,13 @@ import android.content.Context import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.toClipEntry import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.model.api.owner.Owner import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.api.user.User import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -25,7 +26,7 @@ import kotlinx.serialization.Serializable data class Share( val id: String, - val owner: Owner, + val owner: User, @SerialName("last_updated") val lastUpdated: Long, @@ -39,7 +40,10 @@ data class Share( val properties: List, - val permissions: List + val permissions: List, + + @SerialName("permission_preset") + val permissionPreset: PermissionPreset? = null ) { fun getClipEntry(): ClipEntry? { val label = recipients.first().displayName diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 2edf350e..f2f464b8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -9,8 +9,8 @@ package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.share.model.api.icon.Icon -import com.nextcloud.android.common.ui.share.model.api.owner.Owner import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword @@ -27,10 +27,11 @@ import com.nextcloud.android.common.ui.share.model.api.secret.Secret import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.api.user.User class MockShareRepository : ShareRepository { - private val mockOwner = Owner( + private val mockOwner = User( userId = "alice", displayName = "Alice Johnson", icon = Icon( @@ -43,19 +44,19 @@ class MockShareRepository : ShareRepository { Permission( clazz = "read", displayName = "Read", - category = "access", + presets = listOf(PermissionPreset.VIEW, PermissionPreset.EDIT), enabled = true ), Permission( clazz = "update", displayName = "Update", - category = "access", + presets = listOf(PermissionPreset.EDIT), enabled = false ), Permission( clazz = "delete", displayName = "Delete", - category = "access", + presets = listOf(PermissionPreset.EDIT), enabled = false ) ) @@ -99,7 +100,7 @@ class MockShareRepository : ShareRepository { recipients: List, shareState: ShareState = ShareState.ACTIVE, lastUpdated: Long = System.currentTimeMillis(), - owner: Owner = mockOwner + owner: User = mockOwner ) = Share( id = id, owner = owner, @@ -147,7 +148,7 @@ class MockShareRepository : ShareRepository { secret = Secret(false, value = "", url = "") ) ), - owner = Owner( + owner = User( userId = "system", displayName = "System", icon = Icon( @@ -163,7 +164,7 @@ class MockShareRepository : ShareRepository { ), recipients = emptyList(), lastUpdated = 1710000000L, - owner = Owner( + owner = User( userId = "system", displayName = "System", icon = Icon( @@ -189,7 +190,7 @@ class MockShareRepository : ShareRepository { secret = Secret(false, value = "", url = "") ) ), - owner = Owner( + owner = User( userId = "john", displayName = "John External", icon = Icon( @@ -216,7 +217,7 @@ class MockShareRepository : ShareRepository { secret = Secret(false, value = "", url = "") ) ), - owner = Owner( + owner = User( userId = "partner", displayName = "Partner Cloud", icon = Icon( From 3ff951cd8ba4e6feac5ccddfbb56484ea32dedad Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:00:22 +0200 Subject: [PATCH 062/174] wip Signed-off-by: alperozturk96 --- .../model/api/permission/PermissionPreset.kt | 20 +++++++++++++++ .../common/ui/share/model/api/user/User.kt | 25 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt new file mode 100644 index 00000000..fb4f321c --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.permission + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class PermissionPreset { + @SerialName("view") + VIEW, + + @SerialName("edit") + EDIT +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt new file mode 100644 index 00000000..d0870bf3 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.user + +import com.nextcloud.android.common.ui.share.model.api.icon.Icon +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class User( + @SerialName("user_id") + val userId: String, + + val instance: String? = null, + + @SerialName("display_name") + val displayName: String, + + val icon: Icon +) From 744517a740a22204068bdcc7dad302a48a3adeb0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:14:09 +0200 Subject: [PATCH 063/174] add PermissionPresetDropdown Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 1 - .../android/common/ui/share/ShareViewModel.kt | 12 +++ .../bottomsheet/AddOrEditShareBottomSheet.kt | 91 +++++++++++++------ .../share/model/api/request/ShareRequests.kt | 6 ++ .../share/model/ui/PermissionPresetOption.kt | 25 +++++ .../share/repository/MockShareRepository.kt | 16 ++++ .../share/repository/ShareRemoteRepository.kt | 13 +++ .../ui/share/repository/ShareRepository.kt | 6 ++ ui/src/main/res/values/strings.xml | 5 + 9 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index d49523c0..ccd48760 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -155,7 +155,6 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti activeShare?.let { AddOrEditShareBottomSheet( share = it, - sharingCapabilities = sharingCapabilities, viewModel = viewModel, onDismissDraft = { draftShare -> activeShare?.let { viewModel.deleteShare(draftShare.id) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 91d38556..ec6992cb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -16,6 +16,8 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest @@ -301,6 +303,16 @@ class ShareViewModel( replaceInList(updated) } } + + fun updatePermissionPreset(id: String, preset: PermissionPreset) { + viewModelScope.launch(Dispatchers.IO) { + val result = repository.updateSharePermissionPreset(id, UpdateSharePermissionPresetRequest(preset)) + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch + _activeShare.update { updated } + replaceInList(updated) + } + } // endregion // region delete diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index d6ff084e..08c247f4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -21,10 +21,15 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon 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 @@ -49,9 +54,9 @@ import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.RecipientSearchField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView -import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.coroutines.launch @@ -59,7 +64,6 @@ import kotlinx.coroutines.launch @Composable fun AddOrEditShareBottomSheet( share: Share, - sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel, onDismissDraft: (Share) -> Unit = {} ) { @@ -68,7 +72,6 @@ fun AddOrEditShareBottomSheet( val categories = remember { ShareCategory.entries.toList() } var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } - var expandedCategories by remember { mutableStateOf(emptySet()) } ModalBottomSheet( onDismissRequest = { @@ -106,15 +109,6 @@ fun AddOrEditShareBottomSheet( PermissionCategories( share = share, - sharingCapabilities = sharingCapabilities, - expandedCategories = expandedCategories, - onToggleCategory = { categoryName -> - expandedCategories = if (expandedCategories.contains(categoryName)) { - expandedCategories - categoryName - } else { - expandedCategories + categoryName - } - }, viewModel = viewModel ) @@ -232,30 +226,75 @@ private fun CategorySelector( } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun PermissionCategories( share: Share, - sharingCapabilities: SharingCapabilities, - expandedCategories: Set, - onToggleCategory: (String) -> Unit, viewModel: ShareViewModel ) { - // We have 3 options in dropdown null, edit, view - // If user selects null ("Can...") just show all permissions without hiding behind collabsection section - // If user selects edit or view dont show any toggle -> immediately update permission + var selectedPreset by remember(share.id) { mutableStateOf(share.permissionPreset) } + + PermissionPresetDropdown( + selectedOption = PermissionPresetOption.from(selectedPreset), + onOptionSelected = { option -> + selectedPreset = option.preset + option.preset?.let { viewModel.updatePermissionPreset(share.id, it) } + } + ) - share.permissions - .forEach { permission -> - key(permission.clazz) { - ShareSwitch( - label = permission.displayName, - checked = permission.enabled, - onCheckedChange = { isChecked -> - viewModel.updatePermission(share.id, permission.clazz, isChecked) + if (selectedPreset != null) return + + share.permissions.forEach { permission -> + key(permission.clazz) { + ShareSwitch( + label = permission.displayName, + checked = permission.enabled, + onCheckedChange = { isChecked -> + viewModel.updatePermission(share.id, permission.clazz, isChecked) + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PermissionPresetDropdown( + selectedOption: PermissionPresetOption, + onOptionSelected: (PermissionPresetOption) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + value = stringResource(selectedOption.labelRes), + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(R.string.share_view_permission_preset_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true) + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + PermissionPresetOption.entries.forEach { option -> + DropdownMenuItem( + text = { Text(stringResource(option.labelRes)) }, + onClick = { + expanded = false + onOptionSelected(option) } ) } } + } } @Composable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt index 8565a604..c39733b3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share.model.api.request +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.state.ShareState import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -58,6 +59,11 @@ data class UpdateSharePermissionRequest( val enabled: Boolean ) +@Serializable +data class UpdateSharePermissionPresetRequest( + val permissionPreset: PermissionPreset +) + @Serializable data class UpdateShareRecipientSecretRequest( @SerialName("class") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt new file mode 100644 index 00000000..886a0f75 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset + +enum class PermissionPresetOption( + val preset: PermissionPreset?, + val labelRes: Int +) { + CUSTOM(null, R.string.share_view_permission_preset_custom), + VIEW(PermissionPreset.VIEW, R.string.share_view_permission_preset_view), + EDIT(PermissionPreset.EDIT, R.string.share_view_permission_preset_edit); + + companion object { + fun from(preset: PermissionPreset?): PermissionPresetOption = + entries.firstOrNull { it.preset == preset } ?: CUSTOM + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index f2f464b8..f125d92c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -19,6 +19,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest @@ -450,4 +451,19 @@ class MockShareRepository : ShareRepository { if (index >= 0) mockShares[index] = updated return NetworkResult.Success(updated) } + + override suspend fun updateSharePermissionPreset( + id: String, + request: UpdateSharePermissionPresetRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = + if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val updated = current.copy( + permissionPreset = request.permissionPreset, + lastUpdated = System.currentTimeMillis() + ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 6498564e..f0bcb16e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -17,6 +17,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest @@ -188,6 +189,18 @@ class ShareRemoteRepository( json.decodeFromString>(body).ocs.data } + override suspend fun updateSharePermissionPreset( + id: String, + request: UpdateSharePermissionPresetRequest + ): NetworkResult = + client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/permission/preset", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { body -> + json.decodeFromString>(body).ocs.data + } + override suspend fun updateShareRecipientSecret( id: String, request: UpdateShareRecipientSecretRequest diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 8ce580f3..9ed354e0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -11,6 +11,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest @@ -79,6 +80,11 @@ interface ShareRepository { request: UpdateSharePermissionRequest ): NetworkResult + suspend fun updateSharePermissionPreset( + id: String, + request: UpdateSharePermissionPresetRequest + ): NetworkResult + suspend fun updateShareRecipientSecret( id: String, request: UpdateShareRecipientSecretRequest diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 540a7009..1d2825ed 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -71,4 +71,9 @@ Failed to create share. Failed to update share. + Permissions + Can… + Can view + Can edit + \ No newline at end of file From 7b4d707dde7eed232737aa70fb93b88fe76e58cf Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:22:21 +0200 Subject: [PATCH 064/174] remove unused strings Signed-off-by: alperozturk96 --- ui/src/main/res/values/strings.xml | 40 +----------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 1d2825ed..bd273d2a 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -12,45 +12,15 @@ Create a new share Share %s - File drop - Can view - Can edit - Custom permissions - - View files - Edit files - Create files - Delete files - Settings Add people - Name, team, email or federated ID - Participants Not shared No result - Note to recipients - - Anyone with the link - - Share with others - Edit file - Expiration date - Hide download and sync options - Label - Optional name for this link - Expiration date - Password protection - Limit downloads - Hide downloads - Video verification - - Show files in grid view Copied Copy link Send - Create public link Edit Send email @@ -59,19 +29,11 @@ This share will be permanently removed. Failed to fetch shares. - - Discard new share? - This share hasn\'t been saved yet. Do you want to discard it? - Keep - Discard - - Share not found, cannot delete. Failed to delete share. - Failed to create share. Failed to update share. - Permissions + Participants Can… Can view Can edit From b5d65c61e852778db414471cd953efb1dec49d0e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:35:11 +0200 Subject: [PATCH 065/174] wip Signed-off-by: alperozturk96 --- .../component/bottomsheet/AddOrEditShareBottomSheet.kt | 7 +++++-- .../android/common/ui/share/model/ui/ShareCategory.kt | 10 ++++++++-- ui/src/main/res/drawable/ic_anyone.xml | 9 +++++++++ ui/src/main/res/values/strings.xml | 2 ++ 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 ui/src/main/res/drawable/ic_anyone.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 08c247f4..6362cd5a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -217,9 +217,12 @@ private fun CategorySelector( SegmentedButton( selected = selectedCategory == category, onClick = { onCategorySelected(category) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size) + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), + icon = { + Icon(painter = painterResource(category.iconId), contentDescription = "") + } ) { - Text(category.name) + Text(stringResource(category.titleId)) } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt index 491b1b57..e58af38d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt @@ -7,6 +7,12 @@ package com.nextcloud.android.common.ui.share.model.ui -enum class ShareCategory { - Invited, Anyone +import com.nextcloud.android.common.ui.R + +enum class ShareCategory(val titleId: Int, val iconId: Int) { + Invited( + R.string.share_view_invited_category_title, + R.drawable.ic_person_add + ), + Anyone(R.string.share_view_anyone_category_title, R.drawable.ic_anyone) } diff --git a/ui/src/main/res/drawable/ic_anyone.xml b/ui/src/main/res/drawable/ic_anyone.xml new file mode 100644 index 00000000..4e16a67b --- /dev/null +++ b/ui/src/main/res/drawable/ic_anyone.xml @@ -0,0 +1,9 @@ + + + diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index bd273d2a..13c62abe 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -38,4 +38,6 @@ Can view Can edit + Invited people + Anyone \ No newline at end of file From b49a0397e3110d22c01cf71f8a2745b407f2488a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:53:07 +0200 Subject: [PATCH 066/174] wip Signed-off-by: alperozturk96 --- .../component/CollapsibleShareSection.kt | 2 +- .../share/component/RecipientSearchField.kt | 3 +- .../common/ui/share/component/ShareSwitch.kt | 2 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 173 +++++++++++------- 4 files changed, 112 insertions(+), 68 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt index affe1da5..91e640c4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -32,7 +32,7 @@ fun CollapsibleShareSection( onToggle: () -> Unit, content: @Composable () -> Unit ) { - Column(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Row( modifier = Modifier .fillMaxWidth() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt index ade91d16..e7d8a84c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons @@ -59,7 +60,7 @@ fun RecipientSearchField( val chipScrollState = rememberScrollState() Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { if (share.recipients.isNotEmpty()) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index 8af52f43..ebe0744d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -31,7 +31,7 @@ fun ShareSwitch( modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) - .padding(vertical = 4.dp), + .padding(vertical = 4.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 6362cd5a..e0e842a1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -11,12 +11,11 @@ package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -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.automirrored.filled.Send import androidx.compose.material3.Button @@ -47,6 +46,7 @@ import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel @@ -54,10 +54,18 @@ import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.RecipientSearchField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView +import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.property.PropertyString +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.secret.Secret import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.api.user.User import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory +import com.nextcloud.android.common.ui.share.repository.MockShareRepository import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -68,40 +76,50 @@ fun AddOrEditShareBottomSheet( onDismissDraft: (Share) -> Unit = {} ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val scrollState = rememberScrollState() val categories = remember { ShareCategory.entries.toList() } var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } + val context = LocalContext.current - ModalBottomSheet( - onDismissRequest = { - if (share.shareState == ShareState.DRAFT) { - onDismissDraft(share) - } else { - viewModel.setActiveShare(null) - } - }, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, + Column( + modifier = Modifier + .fillMaxSize() ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 32.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) + ModalBottomSheet( + onDismissRequest = { + if (share.shareState == ShareState.DRAFT) { + onDismissDraft(share) + } else { + viewModel.setActiveShare(null) + } + }, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, ) { - ShareTitle(share) + Text( + text = share.title(context), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(16.dp) + ) - CategorySelector( - categories = categories, - selectedCategory = selectedCategory, - onCategorySelected = { category -> - selectedCategory = category - viewModel.addAnyoneRecipient(category, share) + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + categories.forEachIndexed { index, category -> + SegmentedButton( + selected = selectedCategory == category, + onClick = { + selectedCategory = category + viewModel.addAnyoneRecipient(category, share) + }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), + icon = { + Icon(painter = painterResource(category.iconId), contentDescription = "") + } + ) { + Text(stringResource(category.titleId)) + } } - ) + } if (selectedCategory == ShareCategory.Invited) { RecipientSearchField(share, viewModel) @@ -122,7 +140,7 @@ fun AddOrEditShareBottomSheet( } if (share.readyToSend()) { - Buttons(share, selectedCategory, onSend = { + ActionButtons(share, selectedCategory, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) }) } @@ -131,7 +149,7 @@ fun AddOrEditShareBottomSheet( } @Composable -private fun Buttons( +private fun ActionButtons( share: Share, category: ShareCategory, onSend: () -> Unit = {}, @@ -172,7 +190,6 @@ private fun Buttons( } } - Button( onClick = onSend, modifier = Modifier.weight(1f), @@ -195,40 +212,6 @@ private fun Buttons( } } -@Composable -private fun ShareTitle(share: Share) { - val context = LocalContext.current - Text( - text = share.title(context), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 8.dp) - ) -} - -@Composable -private fun CategorySelector( - categories: List, - selectedCategory: ShareCategory, - onCategorySelected: (ShareCategory) -> Unit -) { - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - categories.forEachIndexed { index, category -> - SegmentedButton( - selected = selectedCategory == category, - onClick = { onCategorySelected(category) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), - icon = { - Icon(painter = painterResource(category.iconId), contentDescription = "") - } - ) { - Text(stringResource(category.titleId)) - } - } - } -} - - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PermissionCategories( @@ -269,6 +252,7 @@ private fun PermissionPresetDropdown( var expanded by remember { mutableStateOf(false) } ExposedDropdownMenuBox( + modifier = Modifier.padding(12.dp), expanded = expanded, onExpandedChange = { expanded = it } ) { @@ -323,3 +307,62 @@ private fun AdvancedSettingsSection( } } } + + +@Preview(showBackground = true) +@Composable +private fun AddOrEditShareBottomSheetPreview() { + MaterialTheme { + AddOrEditShareBottomSheet( + share = previewShare, + viewModel = ShareViewModel(MockShareRepository()) + ) + } +} + +private val previewIcon = + com.nextcloud.android.common.ui.share.model.api.icon.Icon(svg = "") + +private val previewUser = User( + userId = "alice", + displayName = "Alice Johnson", + icon = previewIcon +) + +private val previewShare = Share( + id = "preview", + owner = previewUser, + lastUpdated = 0L, + shareState = ShareState.DRAFT, + sources = listOf( + Source(clazz = "file", value = "/Photos/vacation.jpg", displayName = "vacation.jpg", icon = previewIcon) + ), + recipients = listOf( + Recipient( + clazz = "user", + value = "bob@example.com", + displayName = "Bob Smith", + icon = previewIcon, + secret = Secret(updatable = false, value = "", url = "https://example.com/s/abc123") + ) + ), + properties = listOf( + PropertyString(clazz = "note", displayName = "Note", priority = 10, required = false, value = "") + ), + permissions = listOf( + Permission( + clazz = "read", + displayName = "Read", + presets = listOf(PermissionPreset.VIEW, PermissionPreset.EDIT), + enabled = true + ), + Permission( + clazz = "update", + displayName = "Update", + presets = listOf(PermissionPreset.EDIT), + enabled = false + ) + ), + permissionPreset = null +) + From e4ef583908eba27d1245bcb26ea64df6c666355c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 16:03:28 +0200 Subject: [PATCH 067/174] wip Signed-off-by: alperozturk96 --- .../share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 4 ++-- .../common/ui/share/repository/ShareRemoteRepository.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index e0e842a1..e9f6a43d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -125,7 +125,7 @@ fun AddOrEditShareBottomSheet( RecipientSearchField(share, viewModel) } - PermissionCategories( + PermissionsView( share = share, viewModel = viewModel ) @@ -214,7 +214,7 @@ private fun ActionButtons( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun PermissionCategories( +private fun PermissionsView( share: Share, viewModel: ShareViewModel ) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index f0bcb16e..7e09a2f5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -182,7 +182,7 @@ class ShareRemoteRepository( request: UpdateSharePermissionRequest ): NetworkResult = client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id/enabled", + endpoint = "$SHARE_ENDPOINT/$id/permission", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) ) { body -> From af620d62e171313ac5625e817da7d92937abd260 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 16:04:46 +0200 Subject: [PATCH 068/174] wip Signed-off-by: alperozturk96 --- .../ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index e9f6a43d..0060e0e0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -212,6 +212,7 @@ private fun ActionButtons( } } +// FIXME: when toggles are selected recomposition happens @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PermissionsView( From 0518e06ef21ea69dcc38458425789be378f3d3f9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 08:11:50 +0200 Subject: [PATCH 069/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 1 - .../bottomsheet/AddOrEditShareBottomSheet.kt | 133 +++++++++--------- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index ec6992cb..a806bbd7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -299,7 +299,6 @@ class ShareViewModel( val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } replaceInList(updated) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 0060e0e0..49da357a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -148,71 +148,6 @@ fun AddOrEditShareBottomSheet( } } -@Composable -private fun ActionButtons( - share: Share, - category: ShareCategory, - onSend: () -> Unit = {}, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - val clipEntry = share.getClipEntry() - if (category == ShareCategory.Anyone && clipEntry != null) { - val localClipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - - Button( - onClick = { - scope.launch { - localClipboard.setClipEntry(clipEntry) - } - }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - painter = painterResource(R.drawable.ic_link), - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource(R.string.share_view_copy_action), - modifier = Modifier.padding(start = 8.dp), - ) - } - } - - Button( - onClick = onSend, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Send, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource(R.string.share_view_send_action), - modifier = Modifier.padding(start = 8.dp), - ) - } - } -} - -// FIXME: when toggles are selected recomposition happens @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PermissionsView( @@ -233,10 +168,12 @@ private fun PermissionsView( share.permissions.forEach { permission -> key(permission.clazz) { + var checked by remember { mutableStateOf(permission.enabled) } ShareSwitch( label = permission.displayName, - checked = permission.enabled, + checked = checked, onCheckedChange = { isChecked -> + checked = isChecked viewModel.updatePermission(share.id, permission.clazz, isChecked) } ) @@ -310,6 +247,70 @@ private fun AdvancedSettingsSection( } +@Composable +private fun ActionButtons( + share: Share, + category: ShareCategory, + onSend: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + val clipEntry = share.getClipEntry() + if (category == ShareCategory.Anyone && clipEntry != null) { + val localClipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + + Button( + onClick = { + scope.launch { + localClipboard.setClipEntry(clipEntry) + } + }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Icon( + painter = painterResource(R.drawable.ic_link), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.share_view_copy_action), + modifier = Modifier.padding(start = 8.dp), + ) + } + } + + Button( + onClick = onSend, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.share_view_send_action), + modifier = Modifier.padding(start = 8.dp), + ) + } + } +} + @Preview(showBackground = true) @Composable private fun AddOrEditShareBottomSheetPreview() { From 6aaa6c2aae7465aa65bed4975be743cf93cc8ffd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 08:21:17 +0200 Subject: [PATCH 070/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 82 ++++++++++--------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 49da357a..cfa8c934 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -15,7 +15,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +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.automirrored.filled.Send import androidx.compose.material3.Button @@ -96,53 +98,59 @@ fun AddOrEditShareBottomSheet( sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surface, ) { - Text( - text = share.title(context), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(16.dp) - ) + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { + Text( + text = share.title(context), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(16.dp) + ) - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - categories.forEachIndexed { index, category -> - SegmentedButton( - selected = selectedCategory == category, - onClick = { - selectedCategory = category - viewModel.addAnyoneRecipient(category, share) - }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), - icon = { - Icon(painter = painterResource(category.iconId), contentDescription = "") + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + categories.forEachIndexed { index, category -> + SegmentedButton( + selected = selectedCategory == category, + onClick = { + selectedCategory = category + viewModel.addAnyoneRecipient(category, share) + }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), + icon = { + Icon(painter = painterResource(category.iconId), contentDescription = "") + } + ) { + Text(stringResource(category.titleId)) } - ) { - Text(stringResource(category.titleId)) } } - } - - if (selectedCategory == ShareCategory.Invited) { - RecipientSearchField(share, viewModel) - } - PermissionsView( - share = share, - viewModel = viewModel - ) + if (selectedCategory == ShareCategory.Invited) { + RecipientSearchField(share, viewModel) + } - if (share.properties.isNotEmpty()) { - AdvancedSettingsSection( + PermissionsView( share = share, - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings }, viewModel = viewModel ) - } - if (share.readyToSend()) { - ActionButtons(share, selectedCategory, onSend = { - viewModel.updateState(share.id, ShareState.ACTIVE) - }) + if (share.properties.isNotEmpty()) { + AdvancedSettingsSection( + share = share, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + viewModel = viewModel + ) + } + + if (share.readyToSend()) { + ActionButtons(share, selectedCategory, onSend = { + viewModel.updateState(share.id, ShareState.ACTIVE) + }) + } } } } From 65c11b9e44d8f3ca4a66d5a4b4735e3203ad37bc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 08:27:45 +0200 Subject: [PATCH 071/174] wip Signed-off-by: alperozturk96 --- .../ui/network/http/NextcloudHttpClient.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 5294b8fd..2475bb45 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -67,15 +67,22 @@ class NextcloudHttpClient private constructor( try { val response = okHttpClient.newCall(request).execute() val responseBody = response.body.string() - if (response.isSuccessful) { - val envelope = OCSSerializer.json.decodeFromString>(responseBody) - if (envelope.ocs.meta.status != OCS_OK) { - NetworkResult.ServerError(OcsResponse(Ocs(envelope.ocs.meta, envelope.ocs.meta.message))) - } else { - NetworkResult.Success(parse(responseBody)) - } + + if (!response.isSuccessful) { + return@withContext NetworkResult.ServerError( + OCSSerializer.json.decodeFromString>(responseBody) + ) + } + + if (responseBody.isBlank()) { + return@withContext NetworkResult.Success(parse(responseBody)) + } + + val envelope = OCSSerializer.json.decodeFromString>(responseBody) + if (envelope.ocs.meta.status != OCS_OK) { + NetworkResult.ServerError(OcsResponse(Ocs(envelope.ocs.meta, envelope.ocs.meta.message))) } else { - NetworkResult.ServerError(OCSSerializer.json.decodeFromString>(responseBody)) + NetworkResult.Success(parse(responseBody)) } } catch (e: Exception) { NetworkResult.fromException(e) From a292c38b84f261fdc7831893bfe9b079b2061c7a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 08:34:37 +0200 Subject: [PATCH 072/174] wip Signed-off-by: alperozturk96 --- ...SearchField.kt => SelectRecipientField.kt} | 164 +++++++----------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 4 +- ui/src/main/res/values/strings.xml | 4 +- 3 files changed, 65 insertions(+), 107 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/{RecipientSearchField.kt => SelectRecipientField.kt} (55%) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt similarity index 55% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index e7d8a84c..82bf6618 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientSearchField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -7,15 +7,10 @@ package com.nextcloud.android.common.ui.share.component -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material3.DropdownMenuItem @@ -24,7 +19,6 @@ import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.InputChip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -34,7 +28,6 @@ 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.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -48,134 +41,97 @@ import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun RecipientSearchField( +fun SelectRecipientField( share: Share, viewModel: ShareViewModel ) { - var query by remember { mutableStateOf("") } - var expanded by remember { mutableStateOf(false) } - val results by viewModel.recipientSearchResults.collectAsState() - val chipScrollState = rememberScrollState() + val selectedRecipient = share.recipients.firstOrNull { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } - Column( - modifier = Modifier.fillMaxWidth().padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (share.recipients.isNotEmpty()) { - RecipientChipRow( - recipients = share.recipients, - chipScrollState = chipScrollState, - onRemove = { recipient -> - viewModel.removeRecipient( - id = share.id, - clazz = recipient.clazz, - value = recipient.value, - instance = recipient.instance - ) - } - ) - } + if (selectedRecipient == null) { + RecipientSearch(share, viewModel) + return + } - RecipientSearchDropdown( - enabled = share.recipients.isEmpty(), - query = query, - expanded = expanded, - onQueryChange = { - query = it - expanded = true - viewModel.onSearchQueryChanged(it) - }, - onExpandedChange = { expanded = it }, - onDismiss = { expanded = false } - ) { - RecipientDropdownContent( - results = results, - onSelect = { recipient -> - viewModel.addRecipient(share.id, recipient.clazz, recipient.value) - query = "" - expanded = false - } + SelectedRecipient( + recipient = selectedRecipient, + onClear = { + viewModel.removeRecipient( + id = share.id, + clazz = selectedRecipient.clazz, + value = selectedRecipient.value, + instance = selectedRecipient.instance ) } - } + ) } @Composable -private fun RecipientChipRow( - recipients: List, - chipScrollState: androidx.compose.foundation.ScrollState, - onRemove: (Recipient) -> Unit -) { - Row( +private fun SelectedRecipient(recipient: Recipient, onClear: () -> Unit) { + OutlinedTextField( + value = recipient.displayName, + onValueChange = {}, + readOnly = true, + singleLine = true, modifier = Modifier .fillMaxWidth() - .horizontalScroll(chipScrollState), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - recipients.forEach { recipient -> - InputChip( - selected = true, - onClick = { }, - label = { Text(recipient.displayName) }, - leadingIcon = { - recipient.icon?.let { - RecipientIcon( - icon = it, - modifier = Modifier.size(18.dp) - ) - } - }, - trailingIcon = { - IconButton( - onClick = { onRemove(recipient) }, - modifier = Modifier.size(16.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "remove recipient" - ) - } - } - ) + .padding(12.dp), + label = { Text(stringResource(R.string.share_view_select_recipient_label)) }, + leadingIcon = { + recipient.icon?.let { RecipientIcon(icon = it, modifier = Modifier.size(24.dp)) } + }, + trailingIcon = { + IconButton(onClick = onClear) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.share_view_select_recipient_clear) + ) + } } - } + ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RecipientSearchDropdown( - enabled: Boolean, - query: String, - expanded: Boolean, - onQueryChange: (String) -> Unit, - onExpandedChange: (Boolean) -> Unit, - onDismiss: () -> Unit, - content: @Composable () -> Unit -) { +private fun RecipientSearch(share: Share, viewModel: ShareViewModel) { + var query by remember { mutableStateOf("") } + var expanded by remember { mutableStateOf(false) } + val results by viewModel.recipientSearchResults.collectAsState() + ExposedDropdownMenuBox( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), expanded = expanded && query.isNotBlank(), - onExpandedChange = onExpandedChange + onExpandedChange = { expanded = it } ) { OutlinedTextField( value = query, - onValueChange = onQueryChange, - label = { Text(stringResource(R.string.share_view_invited_category_label)) }, - enabled = enabled, + onValueChange = { + query = it + expanded = true + viewModel.onSearchQueryChanged(it) + }, + label = { Text(stringResource(R.string.share_view_select_recipient_label)) }, + singleLine = true, modifier = Modifier .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true) - .fillMaxWidth(), - singleLine = true + .fillMaxWidth() ) if (query.isNotBlank()) { ExposedDropdownMenu( expanded = expanded, - onDismissRequest = onDismiss + onDismissRequest = { expanded = false } ) { - content() + RecipientDropdownContent( + results = results, + onSelect = { recipient -> + viewModel.addRecipient(share.id, recipient.clazz, recipient.value) + query = "" + expanded = false + } + ) } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index cfa8c934..525c6a1a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -53,7 +53,7 @@ import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection -import com.nextcloud.android.common.ui.share.component.RecipientSearchField +import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView import com.nextcloud.android.common.ui.share.model.api.permission.Permission @@ -129,7 +129,7 @@ fun AddOrEditShareBottomSheet( } if (selectedCategory == ShareCategory.Invited) { - RecipientSearchField(share, viewModel) + SelectRecipientField(share, viewModel) } PermissionsView( diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 13c62abe..7b545735 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -13,7 +13,9 @@ Share %s Settings - Add people + + Select recipient + Clear recipient Not shared No result From a7efa9dd9503be180b4b2a71ca7f25cfe92c649d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 08:36:28 +0200 Subject: [PATCH 073/174] wip Signed-off-by: alperozturk96 --- ui/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 7b545735..607d17f5 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ Settings - Select recipient + Recipient Clear recipient Not shared From dc797104f20afcf70be43f58a46dc7f41dabe827 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 09:06:38 +0200 Subject: [PATCH 074/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 87 ++++++------------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 25 +++++- .../property/SharePropertyEnumField.kt | 75 ++++++++++++++++ .../component/property/SharePropertyView.kt | 42 ++++----- .../property/datepicker/ShareDatePicker.kt | 9 +- .../datepicker/util/ShareDateFormatter.kt | 16 +++- ui/src/main/res/values/strings.xml | 1 + 7 files changed, 164 insertions(+), 91 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index a806bbd7..62d57419 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -30,9 +30,6 @@ import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -50,8 +47,8 @@ class ShareViewModel( ) : ViewModel() { companion object { - private const val SEARCH_DEBOUNCE_DELAY_MS = 300L - private const val PROPERTY_DEBOUNCE_DELAY_MS = 1000L + private const val SEARCH_DEBOUNCE_DELAY = 300L + private const val PROPERTY_DEBOUNCE_DELAY = 1000L } private val _state = MutableStateFlow(ShareScreenState.Loading) @@ -68,12 +65,10 @@ class ShareViewModel( private val _errorMessageId = MutableStateFlow(null) val errorMessageId: StateFlow = _errorMessageId - private data class PendingPropertyUpdate(val job: Job, val shareId: String, val value: String) + private val _propertyErrors = MutableStateFlow>(emptyMap()) + val propertyErrors: StateFlow> = _propertyErrors - private val pendingPropertyJobs = mutableMapOf() - - private val _propertyErrors = MutableStateFlow>(emptyMap()) - val propertyErrors: StateFlow> = _propertyErrors + private val propertyUpdateJobs = mutableMapOf() private val currentShares: List get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() @@ -88,7 +83,7 @@ class ShareViewModel( private fun initSearchQuery() { viewModelScope.launch { _searchQuery - .debounce(SEARCH_DEBOUNCE_DELAY_MS.milliseconds) + .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) .distinctUntilChanged() .filter { it.isNotBlank() } .collect { query -> executeSearch(query) } @@ -160,11 +155,8 @@ class ShareViewModel( // region state fun updateState(id: String, shareState: ShareState) { - viewModelScope.launch { - flushPendingProperties(id) - val result = withContext(Dispatchers.IO) { - repository.updateShareState(id, UpdateShareStateRequest(shareState)) - } + viewModelScope.launch(Dispatchers.IO) { + val result = repository.updateShareState(id, UpdateShareStateRequest(shareState)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch if (shareState == ShareState.ACTIVE) { @@ -243,51 +235,24 @@ class ShareViewModel( // endregion // region properties - fun updateProperty(id: String, clazz: String, value: String?) { - viewModelScope.launch(Dispatchers.IO) { - val result = repository.updateShareProperty(id, UpdateSharePropertyRequest(clazz, value)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch - _activeShare.update { updated } - replaceInList(updated) - } - } - - fun updatePropertyDebounced(shareId: String, clazz: String, value: String) { - pendingPropertyJobs[clazz]?.job?.cancel() - val job = viewModelScope.launch { - delay(PROPERTY_DEBOUNCE_DELAY_MS.milliseconds) - pendingPropertyJobs.remove(clazz) - withContext(Dispatchers.IO) { executePropertyUpdate(shareId, clazz, value) } - } - pendingPropertyJobs[clazz] = PendingPropertyUpdate(job, shareId, value) - } - - private suspend fun flushPendingProperties(shareId: String) { - val snapshot = pendingPropertyJobs.toMap() - snapshot.values.forEach { it.job.cancel() } - pendingPropertyJobs.clear() - if (snapshot.isEmpty()) return - withContext(Dispatchers.IO) { - coroutineScope { - snapshot.entries.map { (clazz, pending) -> - async { executePropertyUpdate(shareId, clazz, pending.value) } - }.awaitAll() - } - } - } - - private suspend fun executePropertyUpdate(shareId: String, clazz: String, value: String) { - when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { - is NetworkResult.Success -> { - _propertyErrors.update { it - clazz } - _activeShare.update { result.data } - replaceInList(result.data) - } - - is NetworkResult.ServerError, - is NetworkResult.NetworkException -> { - _propertyErrors.update { it + (clazz to R.string.share_view_update_error_message) } + fun updateProperty(shareId: String, clazz: String, value: String?) { + propertyUpdateJobs[clazz]?.cancel() + propertyUpdateJobs[clazz] = viewModelScope.launch(Dispatchers.IO) { + delay(PROPERTY_DEBOUNCE_DELAY.milliseconds) + when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { + is NetworkResult.Success -> { + _propertyErrors.update { it - clazz } + _activeShare.update { result.data } + replaceInList(result.data) + } + + is NetworkResult.ServerError -> { + _propertyErrors.update { it + (clazz to result.response.ocs.meta.message) } + } + + is NetworkResult.NetworkException -> { + _propertyErrors.update { it + (clazz to null) } + } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 525c6a1a..a4bb3072 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf @@ -82,6 +83,8 @@ fun AddOrEditShareBottomSheet( var selectedCategory by remember { mutableStateOf(categories.first()) } var showAdvancedSettings by remember { mutableStateOf(false) } val context = LocalContext.current + val propertyErrors by viewModel.propertyErrors.collectAsState() + val hasPropertyErrors = propertyErrors.values.any { it != null } Column( modifier = Modifier @@ -146,10 +149,24 @@ fun AddOrEditShareBottomSheet( ) } + if (hasPropertyErrors) { + Text( + text = stringResource(R.string.share_view_property_error_warning), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + if (share.readyToSend()) { - ActionButtons(share, selectedCategory, onSend = { - viewModel.updateState(share.id, ShareState.ACTIVE) - }) + ActionButtons( + share = share, + category = selectedCategory, + sendEnabled = !hasPropertyErrors, + onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } + ) } } } @@ -259,6 +276,7 @@ private fun AdvancedSettingsSection( private fun ActionButtons( share: Share, category: ShareCategory, + sendEnabled: Boolean, onSend: () -> Unit, ) { Row( @@ -299,6 +317,7 @@ private fun ActionButtons( Button( onClick = onSend, + enabled = sendEnabled, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt new file mode 100644 index 00000000..301bd373 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt @@ -0,0 +1,75 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component.property + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +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.Modifier +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SharePropertyEnumField( + property: PropertyEnum, + errorMessage: String?, + onValueSelected: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + var selectedValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } + + ExposedDropdownMenuBox( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + value = selectedValue, + onValueChange = {}, + readOnly = true, + singleLine = true, + label = { Text(property.displayName) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(it) } }, + modifier = Modifier + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true) + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + property.validValues.forEach { value -> + DropdownMenuItem( + text = { Text(value) }, + onClick = { + selectedValue = value + expanded = false + onValueSelected(value) + } + ) + } + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index ca482a15..a5318ddb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp @@ -38,7 +37,12 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyString @Composable fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { val propertyErrors by viewModel.propertyErrors.collectAsState() - val hasError = propertyErrors[property.clazz] != null + val fallbackError = stringResource(R.string.share_view_update_error_message) + val errorMessage = if (propertyErrors.containsKey(property.clazz)) { + propertyErrors[property.clazz] ?: fallbackError + } else { + null + } when (property) { is PropertyBoolean -> { @@ -50,12 +54,12 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM checked = checkedValue, onCheckedChange = { isChecked -> checkedValue = isChecked - viewModel.updatePropertyDebounced(shareId, property.clazz, isChecked.toString()) + viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) } ) - if (hasError) { + if (errorMessage != null) { Text( - text = stringResource(R.string.share_view_update_error_message), + text = errorMessage, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 4.dp, bottom = 4.dp) @@ -71,15 +75,11 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM value = textValue, onValueChange = { textValue = it - viewModel.updatePropertyDebounced(shareId, property.clazz, it) + viewModel.updateProperty(shareId, property.clazz, it) }, label = { Text(property.displayName) }, - isError = hasError, - supportingText = if (hasError) { - { Text(stringResource(R.string.share_view_update_error_message)) } - } else { - null - }, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(it) } }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), singleLine = true ) @@ -92,17 +92,13 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM value = passwordValue, onValueChange = { passwordValue = it - viewModel.updatePropertyDebounced(shareId, property.clazz, it) + viewModel.updateProperty(shareId, property.clazz, it) }, label = { Text(property.displayName) }, placeholder = property.hint?.let { { Text(it) } }, visualTransformation = PasswordVisualTransformation(), - isError = hasError, - supportingText = if (hasError) { - { Text(stringResource(R.string.share_view_update_error_message)) } - } else { - null - }, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(it) } }, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp), @@ -111,13 +107,17 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM } is PropertyDate -> { - ShareDatePicker(property, onDateSelected = { dateValue -> + ShareDatePicker(property, errorMessage = errorMessage, onDateSelected = { dateValue -> viewModel.updateProperty(shareId, property.clazz, dateValue) }) } is PropertyEnum -> { - Text(text = "Enum Property: ${property.displayName} (Under Construction)", color = Color.Gray) + SharePropertyEnumField( + property = property, + errorMessage = errorMessage, + onValueSelected = { value -> viewModel.updateProperty(shareId, property.clazz, value) } + ) } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 186b9238..0e8c76c9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -32,10 +32,10 @@ import com.nextcloud.android.common.ui.share.component.property.datepicker.util. import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate @Composable -fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { - val formatter = ShareDateFormatter() +fun ShareDatePicker(property: PropertyDate, errorMessage: String?, onDateSelected: (String) -> Unit) { + val formatter = remember { ShareDateFormatter() } var showDatePicker by remember { mutableStateOf(false) } - var dateValue by remember { mutableStateOf(property.value ?: "") } + var dateValue by remember(property.clazz) { mutableStateOf(formatter.formatIsoForDisplay(property.value)) } Box(modifier = Modifier.fillMaxWidth()) { OutlinedTextField( @@ -43,6 +43,8 @@ fun ShareDatePicker(property: PropertyDate, onDateSelected: (String) -> Unit) { onValueChange = {}, readOnly = true, enabled = true, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(it) } }, modifier = Modifier.fillMaxWidth(), label = { Text(formatter.getDisplayName(property)) }, trailingIcon = { @@ -117,6 +119,7 @@ private fun ShareDatePickerPreview() { minDate = "2026-01-01", maxDate = "2027-12-31" ), + errorMessage = null, onDateSelected = {} ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt index b63e69f9..8e766bfb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt @@ -11,6 +11,8 @@ import androidx.compose.material3.DatePickerState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.getSelectedDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import java.time.LocalDate +import java.time.OffsetDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -18,17 +20,25 @@ import java.util.Locale class ShareDateFormatter { + private val displayDateFormatter: DateTimeFormatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.getDefault()) + fun getDisplayName(property: PropertyDate): String = property.displayName @OptIn(ExperimentalMaterial3Api::class) fun formatDisplayDate(datePickerState: DatePickerState): String? = - datePickerState.getSelectedDate()?.format( - DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.getDefault()) - ) + datePickerState.getSelectedDate()?.format(displayDateFormatter) @OptIn(ExperimentalMaterial3Api::class) fun formatIso8601Date(datePickerState: DatePickerState): String? = datePickerState.getSelectedDate() ?.atStartOfDay(ZoneId.systemDefault()) ?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + fun formatIsoForDisplay(isoDate: String?): String { + if (isoDate.isNullOrBlank()) return "" + return runCatching { OffsetDateTime.parse(isoDate).toLocalDate().format(displayDateFormatter) } + .recoverCatching { LocalDate.parse(isoDate).format(displayDateFormatter) } + .getOrDefault(isoDate) + } } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 607d17f5..dbeb63e9 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ Failed to delete share. Failed to create share. Failed to update share. + Some settings couldn\'t be saved. Fix the highlighted fields before sharing. Participants Can… From 5f0722f52b335f3f2951767b0616fabddf9a5d8a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 09:25:02 +0200 Subject: [PATCH 075/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 21 ++++++++++ .../ui/share/component/RecipientTokenField.kt | 41 +++++++++++++++++++ .../bottomsheet/AddOrEditShareBottomSheet.kt | 16 ++++++-- .../common/ui/share/model/api/share/Share.kt | 11 ++--- ui/src/main/res/values/strings.xml | 1 + 5 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 62d57419..ceabfb13 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -20,6 +20,7 @@ import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPres import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState @@ -70,6 +71,8 @@ class ShareViewModel( private val propertyUpdateJobs = mutableMapOf() + private var secretUpdateJob: Job? = null + private val currentShares: List get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() @@ -232,6 +235,24 @@ class ShareViewModel( replaceInList(updated) } } + + fun updateRecipientSecret(shareId: String, recipient: Recipient, secret: String) { + secretUpdateJob?.cancel() + secretUpdateJob = viewModelScope.launch(Dispatchers.IO) { + delay(PROPERTY_DEBOUNCE_DELAY.milliseconds) + val request = UpdateShareRecipientSecretRequest( + clazz = recipient.clazz, + value = recipient.value, + instance = recipient.instance, + secret = secret + ) + val result = repository.updateShareRecipientSecret(shareId, request) + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch + _activeShare.update { updated } + replaceInList(updated) + } + } // endregion // region properties diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt new file mode 100644 index 00000000..11a7948d --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient + +@Composable +fun RecipientTokenField(recipient: Recipient, onTokenChange: (String) -> Unit) { + var token by remember(recipient.value) { mutableStateOf(recipient.secret.value ?: "") } + + OutlinedTextField( + value = token, + onValueChange = { + token = it + onTokenChange(it) + }, + label = { Text(stringResource(R.string.share_view_link_token_label)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index a4bb3072..adbbcc58 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -54,6 +54,7 @@ import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection +import com.nextcloud.android.common.ui.share.component.RecipientTokenField import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView @@ -135,6 +136,17 @@ fun AddOrEditShareBottomSheet( SelectRecipientField(share, viewModel) } + // TODO: is it going to be included for now? + val editableRecipient = share.recipients.firstOrNull { it.secret.updatable } + if (editableRecipient != null) { + RecipientTokenField( + recipient = editableRecipient, + onTokenChange = { token -> + viewModel.updateRecipientSecret(share.id, editableRecipient, token) + } + ) + } + PermissionsView( share = share, viewModel = viewModel @@ -163,7 +175,6 @@ fun AddOrEditShareBottomSheet( if (share.readyToSend()) { ActionButtons( share = share, - category = selectedCategory, sendEnabled = !hasPropertyErrors, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } ) @@ -275,7 +286,6 @@ private fun AdvancedSettingsSection( @Composable private fun ActionButtons( share: Share, - category: ShareCategory, sendEnabled: Boolean, onSend: () -> Unit, ) { @@ -286,7 +296,7 @@ private fun ActionButtons( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { val clipEntry = share.getClipEntry() - if (category == ShareCategory.Anyone && clipEntry != null) { + if (clipEntry != null) { val localClipboard = LocalClipboard.current val scope = rememberCoroutineScope() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 435c20f5..33c22c8b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -45,15 +45,12 @@ data class Share( @SerialName("permission_preset") val permissionPreset: PermissionPreset? = null ) { + // FIXME: secret.url is null fun getClipEntry(): ClipEntry? { - val label = recipients.first().displayName - val link = recipients.firstOrNull()?.secret?.url + val recipient = recipients.firstOrNull() ?: return null + val link = recipient.secret.url ?: return null - return if (link != null) { - ClipData.newPlainText(label, link).toClipEntry() - } else { - null - } + return ClipData.newPlainText(recipient.displayName, link).toClipEntry() } fun readyToSend(): Boolean = diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index dbeb63e9..f85d009d 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ Settings Recipient + Link token Clear recipient Not shared From e2543faec781e7d985213c049c868cb9a3d59000 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 09:37:58 +0200 Subject: [PATCH 076/174] wip Signed-off-by: alperozturk96 --- .../android/common/sample/MainViewModel.kt | 2 +- ui/build.gradle | 1 + .../ui/network/http/NextcloudHttpClient.kt | 24 ++--- .../network/http/NextcloudHttpClientLogger.kt | 92 +++++++++++++++++++ 4 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 4744a557..58e6bdf4 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -27,7 +27,7 @@ class MainViewModel : ViewModel() { ) { viewModelScope.launch { val credentials = ServerCredentials(baseUrl, username, token) - val client = NextcloudHttpClient.create(credentials, enableLogging = true) + val client = NextcloudHttpClient.create(credentials) val service = UserStatusRepository(client) when (val result = service.fetchPredefinedStatuses()) { diff --git a/ui/build.gradle b/ui/build.gradle index bf833022..7a9745c0 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -24,6 +24,7 @@ android { buildFeatures { compose = true + buildConfig = true } compileOptions { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 2475bb45..29b462e6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.network.http +import com.nextcloud.android.common.ui.BuildConfig import com.nextcloud.android.common.ui.network.auth.AuthInterceptor import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.model.NetworkResult @@ -18,12 +19,12 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonElement import okhttp3.OkHttpClient import okhttp3.RequestBody -import okhttp3.logging.HttpLoggingInterceptor import java.util.concurrent.TimeUnit class NextcloudHttpClient private constructor( val okHttpClient: OkHttpClient, - val credentials: ServerCredentials + val credentials: ServerCredentials, + private val debugLogger: NextcloudHttpClientLogger ) { companion object { private const val CONNECT_TIMEOUT_SECONDS = 90L @@ -32,26 +33,16 @@ class NextcloudHttpClient private constructor( private const val OCS_OK = "ok" fun create( - credentials: ServerCredentials, - enableLogging: Boolean = false + credentials: ServerCredentials ): NextcloudHttpClient { - val loggingInterceptor = HttpLoggingInterceptor().apply { - level = if (enableLogging) { - HttpLoggingInterceptor.Level.BODY - } else { - HttpLoggingInterceptor.Level.NONE - } - } - val okHttpClient = OkHttpClient.Builder() .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) .addInterceptor(AuthInterceptor(credentials)) - .addInterceptor(loggingInterceptor) .build() - return NextcloudHttpClient(okHttpClient, credentials) + return NextcloudHttpClient(okHttpClient, credentials, NextcloudHttpClientLogger(BuildConfig.DEBUG)) } } @@ -69,22 +60,27 @@ class NextcloudHttpClient private constructor( val responseBody = response.body.string() if (!response.isSuccessful) { + debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = true) return@withContext NetworkResult.ServerError( OCSSerializer.json.decodeFromString>(responseBody) ) } if (responseBody.isBlank()) { + debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = false) return@withContext NetworkResult.Success(parse(responseBody)) } val envelope = OCSSerializer.json.decodeFromString>(responseBody) if (envelope.ocs.meta.status != OCS_OK) { + debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = true) NetworkResult.ServerError(OcsResponse(Ocs(envelope.ocs.meta, envelope.ocs.meta.message))) } else { + debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = false) NetworkResult.Success(parse(responseBody)) } } catch (e: Exception) { + debugLogger.logException(endpoint, method, e) NetworkResult.fromException(e) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt new file mode 100644 index 00000000..98d66e0c --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt @@ -0,0 +1,92 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.network.http + +import android.util.Log +import kotlinx.serialization.json.Json +import okhttp3.RequestBody +import okio.Buffer + +internal class NextcloudHttpClientLogger(private val enabled: Boolean) { + + companion object { + private const val TAG = "NextcloudHttpClient" + + private val prettyJson = Json { + prettyPrint = true + prettyPrintIndent = " " + ignoreUnknownKeys = true + } + } + + fun logRequest(endpoint: String, method: HttpMethod, body: RequestBody?) { + if (!enabled) return + + Log.d( + TAG, + """ + |➡️ REQUEST [$method] $endpoint + |Body: + |${body.readableString().prettyPrintJsonOrRaw()} + """.trimMargin() + ) + } + + fun logResponse( + endpoint: String, + method: HttpMethod, + code: Int, + responseBody: String, + isError: Boolean + ) { + if (!enabled) return + + Log.d( + TAG, + """ + |${statusEmojiFor(code, isError)} RESPONSE [$method] $endpoint (HTTP $code) + |Body: + |${responseBody.ifBlank { "" }.prettyPrintJsonOrRaw()} + """.trimMargin() + ) + } + + fun logException(endpoint: String, method: HttpMethod, e: Exception) { + if (!enabled) return + + Log.e(TAG, "💥 EXCEPTION [$method] $endpoint -> ${e.javaClass.simpleName}: ${e.message}", e) + } + + private fun statusEmojiFor(code: Int, isError: Boolean): String = when { + isError -> "🔴" + code in 200..299 -> "🟢" + code in 300..399 -> "🟡" + else -> "🔴" + } + + private fun RequestBody?.readableString(): String { + if (this == null) return "" + return try { + val buffer = Buffer() + writeTo(buffer) + buffer.readUtf8() + } catch (e: Exception) { + "" + } + } + + private fun String.prettyPrintJsonOrRaw(): String { + if (this.isBlank() || this == "" || this == "") return this + return try { + val element = Json.parseToJsonElement(this) + prettyJson.encodeToString(kotlinx.serialization.json.JsonElement.serializer(), element) + } catch (_: Exception) { + this + } + } +} From 30d3826faba2853dd3384ba67395a679c85b767f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 12:24:44 +0200 Subject: [PATCH 077/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 80 ++++++++++++++++--- .../QuickSharePermissionBottomSheet.kt | 76 ++++++++++++++++++ .../share/model/ui/ShareItemOverlayState.kt | 1 + ui/src/main/res/values/strings.xml | 1 + 4 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index ccd48760..aaaac389 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -8,16 +8,23 @@ package com.nextcloud.android.common.ui.share import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme @@ -62,9 +69,12 @@ import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.component.RecipientIcon import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet +import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePermissionBottomSheet import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState @@ -117,12 +127,14 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti CircularProgressIndicator() } } + is ShareScreenState.Empty -> { ContentUnavailableView( iconId = R.drawable.ic_person_add, title = stringResource(R.string.share_view_empty_title), ) } + is ShareScreenState.Loaded -> { LazyColumn( modifier = Modifier @@ -143,6 +155,9 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti share = share, type = type, onSelectShare = { selected -> viewModel.setActiveShare(selected) }, + onChangePreset = { selected, preset -> + viewModel.updatePermissionPreset(selected.id, preset) + }, onDeleteShare = { viewModel.deleteShare(it.id) }, onSendEmail = { } ) @@ -169,20 +184,41 @@ private fun ShareItem( share: Share, type: ShareItemType, onSelectShare: (Share) -> Unit, + onChangePreset: (Share, PermissionPreset) -> Unit, onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit ) { var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) } val haptics = LocalHapticFeedback.current - if (overlayState == ShareItemOverlayState.DeleteConfirmation) { - DeleteShareConfirmationDialog( - onConfirm = { - overlayState = ShareItemOverlayState.None - onDeleteShare(share) - }, - onDismiss = { overlayState = ShareItemOverlayState.None } - ) + when (overlayState) { + is ShareItemOverlayState.QuickShare -> { + QuickSharePermissionBottomSheet( + selectedOption = PermissionPresetOption.from(share.permissionPreset), + onOptionSelected = { option -> + overlayState = ShareItemOverlayState.None + val preset = option.preset + if (preset != null) { + onChangePreset(share, preset) + } else { + onSelectShare(share) + } + }, + onDismiss = { overlayState = ShareItemOverlayState.None } + ) + } + + is ShareItemOverlayState.DeleteConfirmation -> { + DeleteShareConfirmationDialog( + onConfirm = { + overlayState = ShareItemOverlayState.None + onDeleteShare(share) + }, + onDismiss = { overlayState = ShareItemOverlayState.None } + ) + } + + else -> Unit } ListItem( @@ -213,11 +249,20 @@ private fun ShareItem( share.recipients.first().icon?.let { RecipientIcon(icon = it) } }, supportingContent = { - Text( - text = share.shareState.name, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Row( + modifier = Modifier.clickable { overlayState = ShareItemOverlayState.QuickShare }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(PermissionPresetOption.from(share.permissionPreset).labelRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.width(4.dp)) + + Icon(Icons.Default.KeyboardArrowDown, contentDescription = "Down arrow") + } + }, trailingContent = { Box { @@ -230,6 +275,9 @@ private fun ShareItem( onDismissRequest = { overlayState = ShareItemOverlayState.None } ) { DropdownMenuItem( + leadingIcon = { + Icon(Icons.Default.Edit, contentDescription = "Edit icon") + }, text = { Text(stringResource(R.string.share_view_list_item_edit)) }, onClick = { overlayState = ShareItemOverlayState.None @@ -237,6 +285,9 @@ private fun ShareItem( } ) DropdownMenuItem( + leadingIcon = { + Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send icon") + }, text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, onClick = { onSendEmail(share) @@ -245,6 +296,9 @@ private fun ShareItem( ) HorizontalDivider() DropdownMenuItem( + leadingIcon = { + Icon(Icons.Default.Delete, contentDescription = "Delete icon") + }, text = { Text( stringResource(R.string.share_view_list_item_delete), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt new file mode 100644 index 00000000..0fdbbe75 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component.bottomsheet + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun QuickSharePermissionBottomSheet( + selectedOption: PermissionPresetOption, + onOptionSelected: (PermissionPresetOption) -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp) + ) { + Text( + text = stringResource(R.string.share_view_quick_permission_title), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(16.dp) + ) + + PermissionPresetOption.entries.forEach { option -> + ListItem( + modifier = Modifier.clickable { onOptionSelected(option) }, + headlineContent = { Text(stringResource(option.labelRes)) }, + trailingContent = { + if (option == selectedOption) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) + } + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt index 0e50bb29..0b47eff0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt @@ -10,5 +10,6 @@ package com.nextcloud.android.common.ui.share.model.ui sealed class ShareItemOverlayState { data object None : ShareItemOverlayState() data object ContextMenu : ShareItemOverlayState() + data object QuickShare : ShareItemOverlayState() data object DeleteConfirmation : ShareItemOverlayState() } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index f85d009d..0d432dd0 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ Failed to update share. Some settings couldn\'t be saved. Fix the highlighted fields before sharing. + Set permission Participants Can… Can view From 413ee6bd4340ee2541c0aeac650dc2e4b9222492 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 12:43:33 +0200 Subject: [PATCH 078/174] wip Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareScreen.kt | 4 ++-- .../com/nextcloud/android/common/ui/share/ShareViewModel.kt | 6 ++++-- .../component/bottomsheet/AddOrEditShareBottomSheet.kt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index aaaac389..05d050c4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -156,10 +156,10 @@ private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabiliti type = type, onSelectShare = { selected -> viewModel.setActiveShare(selected) }, onChangePreset = { selected, preset -> - viewModel.updatePermissionPreset(selected.id, preset) + viewModel.updatePermissionPreset(selected.id, preset, updateActiveShare = false) }, onDeleteShare = { viewModel.deleteShare(it.id) }, - onSendEmail = { } + onSendEmail = { selected -> viewModel.setActiveShare(selected) } // TODO: is that right? ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index ceabfb13..3fbceb82 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -289,12 +289,14 @@ class ShareViewModel( } } - fun updatePermissionPreset(id: String, preset: PermissionPreset) { + fun updatePermissionPreset(id: String, preset: PermissionPreset, updateActiveShare: Boolean) { viewModelScope.launch(Dispatchers.IO) { val result = repository.updateSharePermissionPreset(id, UpdateSharePermissionPresetRequest(preset)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } + if (updateActiveShare) { + _activeShare.update { updated } + } replaceInList(updated) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index adbbcc58..2472f823 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -196,7 +196,7 @@ private fun PermissionsView( selectedOption = PermissionPresetOption.from(selectedPreset), onOptionSelected = { option -> selectedPreset = option.preset - option.preset?.let { viewModel.updatePermissionPreset(share.id, it) } + option.preset?.let { viewModel.updatePermissionPreset(share.id, it, true) } } ) From d29d1ce58cc0751af51989c970469bf839fd989c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 12:44:12 +0200 Subject: [PATCH 079/174] wip Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareScreen.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 05d050c4..1d7b1e83 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -84,7 +84,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @Composable -private fun ShareScreen(sourceId: String, sharingCapabilities: SharingCapabilities, viewModel: ShareViewModel) { +private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() val screenState by viewModel.state.collectAsState() val activeShare by viewModel.activeShare.collectAsState() @@ -316,15 +316,11 @@ private fun ShareItem( ) } -private val json = Json { ignoreUnknownKeys = true } - fun ComposeView.initShareScreen( sourceId: String, - sharingJson: String, credentials: ServerCredentials, colorScheme: ColorScheme ) { - val sharingCapabilities = json.decodeFromString(sharingJson) val nextcloudHttpClient = NextcloudHttpClient.create(credentials) val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) @@ -338,7 +334,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(sourceId, sharingCapabilities, viewModel) + ShareScreen(sourceId, viewModel) } ) } From b27db97b053d943777a98065f69ab363ff7a644a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 12:51:00 +0200 Subject: [PATCH 080/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 21 ++++++++++++++----- .../bottomsheet/AddOrEditShareBottomSheet.kt | 7 ++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 1d7b1e83..b1909300 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -71,7 +71,6 @@ import com.nextcloud.android.common.ui.share.component.RecipientIcon import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePermissionBottomSheet import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog -import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption @@ -81,7 +80,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json @Composable private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { @@ -91,6 +89,7 @@ private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current + var editorInitialPreset by remember { mutableStateOf(null) } LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -154,12 +153,22 @@ private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { ShareItem( share = share, type = type, - onSelectShare = { selected -> viewModel.setActiveShare(selected) }, + onSelectShare = { selected -> + editorInitialPreset = null + viewModel.setActiveShare(selected) + }, + onCustomizeShare = { selected -> + editorInitialPreset = PermissionPresetOption.CUSTOM + viewModel.setActiveShare(selected) + }, onChangePreset = { selected, preset -> viewModel.updatePermissionPreset(selected.id, preset, updateActiveShare = false) }, onDeleteShare = { viewModel.deleteShare(it.id) }, - onSendEmail = { selected -> viewModel.setActiveShare(selected) } // TODO: is that right? + onSendEmail = { selected -> + editorInitialPreset = null + viewModel.setActiveShare(selected) + } ) } } @@ -171,6 +180,7 @@ private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { AddOrEditShareBottomSheet( share = it, viewModel = viewModel, + initialPresetOption = editorInitialPreset, onDismissDraft = { draftShare -> activeShare?.let { viewModel.deleteShare(draftShare.id) } viewModel.setActiveShare(null) @@ -184,6 +194,7 @@ private fun ShareItem( share: Share, type: ShareItemType, onSelectShare: (Share) -> Unit, + onCustomizeShare: (Share) -> Unit, onChangePreset: (Share, PermissionPreset) -> Unit, onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit @@ -201,7 +212,7 @@ private fun ShareItem( if (preset != null) { onChangePreset(share, preset) } else { - onSelectShare(share) + onCustomizeShare(share) } }, onDismiss = { overlayState = ShareItemOverlayState.None } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 2472f823..c372c819 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -77,6 +77,7 @@ import kotlinx.coroutines.launch fun AddOrEditShareBottomSheet( share: Share, viewModel: ShareViewModel, + initialPresetOption: PermissionPresetOption? = null, onDismissDraft: (Share) -> Unit = {} ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -149,6 +150,7 @@ fun AddOrEditShareBottomSheet( PermissionsView( share = share, + initialPresetOption = initialPresetOption, viewModel = viewModel ) @@ -188,9 +190,12 @@ fun AddOrEditShareBottomSheet( @Composable private fun PermissionsView( share: Share, + initialPresetOption: PermissionPresetOption?, viewModel: ShareViewModel ) { - var selectedPreset by remember(share.id) { mutableStateOf(share.permissionPreset) } + var selectedPreset by remember(share.id) { + mutableStateOf(if (initialPresetOption != null) initialPresetOption.preset else share.permissionPreset) + } PermissionPresetDropdown( selectedOption = PermissionPresetOption.from(selectedPreset), From 2eb3d9ede2398151839848a54adee6049873ee18 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 12:55:32 +0200 Subject: [PATCH 081/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index b1909300..c9f5d862 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxSize 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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -269,6 +270,7 @@ private fun ShareItem( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, ) + Spacer(modifier = Modifier.width(4.dp)) Icon(Icons.Default.KeyboardArrowDown, contentDescription = "Down arrow") @@ -287,7 +289,10 @@ private fun ShareItem( ) { DropdownMenuItem( leadingIcon = { - Icon(Icons.Default.Edit, contentDescription = "Edit icon") + Icon(Icons.Default.Edit, + contentDescription = "Edit icon", + modifier = Modifier.size(18.dp) + ) }, text = { Text(stringResource(R.string.share_view_list_item_edit)) }, onClick = { @@ -297,7 +302,11 @@ private fun ShareItem( ) DropdownMenuItem( leadingIcon = { - Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send icon") + Icon( + Icons.AutoMirrored.Filled.Send, + contentDescription = "Send icon", + modifier = Modifier.size(18.dp) + ) }, text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, onClick = { @@ -308,7 +317,11 @@ private fun ShareItem( HorizontalDivider() DropdownMenuItem( leadingIcon = { - Icon(Icons.Default.Delete, contentDescription = "Delete icon") + Icon( + Icons.Default.Delete, + contentDescription = "Delete icon", + modifier = Modifier.size(18.dp) + ) }, text = { Text( From 6a7e12ba3a83b523be47568c84c74d69697a75e5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 13:12:00 +0200 Subject: [PATCH 082/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index c9f5d862..6c4d101f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -16,11 +16,13 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Delete @@ -254,15 +256,21 @@ private fun ShareItem( Text( text = headline, - style = MaterialTheme.typography.titleSmall + style = MaterialTheme.typography.titleMedium ) }, leadingContent = { share.recipients.first().icon?.let { RecipientIcon(icon = it) } }, supportingContent = { + val chipHorizontalPadding = 10.dp Row( - modifier = Modifier.clickable { overlayState = ShareItemOverlayState.QuickShare }, + modifier = Modifier + .offset(x = -chipHorizontalPadding) + .clip(RoundedCornerShape(percent = 50)) + .clickable { overlayState = ShareItemOverlayState.QuickShare } + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.08f)) + .padding(horizontal = chipHorizontalPadding, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically ) { Text( @@ -271,11 +279,15 @@ private fun ShareItem( color = MaterialTheme.colorScheme.primary, ) - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(2.dp)) - Icon(Icons.Default.KeyboardArrowDown, contentDescription = "Down arrow") + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) } - }, trailingContent = { Box { From 66c37025d28b8d10dc8ceebc1b18f17a71c3e813 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 13:58:03 +0200 Subject: [PATCH 083/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 10 +++++----- .../android/common/ui/share/model/api/share/Share.kt | 3 +-- .../android/common/ui/share/model/ui/ShareCategory.kt | 11 ++++++++++- ui/src/main/res/values/strings.xml | 1 + 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index c372c819..01ae6c84 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -18,8 +18,6 @@ import androidx.compose.foundation.layout.size 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.automirrored.filled.Send import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenuItem @@ -174,9 +172,11 @@ fun AddOrEditShareBottomSheet( ) } + // FIXME: make readyToSend suitable for anyone category if (share.readyToSend()) { ActionButtons( share = share, + category = selectedCategory, sendEnabled = !hasPropertyErrors, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } ) @@ -287,10 +287,10 @@ private fun AdvancedSettingsSection( } } - @Composable private fun ActionButtons( share: Share, + category: ShareCategory, sendEnabled: Boolean, onSend: () -> Unit, ) { @@ -341,12 +341,12 @@ private fun ActionButtons( ), ) { Icon( - imageVector = Icons.AutoMirrored.Filled.Send, + imageVector = category.sendActionIcon, contentDescription = null, modifier = Modifier.size(20.dp), ) Text( - text = stringResource(R.string.share_view_send_action), + text = stringResource(category.sendActionTitleId), modifier = Modifier.padding(start = 8.dp), ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 33c22c8b..0c00667b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -63,8 +63,7 @@ data class Share( return if (shareState == ShareState.DRAFT) { context.getString(R.string.share_view_bottom_sheet_new_title) } else { - // TODO do not hardcode - context.getString(R.string.share_view_bottom_sheet_edit_title, "") + context.getString(R.string.share_view_bottom_sheet_edit_title, sources.firstOrNull()?.displayName) } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt index e58af38d..44d78f9e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt @@ -7,6 +7,9 @@ package com.nextcloud.android.common.ui.share.model.ui +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Share import com.nextcloud.android.common.ui.R enum class ShareCategory(val titleId: Int, val iconId: Int) { @@ -14,5 +17,11 @@ enum class ShareCategory(val titleId: Int, val iconId: Int) { R.string.share_view_invited_category_title, R.drawable.ic_person_add ), - Anyone(R.string.share_view_anyone_category_title, R.drawable.ic_anyone) + Anyone(R.string.share_view_anyone_category_title, R.drawable.ic_anyone); + + val sendActionTitleId + get() = if (this == Invited) R.string.share_view_send_action else R.string.share_view_share_action + + val sendActionIcon + get() = if (this == Invited) Icons.AutoMirrored.Filled.Send else Icons.Default.Share } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 0d432dd0..6f3f2b37 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Copied Copy link Send + Share Edit Send email From 11b4ab3d0f6d6a25350807d2886200f3bc79d376 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 13:34:10 +0200 Subject: [PATCH 084/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/model/api/recipients/Recipient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index 5d250b31..58d12ca0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -32,6 +32,6 @@ data class Recipient( val initiator: User? = null ) { companion object { - const val TOKEN_RECIPIENT_CLASS = "OC\\CoreSharing\\Recipient\\TokenShareRecipientType" + const val TOKEN_RECIPIENT_CLASS = """\OC\Core\Sharing\Recipient\TokenShareRecipientType""" } } From 344c3d3096a956b9eeb7a54b922bde97b3b43769 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 13:51:05 +0200 Subject: [PATCH 085/174] wip Signed-off-by: alperozturk96 --- .../common/ui/share/component/CustomLink.kt | 88 +++++++++++++++++++ .../ui/share/component/RecipientTokenField.kt | 41 --------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 27 +++--- .../share/model/api/recipients/Recipient.kt | 2 +- ui/src/main/res/values/strings.xml | 5 +- 5 files changed, 107 insertions(+), 56 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt new file mode 100644 index 00000000..b0e05afb --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -0,0 +1,88 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import java.util.UUID + +@Composable +fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { + val prefix = remember(recipient.secret.url, recipient.secret.value) { + val url = recipient.secret.url.orEmpty() + val token = recipient.secret.value.orEmpty() + if (token.isNotEmpty() && url.endsWith(token)) url.removeSuffix(token) else url + } + var token by remember(recipient.value) { mutableStateOf(recipient.secret.value ?: "") } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text( + text = stringResource(R.string.share_view_custom_link_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Text( + text = stringResource(R.string.share_view_custom_link_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp, bottom = 8.dp) + ) + + OutlinedTextField( + value = token, + onValueChange = { + token = it + onTokenChange(it) + }, + label = { + Text(prefix) + }, + singleLine = true, + trailingIcon = { + IconButton( + onClick = { + val refreshedToken = UUID.randomUUID().toString() + token = refreshedToken + onTokenChange(refreshedToken) + } + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(R.string.share_view_custom_link_refresh) + ) + } + }, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt deleted file mode 100644 index 11a7948d..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientTokenField.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.component - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -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.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient - -@Composable -fun RecipientTokenField(recipient: Recipient, onTokenChange: (String) -> Unit) { - var token by remember(recipient.value) { mutableStateOf(recipient.secret.value ?: "") } - - OutlinedTextField( - value = token, - onValueChange = { - token = it - onTokenChange(it) - }, - label = { Text(stringResource(R.string.share_view_link_token_label)) }, - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - ) -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 01ae6c84..1cc31dd8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -52,7 +52,7 @@ import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection -import com.nextcloud.android.common.ui.share.component.RecipientTokenField +import com.nextcloud.android.common.ui.share.component.CustomLink import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView @@ -135,26 +135,17 @@ fun AddOrEditShareBottomSheet( SelectRecipientField(share, viewModel) } - // TODO: is it going to be included for now? - val editableRecipient = share.recipients.firstOrNull { it.secret.updatable } - if (editableRecipient != null) { - RecipientTokenField( - recipient = editableRecipient, - onTokenChange = { token -> - viewModel.updateRecipientSecret(share.id, editableRecipient, token) - } - ) - } - PermissionsView( share = share, initialPresetOption = initialPresetOption, viewModel = viewModel ) - if (share.properties.isNotEmpty()) { + val editableRecipient = share.recipients.firstOrNull { it.secret.updatable } + if (share.properties.isNotEmpty() || editableRecipient != null) { AdvancedSettingsSection( share = share, + editableRecipient = editableRecipient, isExpanded = showAdvancedSettings, onToggle = { showAdvancedSettings = !showAdvancedSettings }, viewModel = viewModel @@ -266,6 +257,7 @@ private fun PermissionPresetDropdown( @Composable private fun AdvancedSettingsSection( share: Share, + editableRecipient: Recipient?, isExpanded: Boolean, onToggle: () -> Unit, viewModel: ShareViewModel @@ -284,6 +276,15 @@ private fun AdvancedSettingsSection( ) } } + + if (editableRecipient != null) { + CustomLink( + recipient = editableRecipient, + onTokenChange = { token -> + viewModel.updateRecipientSecret(share.id, editableRecipient, token) + } + ) + } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index 58d12ca0..d5f967d5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -32,6 +32,6 @@ data class Recipient( val initiator: User? = null ) { companion object { - const val TOKEN_RECIPIENT_CLASS = """\OC\Core\Sharing\Recipient\TokenShareRecipientType""" + const val TOKEN_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\TokenShareRecipientType""" } } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 6f3f2b37..be4b1939 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -15,9 +15,12 @@ Settings Recipient - Link token Clear recipient + Custom link + The link can be changed to be easy to remember, but do not set it to something that is easy to guess. + Refresh link + Not shared No result From 04f761dbbdd3d4fe69266767ed432e24253e46f4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 14:01:55 +0200 Subject: [PATCH 086/174] wip Signed-off-by: alperozturk96 --- .../common/ui/share/component/CustomLink.kt | 10 ++++----- .../ui/share/component/ShareTokenGenerator.kt | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index b0e05afb..b28e4313 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient -import java.util.UUID @Composable fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { @@ -60,9 +59,10 @@ fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { OutlinedTextField( value = token, - onValueChange = { - token = it - onTokenChange(it) + onValueChange = { newValue -> + val trimmed = newValue.take(ShareTokenGenerator.MAX_LENGTH) + token = trimmed + if (trimmed.isNotBlank()) onTokenChange(trimmed) }, label = { Text(prefix) @@ -71,7 +71,7 @@ fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { trailingIcon = { IconButton( onClick = { - val refreshedToken = UUID.randomUUID().toString() + val refreshedToken = ShareTokenGenerator.generate() token = refreshedToken onTokenChange(refreshedToken) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt new file mode 100644 index 00000000..3141da73 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import java.security.SecureRandom + +object ShareTokenGenerator { + const val MAX_LENGTH = 32 + private const val DEFAULT_LENGTH = 15 + private const val HUMAN_READABLE_CHARS = "abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789" + + private val random = SecureRandom() + + fun generate(length: Int = DEFAULT_LENGTH): String = buildString { + repeat(length) { append(HUMAN_READABLE_CHARS[random.nextInt(HUMAN_READABLE_CHARS.length)]) } + } +} From b24738fe11ae768a56d9724d19692eb05bc158b4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 14:09:36 +0200 Subject: [PATCH 087/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 18 ++++------ .../common/ui/share/model/api/share/Share.kt | 33 +++++++++++++------ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 1cc31dd8..e711e888 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -141,11 +141,9 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) - val editableRecipient = share.recipients.firstOrNull { it.secret.updatable } - if (share.properties.isNotEmpty() || editableRecipient != null) { + if (share.isAdvancedSettingsExists) { AdvancedSettingsSection( share = share, - editableRecipient = editableRecipient, isExpanded = showAdvancedSettings, onToggle = { showAdvancedSettings = !showAdvancedSettings }, viewModel = viewModel @@ -164,7 +162,7 @@ fun AddOrEditShareBottomSheet( } // FIXME: make readyToSend suitable for anyone category - if (share.readyToSend()) { + if (share.canSend) { ActionButtons( share = share, category = selectedCategory, @@ -257,7 +255,6 @@ private fun PermissionPresetDropdown( @Composable private fun AdvancedSettingsSection( share: Share, - editableRecipient: Recipient?, isExpanded: Boolean, onToggle: () -> Unit, viewModel: ShareViewModel @@ -277,11 +274,11 @@ private fun AdvancedSettingsSection( } } - if (editableRecipient != null) { + share.editableRecipient?.let { CustomLink( - recipient = editableRecipient, + recipient = it, onTokenChange = { token -> - viewModel.updateRecipientSecret(share.id, editableRecipient, token) + viewModel.updateRecipientSecret(share.id, it, token) } ) } @@ -301,15 +298,14 @@ private fun ActionButtons( .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - val clipEntry = share.getClipEntry() - if (clipEntry != null) { + if (share.clipEntry != null) { val localClipboard = LocalClipboard.current val scope = rememberCoroutineScope() Button( onClick = { scope.launch { - localClipboard.setClipEntry(clipEntry) + localClipboard.setClipEntry(share.clipEntry) } }, modifier = Modifier.weight(1f), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 0c00667b..13b06b20 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -46,18 +46,31 @@ data class Share( val permissionPreset: PermissionPreset? = null ) { // FIXME: secret.url is null - fun getClipEntry(): ClipEntry? { - val recipient = recipients.firstOrNull() ?: return null - val link = recipient.secret.url ?: return null + val clipEntry: ClipEntry? + get() { + val recipient = recipients.firstOrNull() ?: return null + val link = recipient.secret.url ?: return null - return ClipData.newPlainText(recipient.displayName, link).toClipEntry() - } + return ClipData.newPlainText(recipient.displayName, link).toClipEntry() + } + + val editableRecipient: Recipient? + get() { + return recipients.firstOrNull { it.secret.updatable } + } - fun readyToSend(): Boolean = - sources.isNotEmpty() && - recipients.isNotEmpty() && - permissions.any { it.enabled } && - properties.none { it.required && it.value.isNullOrEmpty() } + val isAdvancedSettingsExists: Boolean + get() { + return properties.isNotEmpty() || editableRecipient != null + } + + val canSend: Boolean + get() { + return sources.isNotEmpty() && + recipients.isNotEmpty() && + permissions.any { it.enabled } && + properties.none { it.required && it.value.isNullOrEmpty() } + } fun title(context: Context): String { return if (shareState == ShareState.DRAFT) { From f4d415188cfb59952577e047ad68eb441df37852 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 14:21:23 +0200 Subject: [PATCH 088/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 6 +++++ .../common/ui/share/component/CustomLink.kt | 21 +++++++++++++----- .../ui/share/component/ShareTokenGenerator.kt | 22 ------------------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 2 ++ .../common/ui/share/model/api/share/Share.kt | 5 +++++ .../share/repository/MockShareRepository.kt | 2 ++ .../share/repository/ShareRemoteRepository.kt | 9 ++++++++ .../ui/share/repository/ShareRepository.kt | 2 ++ 8 files changed, 42 insertions(+), 27 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 3fbceb82..1a657f81 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -253,6 +253,12 @@ class ShareViewModel( replaceInList(updated) } } + + suspend fun generateSecret(): String? = withContext(Dispatchers.IO) { + _errorMessageId.update { null } + val result = repository.generateSecret() + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + } // endregion // region properties diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index b28e4313..7231bf95 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,9 +29,17 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import kotlinx.coroutines.launch + +private const val MAX_TOKEN_LENGTH = 32 @Composable -fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { +fun CustomLink( + recipient: Recipient, + onGenerateSecret: suspend () -> String?, + onTokenChange: (String) -> Unit +) { + val scope = rememberCoroutineScope() val prefix = remember(recipient.secret.url, recipient.secret.value) { val url = recipient.secret.url.orEmpty() val token = recipient.secret.value.orEmpty() @@ -60,7 +69,7 @@ fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { OutlinedTextField( value = token, onValueChange = { newValue -> - val trimmed = newValue.take(ShareTokenGenerator.MAX_LENGTH) + val trimmed = newValue.take(MAX_TOKEN_LENGTH) token = trimmed if (trimmed.isNotBlank()) onTokenChange(trimmed) }, @@ -71,9 +80,11 @@ fun CustomLink(recipient: Recipient, onTokenChange: (String) -> Unit) { trailingIcon = { IconButton( onClick = { - val refreshedToken = ShareTokenGenerator.generate() - token = refreshedToken - onTokenChange(refreshedToken) + scope.launch { + val generated = onGenerateSecret() ?: return@launch + token = generated + onTokenChange(generated) + } } ) { Icon( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt deleted file mode 100644 index 3141da73..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTokenGenerator.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.component - -import java.security.SecureRandom - -object ShareTokenGenerator { - const val MAX_LENGTH = 32 - private const val DEFAULT_LENGTH = 15 - private const val HUMAN_READABLE_CHARS = "abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789" - - private val random = SecureRandom() - - fun generate(length: Int = DEFAULT_LENGTH): String = buildString { - repeat(length) { append(HUMAN_READABLE_CHARS[random.nextInt(HUMAN_READABLE_CHARS.length)]) } - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index e711e888..3fc7c166 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -117,6 +117,7 @@ fun AddOrEditShareBottomSheet( categories.forEachIndexed { index, category -> SegmentedButton( selected = selectedCategory == category, + enabled = category != ShareCategory.Anyone || !share.hasInvitedRecipients, onClick = { selectedCategory = category viewModel.addAnyoneRecipient(category, share) @@ -277,6 +278,7 @@ private fun AdvancedSettingsSection( share.editableRecipient?.let { CustomLink( recipient = it, + onGenerateSecret = { viewModel.generateSecret() }, onTokenChange = { token -> viewModel.updateRecipientSecret(share.id, it, token) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 13b06b20..f340809c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -59,6 +59,11 @@ data class Share( return recipients.firstOrNull { it.secret.updatable } } + val hasInvitedRecipients: Boolean + get() { + return recipients.any { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } + } + val isAdvancedSettingsExists: Boolean get() { return properties.isNotEmpty() || editableRecipient != null diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index f125d92c..96c43283 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -434,6 +434,8 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(current) } + override suspend fun generateSecret(): NetworkResult = NetworkResult.Success("mockSecret12345") + override suspend fun updateSharePermission( id: String, request: UpdateSharePermissionRequest diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 7e09a2f5..71eccd8f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -34,6 +34,7 @@ class ShareRemoteRepository( private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share" private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares" private const val RECIPIENTS_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/recipients" + private const val SECRET_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/secret" } override suspend fun fetchRecipients( @@ -212,4 +213,12 @@ class ShareRemoteRepository( ) { body -> json.decodeFromString>(body).ocs.data } + + override suspend fun generateSecret(): NetworkResult = + client.executeRequest( + endpoint = SECRET_ENDPOINT, + method = HttpMethod.GET + ) { body -> + json.decodeFromString>(body).ocs.data + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 9ed354e0..51bd49d2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -89,4 +89,6 @@ interface ShareRepository { id: String, request: UpdateShareRecipientSecretRequest ): NetworkResult + + suspend fun generateSecret(): NetworkResult } From d7722e70c73536f59f5cc30ce22d94a88a74bc85 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 14:52:44 +0200 Subject: [PATCH 089/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 35 +++++++++++-------- .../common/ui/share/component/CustomLink.kt | 4 +-- .../bottomsheet/AddOrEditShareBottomSheet.kt | 10 +++--- .../common/ui/share/model/api/share/Share.kt | 10 ------ 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 1a657f81..da84e523 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -207,25 +207,32 @@ class ShareViewModel( } } - fun addAnyoneRecipient(category: ShareCategory, share: Share) { - if (category != ShareCategory.Anyone) return - - share.recipients - .filter { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } - .forEach { recipient -> - removeRecipient(share.id, recipient.clazz, recipient.value, recipient.instance) + fun selectCategory(category: ShareCategory, share: Share) { + when (category) { + ShareCategory.Anyone -> { + removeRecipients(share) { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } + if (share.recipients.none { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS }) { + addRecipient( + id = share.id, + clazz = Recipient.TOKEN_RECIPIENT_CLASS, + value = UUID.randomUUID().toString(), + instance = null + ) + } } - if (share.recipients.none { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS }) { - addRecipient( - id = share.id, - clazz = Recipient.TOKEN_RECIPIENT_CLASS, - value = UUID.randomUUID().toString(), - instance = null - ) + ShareCategory.Invited -> { + removeRecipients(share) { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } + } } } + private fun removeRecipients(share: Share, predicate: (Recipient) -> Boolean) { + share.recipients + .filter(predicate) + .forEach { removeRecipient(share.id, it.clazz, it.value, it.instance) } + } + fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareRecipient(id, clazz, value, instance) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index 7231bf95..8c581c52 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -73,9 +73,7 @@ fun CustomLink( token = trimmed if (trimmed.isNotBlank()) onTokenChange(trimmed) }, - label = { - Text(prefix) - }, + label = { Text(prefix) }, singleLine = true, trailingIcon = { IconButton( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 3fc7c166..a0e2c722 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -117,10 +117,9 @@ fun AddOrEditShareBottomSheet( categories.forEachIndexed { index, category -> SegmentedButton( selected = selectedCategory == category, - enabled = category != ShareCategory.Anyone || !share.hasInvitedRecipients, onClick = { selectedCategory = category - viewModel.addAnyoneRecipient(category, share) + viewModel.selectCategory(category, share) }, shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), icon = { @@ -142,9 +141,11 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) - if (share.isAdvancedSettingsExists) { + val editableRecipient = share.editableRecipient + if (share.properties.isNotEmpty() || editableRecipient != null) { AdvancedSettingsSection( share = share, + editableRecipient = editableRecipient, isExpanded = showAdvancedSettings, onToggle = { showAdvancedSettings = !showAdvancedSettings }, viewModel = viewModel @@ -256,6 +257,7 @@ private fun PermissionPresetDropdown( @Composable private fun AdvancedSettingsSection( share: Share, + editableRecipient: Recipient?, isExpanded: Boolean, onToggle: () -> Unit, viewModel: ShareViewModel @@ -275,7 +277,7 @@ private fun AdvancedSettingsSection( } } - share.editableRecipient?.let { + editableRecipient?.let { CustomLink( recipient = it, onGenerateSecret = { viewModel.generateSecret() }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index f340809c..5cca8b4c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -59,16 +59,6 @@ data class Share( return recipients.firstOrNull { it.secret.updatable } } - val hasInvitedRecipients: Boolean - get() { - return recipients.any { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } - } - - val isAdvancedSettingsExists: Boolean - get() { - return properties.isNotEmpty() || editableRecipient != null - } - val canSend: Boolean get() { return sources.isNotEmpty() && From 432c46395081ede685c2dd3fffdf46a8d77c1151 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 15:42:25 +0200 Subject: [PATCH 090/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 5 ++- .../android/common/ui/share/ShareViewModel.kt | 10 +++++- .../bottomsheet/AddOrEditShareBottomSheet.kt | 35 ++++++++++--------- .../common/ui/share/model/api/share/Share.kt | 25 ++++++++----- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 6c4d101f..e7eef100 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -355,10 +355,13 @@ private fun ShareItem( fun ComposeView.initShareScreen( sourceId: String, credentials: ServerCredentials, + internalLink: String, colorScheme: ColorScheme ) { val nextcloudHttpClient = NextcloudHttpClient.create(credentials) - val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) + val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)).apply { + setInternalLink(internalLink) + } setContent { setSingletonImageLoaderFactory { context -> diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index da84e523..7d6a0c99 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -12,11 +12,11 @@ import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.dataOrElse +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest @@ -71,6 +71,8 @@ class ShareViewModel( private val propertyUpdateJobs = mutableMapOf() + private var internalLink: String = "" + private var secretUpdateJob: Job? = null private val currentShares: List @@ -185,6 +187,12 @@ class ShareViewModel( } } + fun setInternalLink(value: String) { + internalLink = value + } + + fun getInternalLink(): String = internalLink + fun removeSource(id: String, clazz: String, value: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareSource(id, clazz, value) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index a0e2c722..2cf59444 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -141,16 +141,13 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) - val editableRecipient = share.editableRecipient - if (share.properties.isNotEmpty() || editableRecipient != null) { - AdvancedSettingsSection( - share = share, - editableRecipient = editableRecipient, - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings }, - viewModel = viewModel - ) - } + AdvancedSettingsSection( + share = share, + category = selectedCategory, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + viewModel = viewModel + ) if (hasPropertyErrors) { Text( @@ -163,11 +160,11 @@ fun AddOrEditShareBottomSheet( ) } - // FIXME: make readyToSend suitable for anyone category if (share.canSend) { ActionButtons( share = share, category = selectedCategory, + internalLink = viewModel.getInternalLink(), sendEnabled = !hasPropertyErrors, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } ) @@ -257,7 +254,7 @@ private fun PermissionPresetDropdown( @Composable private fun AdvancedSettingsSection( share: Share, - editableRecipient: Recipient?, + category: ShareCategory, isExpanded: Boolean, onToggle: () -> Unit, viewModel: ShareViewModel @@ -277,12 +274,16 @@ private fun AdvancedSettingsSection( } } - editableRecipient?.let { + share.getCustomLinkRecipient(category)?.let { CustomLink( recipient = it, onGenerateSecret = { viewModel.generateSecret() }, onTokenChange = { token -> - viewModel.updateRecipientSecret(share.id, it, token) + var recipient = it + if (category == ShareCategory.Invited) { + recipient = it.copy(value = token) + } + viewModel.updateRecipientSecret(share.id, recipient, token) } ) } @@ -293,6 +294,7 @@ private fun AdvancedSettingsSection( private fun ActionButtons( share: Share, category: ShareCategory, + internalLink: String, sendEnabled: Boolean, onSend: () -> Unit, ) { @@ -302,14 +304,15 @@ private fun ActionButtons( .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - if (share.clipEntry != null) { + val clipEntry = share.getClipEntry(category, internalLink) + if (clipEntry != null) { val localClipboard = LocalClipboard.current val scope = rememberCoroutineScope() Button( onClick = { scope.launch { - localClipboard.setClipEntry(share.clipEntry) + localClipboard.setClipEntry(clipEntry) } }, modifier = Modifier.weight(1f), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 5cca8b4c..7aadd098 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -19,6 +19,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User +import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -45,19 +46,25 @@ data class Share( @SerialName("permission_preset") val permissionPreset: PermissionPreset? = null ) { - // FIXME: secret.url is null - val clipEntry: ClipEntry? - get() { - val recipient = recipients.firstOrNull() ?: return null - val link = recipient.secret.url ?: return null + fun getClipEntry(category: ShareCategory, internalLink: String): ClipEntry? { + val recipient = recipients.firstOrNull() ?: return null - return ClipData.newPlainText(recipient.displayName, link).toClipEntry() + return if (category == ShareCategory.Anyone) { + val link = recipient.secret.url ?: return null + ClipData.newPlainText(recipient.displayName, link).toClipEntry() + } else { + ClipData.newPlainText(recipient.displayName, internalLink).toClipEntry() } + } - val editableRecipient: Recipient? - get() { - return recipients.firstOrNull { it.secret.updatable } + fun getCustomLinkRecipient(category: ShareCategory): Recipient? { + return if (category == ShareCategory.Anyone) { + recipients.firstOrNull { it.secret.updatable } + } else { + val firstRecipient = recipients.firstOrNull() + firstRecipient?.copy(clazz = Recipient.TOKEN_RECIPIENT_CLASS) } + } val canSend: Boolean get() { From b42b1593cbadce1efc17b50ff30dd46151cd0aeb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 16:01:32 +0200 Subject: [PATCH 091/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 5 +-- .../android/common/ui/share/ShareViewModel.kt | 8 ----- .../bottomsheet/AddOrEditShareBottomSheet.kt | 31 +++++++------------ .../common/ui/share/model/api/share/Share.kt | 23 +++++--------- 4 files changed, 20 insertions(+), 47 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index e7eef100..6c4d101f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -355,13 +355,10 @@ private fun ShareItem( fun ComposeView.initShareScreen( sourceId: String, credentials: ServerCredentials, - internalLink: String, colorScheme: ColorScheme ) { val nextcloudHttpClient = NextcloudHttpClient.create(credentials) - val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)).apply { - setInternalLink(internalLink) - } + val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) setContent { setSingletonImageLoaderFactory { context -> diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 7d6a0c99..8d0c5271 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -71,8 +71,6 @@ class ShareViewModel( private val propertyUpdateJobs = mutableMapOf() - private var internalLink: String = "" - private var secretUpdateJob: Job? = null private val currentShares: List @@ -187,12 +185,6 @@ class ShareViewModel( } } - fun setInternalLink(value: String) { - internalLink = value - } - - fun getInternalLink(): String = internalLink - fun removeSource(id: String, clazz: String, value: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareSource(id, clazz, value) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 2cf59444..d16d4f02 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -141,13 +141,14 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) - AdvancedSettingsSection( - share = share, - category = selectedCategory, - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings }, - viewModel = viewModel - ) + if (share.properties.isNotEmpty() || share.customLinkRecipient != null) { + AdvancedSettingsSection( + share = share, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + viewModel = viewModel + ) + } if (hasPropertyErrors) { Text( @@ -164,7 +165,6 @@ fun AddOrEditShareBottomSheet( ActionButtons( share = share, category = selectedCategory, - internalLink = viewModel.getInternalLink(), sendEnabled = !hasPropertyErrors, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } ) @@ -254,7 +254,6 @@ private fun PermissionPresetDropdown( @Composable private fun AdvancedSettingsSection( share: Share, - category: ShareCategory, isExpanded: Boolean, onToggle: () -> Unit, viewModel: ShareViewModel @@ -274,16 +273,12 @@ private fun AdvancedSettingsSection( } } - share.getCustomLinkRecipient(category)?.let { + share.customLinkRecipient?.let { CustomLink( recipient = it, onGenerateSecret = { viewModel.generateSecret() }, onTokenChange = { token -> - var recipient = it - if (category == ShareCategory.Invited) { - recipient = it.copy(value = token) - } - viewModel.updateRecipientSecret(share.id, recipient, token) + viewModel.updateRecipientSecret(share.id, it, token) } ) } @@ -294,7 +289,6 @@ private fun AdvancedSettingsSection( private fun ActionButtons( share: Share, category: ShareCategory, - internalLink: String, sendEnabled: Boolean, onSend: () -> Unit, ) { @@ -304,15 +298,14 @@ private fun ActionButtons( .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - val clipEntry = share.getClipEntry(category, internalLink) - if (clipEntry != null) { + if (share.clipEntry != null) { val localClipboard = LocalClipboard.current val scope = rememberCoroutineScope() Button( onClick = { scope.launch { - localClipboard.setClipEntry(clipEntry) + localClipboard.setClipEntry(share.clipEntry) } }, modifier = Modifier.weight(1f), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 7aadd098..b7f4e780 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -19,7 +19,6 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User -import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -46,25 +45,17 @@ data class Share( @SerialName("permission_preset") val permissionPreset: PermissionPreset? = null ) { - fun getClipEntry(category: ShareCategory, internalLink: String): ClipEntry? { - val recipient = recipients.firstOrNull() ?: return null - - return if (category == ShareCategory.Anyone) { + val clipEntry: ClipEntry? + get() { + val recipient = recipients.firstOrNull() ?: return null val link = recipient.secret.url ?: return null - ClipData.newPlainText(recipient.displayName, link).toClipEntry() - } else { - ClipData.newPlainText(recipient.displayName, internalLink).toClipEntry() + return ClipData.newPlainText(recipient.displayName, link).toClipEntry() } - } - fun getCustomLinkRecipient(category: ShareCategory): Recipient? { - return if (category == ShareCategory.Anyone) { - recipients.firstOrNull { it.secret.updatable } - } else { - val firstRecipient = recipients.firstOrNull() - firstRecipient?.copy(clazz = Recipient.TOKEN_RECIPIENT_CLASS) + val customLinkRecipient: Recipient? + get() { + return recipients.firstOrNull { it.secret.updatable } } - } val canSend: Boolean get() { From 9b1f67d85760a83b94e76e41103ef0020284e651 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 15:10:48 +0200 Subject: [PATCH 092/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 6 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 66 ++++++++++++------- .../common/ui/share/model/api/share/Share.kt | 13 ++-- .../common/ui/share/model/ui/ShareCategory.kt | 3 + ui/src/main/res/values/strings.xml | 5 +- 5 files changed, 63 insertions(+), 30 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 6c4d101f..d5b80df5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -85,7 +85,7 @@ import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @Composable -private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { +private fun ShareScreen(sourceId: String, internalLink: String, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsState() val screenState by viewModel.state.collectAsState() val activeShare by viewModel.activeShare.collectAsState() @@ -182,6 +182,7 @@ private fun ShareScreen(sourceId: String, viewModel: ShareViewModel) { activeShare?.let { AddOrEditShareBottomSheet( share = it, + internalLink = internalLink, viewModel = viewModel, initialPresetOption = editorInitialPreset, onDismissDraft = { draftShare -> @@ -354,6 +355,7 @@ private fun ShareItem( fun ComposeView.initShareScreen( sourceId: String, + internalLink: String, credentials: ServerCredentials, colorScheme: ColorScheme ) { @@ -370,7 +372,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(sourceId, viewModel) + ShareScreen(sourceId, internalLink, viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index d16d4f02..45201dff 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -74,6 +74,7 @@ import kotlinx.coroutines.launch @Composable fun AddOrEditShareBottomSheet( share: Share, + internalLink: String, viewModel: ShareViewModel, initialPresetOption: PermissionPresetOption? = null, onDismissDraft: (Share) -> Unit = {} @@ -164,6 +165,7 @@ fun AddOrEditShareBottomSheet( if (share.canSend) { ActionButtons( share = share, + internalLink = internalLink, category = selectedCategory, sendEnabled = !hasPropertyErrors, onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } @@ -288,6 +290,7 @@ private fun AdvancedSettingsSection( @Composable private fun ActionButtons( share: Share, + internalLink: String, category: ShareCategory, sendEnabled: Boolean, onSend: () -> Unit, @@ -298,14 +301,15 @@ private fun ActionButtons( .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - if (share.clipEntry != null) { + val clipEntry = share.getClipEntry(internalLink, category) + if (clipEntry != null) { val localClipboard = LocalClipboard.current val scope = rememberCoroutineScope() Button( onClick = { scope.launch { - localClipboard.setClipEntry(share.clipEntry) + localClipboard.setClipEntry(clipEntry) } }, modifier = Modifier.weight(1f), @@ -321,31 +325,48 @@ private fun ActionButtons( modifier = Modifier.size(20.dp), ) Text( - text = stringResource(R.string.share_view_copy_action), + text = stringResource(category.copyLinkTitleId), modifier = Modifier.padding(start = 8.dp), ) } } - Button( - onClick = onSend, - enabled = sendEnabled, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Icon( - imageVector = category.sendActionIcon, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource(category.sendActionTitleId), - modifier = Modifier.padding(start = 8.dp), - ) + if (category == ShareCategory.Invited) { + Button( + onClick = onSend, + enabled = sendEnabled, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Icon( + imageVector = category.sendActionIcon, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(category.sendActionTitleId), + modifier = Modifier.padding(start = 8.dp), + ) + } + } else { + Button( + onClick = onSend, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Text( + text = stringResource(category.sendActionTitleId), + modifier = Modifier.padding(start = 8.dp), + ) + } } } } @@ -356,6 +377,7 @@ private fun AddOrEditShareBottomSheetPreview() { MaterialTheme { AddOrEditShareBottomSheet( share = previewShare, + internalLink = "internal_link", viewModel = ShareViewModel(MockShareRepository()) ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index b7f4e780..9b8b2ecb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -19,6 +19,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User +import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -45,12 +46,16 @@ data class Share( @SerialName("permission_preset") val permissionPreset: PermissionPreset? = null ) { - val clipEntry: ClipEntry? - get() { - val recipient = recipients.firstOrNull() ?: return null + fun getClipEntry(internalLink: String, category: ShareCategory): ClipEntry? { + val recipient = recipients.firstOrNull() ?: return null + + return if (category == ShareCategory.Anyone) { val link = recipient.secret.url ?: return null - return ClipData.newPlainText(recipient.displayName, link).toClipEntry() + ClipData.newPlainText(recipient.displayName, link).toClipEntry() + } else { + ClipData.newPlainText(recipient.displayName, internalLink).toClipEntry() } + } val customLinkRecipient: Recipient? get() { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt index 44d78f9e..6522188e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt @@ -19,6 +19,9 @@ enum class ShareCategory(val titleId: Int, val iconId: Int) { ), Anyone(R.string.share_view_anyone_category_title, R.drawable.ic_anyone); + val copyLinkTitleId + get() = if (this == Invited) R.string.share_view_invited_copy_action else R.string.share_view_anyone_copy_action + val sendActionTitleId get() = if (this == Invited) R.string.share_view_send_action else R.string.share_view_share_action diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index be4b1939..063b4096 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -25,9 +25,10 @@ No result Copied - Copy link + Copy private link + Copy public link Send - Share + Share public link… Edit Send email From e711122af2735c48258047ca7500a9cae119ed61 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 15:16:36 +0200 Subject: [PATCH 093/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 45201dff..699e6fbb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -301,34 +301,33 @@ private fun ActionButtons( .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - val clipEntry = share.getClipEntry(internalLink, category) - if (clipEntry != null) { - val localClipboard = LocalClipboard.current - val scope = rememberCoroutineScope() + val localClipboard = LocalClipboard.current + val scope = rememberCoroutineScope() - Button( - onClick = { - scope.launch { - localClipboard.setClipEntry(clipEntry) + Button( + onClick = { + scope.launch { + share.getClipEntry(internalLink, category)?.let { + localClipboard.setClipEntry(it) } - }, - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(10.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - ) { - Icon( - painter = painterResource(R.drawable.ic_link), - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource(category.copyLinkTitleId), - modifier = Modifier.padding(start = 8.dp), - ) - } + } + }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Icon( + painter = painterResource(R.drawable.ic_link), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(category.copyLinkTitleId), + modifier = Modifier.padding(start = 8.dp), + ) } if (category == ShareCategory.Invited) { From cc2492fd3869ef4f0b42f94690b8871ac5157ecd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 15:40:12 +0200 Subject: [PATCH 094/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 29 +++++++----- .../android/common/ui/share/ShareViewModel.kt | 46 ++++++++++++------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 25 ++++++++-- .../common/ui/share/model/api/share/Share.kt | 5 ++ .../ui/share/model/ui/ActiveShareState.kt | 16 +++++++ 5 files changed, 89 insertions(+), 32 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index d5b80df5..c77e0dd3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -76,6 +76,7 @@ import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePer import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType @@ -179,17 +180,23 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share } } - activeShare?.let { - AddOrEditShareBottomSheet( - share = it, - internalLink = internalLink, - viewModel = viewModel, - initialPresetOption = editorInitialPreset, - onDismissDraft = { draftShare -> - activeShare?.let { viewModel.deleteShare(draftShare.id) } - viewModel.setActiveShare(null) - } - ) + if (activeShare !is ActiveShareState.Dismiss) { + val share = activeShare as? ActiveShareState.SharedAndDismiss + val share2 = activeShare as? ActiveShareState.Update + val activeShareObject = share?.value ?: share2?.value + + activeShareObject?.let { + AddOrEditShareBottomSheet( + share = it, + internalLink = internalLink, + viewModel = viewModel, + initialPresetOption = editorInitialPreset, + onDismissDraft = { draftShare -> + activeShare.let { viewModel.deleteShare(draftShare.id) } + viewModel.setActiveShare(null) + } + ) + } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 8d0c5271..299e05ae 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -24,6 +24,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipi import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.filtered @@ -55,8 +56,8 @@ class ShareViewModel( private val _state = MutableStateFlow(ShareScreenState.Loading) val state: StateFlow = _state - private val _activeShare = MutableStateFlow(null) - val activeShare: StateFlow = _activeShare + private val _activeShare = MutableStateFlow(ActiveShareState.Dismiss) + val activeShare: StateFlow = _activeShare private val _searchQuery = MutableStateFlow("") @@ -133,7 +134,7 @@ class ShareViewModel( val share = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } ?: return@launch - _activeShare.update { share } + _activeShare.update { share.toActiveShare() } replaceInList(share) } } @@ -148,7 +149,7 @@ class ShareViewModel( val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } if (draft != null) { - _activeShare.update { draft } + _activeShare.update { draft.toActiveShare() } _state.update { ShareScreenState.Loaded(listOf(draft) + currentShares) } } @@ -157,15 +158,19 @@ class ShareViewModel( // endregion // region state - fun updateState(id: String, shareState: ShareState) { + fun updateState(id: String, shareState: ShareState, updateAndDontDismiss: Boolean = false) { viewModelScope.launch(Dispatchers.IO) { val result = repository.updateShareState(id, UpdateShareStateRequest(shareState)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch if (shareState == ShareState.ACTIVE) { - _activeShare.update { null } + if (updateAndDontDismiss) { + _activeShare.update { ActiveShareState.Update(updated) } + } else { + _activeShare.update { ActiveShareState.Dismiss } + } } else { - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } } replaceInList(updated) } @@ -180,7 +185,7 @@ class ShareViewModel( val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } replaceInList(updated) } } @@ -190,7 +195,7 @@ class ShareViewModel( val result = repository.removeShareSource(id, clazz, value) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } replaceInList(updated) } } @@ -202,7 +207,7 @@ class ShareViewModel( val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } replaceInList(updated) } } @@ -238,7 +243,7 @@ class ShareViewModel( val result = repository.removeShareRecipient(id, clazz, value, instance) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } replaceInList(updated) } } @@ -256,7 +261,7 @@ class ShareViewModel( val result = repository.updateShareRecipientSecret(shareId, request) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } replaceInList(updated) } } @@ -276,7 +281,7 @@ class ShareViewModel( when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { is NetworkResult.Success -> { _propertyErrors.update { it - clazz } - _activeShare.update { result.data } + _activeShare.update { result.data.toActiveShare() } replaceInList(result.data) } @@ -308,7 +313,7 @@ class ShareViewModel( val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch if (updateActiveShare) { - _activeShare.update { updated } + _activeShare.update { updated.toActiveShare() } } replaceInList(updated) } @@ -326,7 +331,13 @@ class ShareViewModel( if (remaining.filtered().isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(remaining) } - if (_activeShare.value?.id == id) _activeShare.update { null } + + if (_activeShare.value is ActiveShareState.SharedAndDismiss) { + val sharedAndDismiss = (_activeShare.value as ActiveShareState.SharedAndDismiss) + if (sharedAndDismiss.value.id == id) { + _activeShare.update { ActiveShareState.Dismiss } + } + } } } // endregion @@ -338,7 +349,10 @@ class ShareViewModel( fun setActiveShare(value: Share?) { _propertyErrors.update { emptyMap() } - _activeShare.update { value } + + value?.let { + _activeShare.update { value.toActiveShare() } + } } // endregion diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 699e6fbb..e86484a5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -168,7 +168,7 @@ fun AddOrEditShareBottomSheet( internalLink = internalLink, category = selectedCategory, sendEnabled = !hasPropertyErrors, - onSend = { viewModel.updateState(share.id, ShareState.ACTIVE) } + viewModel = viewModel ) } } @@ -293,7 +293,7 @@ private fun ActionButtons( internalLink: String, category: ShareCategory, sendEnabled: Boolean, - onSend: () -> Unit, + viewModel: ShareViewModel ) { Row( modifier = Modifier @@ -308,7 +308,18 @@ private fun ActionButtons( onClick = { scope.launch { share.getClipEntry(internalLink, category)?.let { - localClipboard.setClipEntry(it) + if (category == ShareCategory.Anyone) { + + // recompose ActionButtons + viewModel.updateState(share.id, ShareState.ACTIVE, updateAndDontDismiss = true) + + // share object is updated active share + if (share.shareState == ShareState.ACTIVE) { + localClipboard.setClipEntry(it) + } + } else { + localClipboard.setClipEntry(it) + } } } }, @@ -332,7 +343,9 @@ private fun ActionButtons( if (category == ShareCategory.Invited) { Button( - onClick = onSend, + onClick = { + viewModel.updateState(share.id, ShareState.ACTIVE) + }, enabled = sendEnabled, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), @@ -353,7 +366,9 @@ private fun ActionButtons( } } else { Button( - onClick = onSend, + onClick = { + viewModel.updateState(share.id, ShareState.ACTIVE) + }, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 9b8b2ecb..f44a2d10 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -19,6 +19,7 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User +import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -46,6 +47,10 @@ data class Share( @SerialName("permission_preset") val permissionPreset: PermissionPreset? = null ) { + fun toActiveShare(): ActiveShareState { + return ActiveShareState.SharedAndDismiss(this) + } + fun getClipEntry(internalLink: String, category: ShareCategory): ClipEntry? { val recipient = recipients.firstOrNull() ?: return null diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt new file mode 100644 index 00000000..9c6f6884 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import com.nextcloud.android.common.ui.share.model.api.share.Share + +sealed class ActiveShareState { + data object Dismiss: ActiveShareState() + data class Update(val value: Share): ActiveShareState() + data class SharedAndDismiss(val value: Share): ActiveShareState() +} From 808f796a24f0f46a62e8e676820c91e0238dfb86 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 15:49:28 +0200 Subject: [PATCH 095/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 31 +++++++------------ .../android/common/ui/share/ShareViewModel.kt | 14 ++++----- .../common/ui/share/model/api/share/Share.kt | 2 +- .../ui/share/model/ui/ActiveShareState.kt | 13 ++++++-- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index c77e0dd3..f5411ae4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -76,7 +76,6 @@ import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePer import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share -import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType @@ -180,23 +179,17 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share } } - if (activeShare !is ActiveShareState.Dismiss) { - val share = activeShare as? ActiveShareState.SharedAndDismiss - val share2 = activeShare as? ActiveShareState.Update - val activeShareObject = share?.value ?: share2?.value - - activeShareObject?.let { - AddOrEditShareBottomSheet( - share = it, - internalLink = internalLink, - viewModel = viewModel, - initialPresetOption = editorInitialPreset, - onDismissDraft = { draftShare -> - activeShare.let { viewModel.deleteShare(draftShare.id) } - viewModel.setActiveShare(null) - } - ) - } + activeShare.shareOrNull?.let { activeShareObject -> + AddOrEditShareBottomSheet( + share = activeShareObject, + internalLink = internalLink, + viewModel = viewModel, + initialPresetOption = editorInitialPreset, + onDismissDraft = { draftShare -> + viewModel.deleteShare(draftShare.id) + viewModel.setActiveShare(null) + } + ) } } @@ -252,7 +245,7 @@ private fun ShareItem( onLongClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) overlayState = ShareItemOverlayState.ContextMenu - }, + } ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), headlineContent = { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 299e05ae..a32d7bc9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -56,7 +56,7 @@ class ShareViewModel( private val _state = MutableStateFlow(ShareScreenState.Loading) val state: StateFlow = _state - private val _activeShare = MutableStateFlow(ActiveShareState.Dismiss) + private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare private val _searchQuery = MutableStateFlow("") @@ -165,9 +165,9 @@ class ShareViewModel( ?: return@launch if (shareState == ShareState.ACTIVE) { if (updateAndDontDismiss) { - _activeShare.update { ActiveShareState.Update(updated) } + _activeShare.update { ActiveShareState.Activating(updated) } } else { - _activeShare.update { ActiveShareState.Dismiss } + _activeShare.update { ActiveShareState.None } } } else { _activeShare.update { updated.toActiveShare() } @@ -332,11 +332,9 @@ class ShareViewModel( else ShareScreenState.Loaded(remaining) } - if (_activeShare.value is ActiveShareState.SharedAndDismiss) { - val sharedAndDismiss = (_activeShare.value as ActiveShareState.SharedAndDismiss) - if (sharedAndDismiss.value.id == id) { - _activeShare.update { ActiveShareState.Dismiss } - } + val editingShare = _activeShare.value + if (editingShare is ActiveShareState.Editing && editingShare.share.id == id) { + _activeShare.update { ActiveShareState.None } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index f44a2d10..c9388669 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -48,7 +48,7 @@ data class Share( val permissionPreset: PermissionPreset? = null ) { fun toActiveShare(): ActiveShareState { - return ActiveShareState.SharedAndDismiss(this) + return ActiveShareState.Editing(this) } fun getClipEntry(internalLink: String, category: ShareCategory): ClipEntry? { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt index 9c6f6884..e7b56192 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt @@ -10,7 +10,14 @@ package com.nextcloud.android.common.ui.share.model.ui import com.nextcloud.android.common.ui.share.model.api.share.Share sealed class ActiveShareState { - data object Dismiss: ActiveShareState() - data class Update(val value: Share): ActiveShareState() - data class SharedAndDismiss(val value: Share): ActiveShareState() + data object None : ActiveShareState() + data class Editing(val share: Share) : ActiveShareState() + data class Activating(val share: Share) : ActiveShareState() + + val shareOrNull: Share? + get() = when (this) { + is Editing -> share + is Activating -> share + None -> null + } } From 701930cf9d760bbcaab698c0ce59af0ae95b458f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 11:34:03 +0200 Subject: [PATCH 096/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 18 ++++++++++-------- .../share/component/SelectRecipientField.kt | 13 +++++++++++++ .../common/ui/share/model/api/share/Share.kt | 19 +++++++++++++++++++ ui/src/main/res/drawable/ic_circles.xml | 16 ---------------- ui/src/main/res/drawable/ic_email.xml | 16 ---------------- ui/src/main/res/drawable/ic_group.xml | 16 ---------------- ui/src/main/res/drawable/ic_talk.xml | 18 ------------------ ui/src/main/res/drawable/ic_user.xml | 16 ---------------- ui/src/main/res/values/strings.xml | 2 ++ 9 files changed, 44 insertions(+), 90 deletions(-) delete mode 100644 ui/src/main/res/drawable/ic_circles.xml delete mode 100644 ui/src/main/res/drawable/ic_email.xml delete mode 100644 ui/src/main/res/drawable/ic_group.xml delete mode 100644 ui/src/main/res/drawable/ic_talk.xml delete mode 100644 ui/src/main/res/drawable/ic_user.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index f5411ae4..288e85d4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -58,6 +58,7 @@ 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.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource @@ -70,6 +71,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.share.component.PublicLinkIcon import com.nextcloud.android.common.ui.share.component.RecipientIcon import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePermissionBottomSheet @@ -203,6 +205,7 @@ private fun ShareItem( onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit ) { + val context = LocalContext.current var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) } val haptics = LocalHapticFeedback.current @@ -249,19 +252,18 @@ private fun ShareItem( ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), headlineContent = { - val headline = if (share.recipients.isNotEmpty()) { - share.recipients.first().displayName - } else { - "" - } - Text( - text = headline, + text = share.getHeadline(context), style = MaterialTheme.typography.titleMedium ) }, leadingContent = { - share.recipients.first().icon?.let { RecipientIcon(icon = it) } + val icon = share.recipients.first().icon + if (icon != null) { + RecipientIcon(icon = icon) + } else { + PublicLinkIcon() + } }, supportingContent = { val chipHorizontalPadding = 10.dp diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 82bf6618..1a9bbff3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share.component +import android.graphics.Color import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -30,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage @@ -172,6 +174,17 @@ private fun RecipientDropdownContent( } } +// TODO: can this come from backend? +@Composable +fun PublicLinkIcon(modifier: Modifier = Modifier) { + Icon( + painter = painterResource(R.drawable.ic_link), + contentDescription = "public link icon", + tint = MaterialTheme.colorScheme.onSurface, + modifier = modifier + ) +} + @Composable fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { val isDark = isSystemInDarkTheme() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index c9388669..3dbb8311 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -62,6 +62,25 @@ data class Share( } } + fun getHeadline(context: Context): String { + return if (belongsAnyoneTab) { + // TODO: maybe backend can provide this + // FIXME: Share link 1,2,3 show this in UI + context.getString(R.string.share_view_public_link) + } else { + if (recipients.isNotEmpty()) { + recipients.first().displayName + } else { + "" + } + } + } + + val belongsAnyoneTab: Boolean + get() { + return customLinkRecipient != null + } + val customLinkRecipient: Recipient? get() { return recipients.firstOrNull { it.secret.updatable } diff --git a/ui/src/main/res/drawable/ic_circles.xml b/ui/src/main/res/drawable/ic_circles.xml deleted file mode 100644 index 5b07aff7..00000000 --- a/ui/src/main/res/drawable/ic_circles.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/ui/src/main/res/drawable/ic_email.xml b/ui/src/main/res/drawable/ic_email.xml deleted file mode 100644 index 3319f67e..00000000 --- a/ui/src/main/res/drawable/ic_email.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/ui/src/main/res/drawable/ic_group.xml b/ui/src/main/res/drawable/ic_group.xml deleted file mode 100644 index e68f08e7..00000000 --- a/ui/src/main/res/drawable/ic_group.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/ui/src/main/res/drawable/ic_talk.xml b/ui/src/main/res/drawable/ic_talk.xml deleted file mode 100644 index e55ac5d9..00000000 --- a/ui/src/main/res/drawable/ic_talk.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/ui/src/main/res/drawable/ic_user.xml b/ui/src/main/res/drawable/ic_user.xml deleted file mode 100644 index d6267b2f..00000000 --- a/ui/src/main/res/drawable/ic_user.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 063b4096..401205c8 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -9,6 +9,8 @@ OK Cancel + Share link + Create a new share Share %s From 306b2fc6057075e146ff0b43ab17e1cffff0176c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 11:39:49 +0200 Subject: [PATCH 097/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 22 ++++++++++++++----- .../share/component/SelectRecipientField.kt | 4 ++++ ui/src/main/res/values/strings.xml | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 288e85d4..2788217d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -63,6 +63,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.ImageLoader import coil3.compose.setSingletonImageLoaderFactory @@ -254,15 +255,22 @@ private fun ShareItem( headlineContent = { Text( text = share.getHeadline(context), - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) }, leadingContent = { - val icon = share.recipients.first().icon - if (icon != null) { - RecipientIcon(icon = icon) - } else { - PublicLinkIcon() + Box( + modifier = Modifier.size(24.dp), + contentAlignment = Alignment.Center + ) { + val icon = share.recipients.firstOrNull()?.icon + if (icon != null) { + RecipientIcon(icon = icon, modifier = Modifier.size(24.dp)) + } else { + PublicLinkIcon(modifier = Modifier.size(24.dp)) + } } }, supportingContent = { @@ -280,6 +288,8 @@ private fun ShareItem( text = stringResource(PermissionPresetOption.from(share.permissionPreset).labelRes), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.width(2.dp)) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 1a9bbff3..bbb8ac83 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -9,6 +9,7 @@ package com.nextcloud.android.common.ui.share.component import android.graphics.Color import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -199,5 +200,8 @@ fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { contentDescription = null, modifier = modifier, ) + } else { + // reserve the same space even with no image + Box(modifier = modifier) } } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 401205c8..e44220bb 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -44,7 +44,7 @@ Failed to update share. Some settings couldn\'t be saved. Fix the highlighted fields before sharing. - Set permission + Change permission Participants Can… Can view From d1cdf88796386706d069676d1871e6504d2252d9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 11:47:13 +0200 Subject: [PATCH 098/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 2788217d..3d680b58 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -54,6 +54,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -63,6 +64,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.ImageLoader @@ -275,6 +277,8 @@ private fun ShareItem( }, supportingContent = { val chipHorizontalPadding = 10.dp + val selectedLabelRes = PermissionPresetOption.from(share.permissionPreset).labelRes + Row( modifier = Modifier .offset(x = -chipHorizontalPadding) @@ -284,13 +288,25 @@ private fun ShareItem( .padding(horizontal = chipHorizontalPadding, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = stringResource(PermissionPresetOption.from(share.permissionPreset).labelRes), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + Box(contentAlignment = Alignment.CenterStart) { + PermissionPresetOption.entries.forEach { option -> + Text( + text = stringResource(option.labelRes), + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + modifier = Modifier + .alpha(0f) + .clearAndSetSemantics {} + ) + } + + Text( + text = stringResource(selectedLabelRes), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1 + ) + } Spacer(modifier = Modifier.width(2.dp)) From d07d41e11404e32ff53812593ef9acd78771de6a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 12:07:35 +0200 Subject: [PATCH 099/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 5 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 55 +++++++++++++------ 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index a32d7bc9..610f4b61 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -347,10 +347,7 @@ class ShareViewModel( fun setActiveShare(value: Share?) { _propertyErrors.update { emptyMap() } - - value?.let { - _activeShare.update { value.toActiveShare() } - } + _activeShare.update { value?.toActiveShare() ?: ActiveShareState.None } } // endregion diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index e86484a5..55911083 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -81,7 +81,9 @@ fun AddOrEditShareBottomSheet( ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val categories = remember { ShareCategory.entries.toList() } - var selectedCategory by remember { mutableStateOf(categories.first()) } + var selectedCategory by remember(share.id) { + mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) + } var showAdvancedSettings by remember { mutableStateOf(false) } val context = LocalContext.current val propertyErrors by viewModel.propertyErrors.collectAsState() @@ -114,23 +116,15 @@ fun AddOrEditShareBottomSheet( modifier = Modifier.padding(16.dp) ) - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - categories.forEachIndexed { index, category -> - SegmentedButton( - selected = selectedCategory == category, - onClick = { - selectedCategory = category - viewModel.selectCategory(category, share) - }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), - icon = { - Icon(painter = painterResource(category.iconId), contentDescription = "") - } - ) { - Text(stringResource(category.titleId)) - } + ShareCategorySelector( + share = share, + categories = categories, + selectedCategory = selectedCategory, + onCategorySelected = { category -> + selectedCategory = category + viewModel.selectCategory(category, share) } - } + ) if (selectedCategory == ShareCategory.Invited) { SelectRecipientField(share, viewModel) @@ -176,6 +170,33 @@ fun AddOrEditShareBottomSheet( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShareCategorySelector( + share: Share, + categories: List, + selectedCategory: ShareCategory, + onCategorySelected: (ShareCategory) -> Unit +) { + // only allow user to select between taps if it is draft share + if (share.shareState != ShareState.DRAFT) return + + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + categories.forEachIndexed { index, category -> + SegmentedButton( + selected = selectedCategory == category, + onClick = { onCategorySelected(category) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = categories.size), + icon = { + Icon(painter = painterResource(category.iconId), contentDescription = "") + } + ) { + Text(stringResource(category.titleId)) + } + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PermissionsView( From bcd941802ca03121e852a69c3f2c19d4f31ec930 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 14:47:38 +0200 Subject: [PATCH 100/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 12 ++++++++---- .../common/ui/share/model/api/share/Share.kt | 18 +++++++++--------- ui/src/main/res/values/strings.xml | 1 + 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 3d680b58..ded8c048 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -98,6 +98,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current var editorInitialPreset by remember { mutableStateOf(null) } + val context = LocalContext.current LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -149,8 +150,10 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { - itemsIndexed(state.shares.filtered(), key = { _, share -> share.id }) { index, share -> - val type = ShareItemType.type(index, state.shares.lastIndex) + val shares = state.shares.filtered() + itemsIndexed(shares, key = { _, share -> share.id }) { index, share -> + val type = ShareItemType.type(index, shares.lastIndex) + val title = share.getHeadline(context, shares) if (index == 0) { Spacer(modifier = Modifier.height(16.dp)) @@ -160,6 +163,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share ShareItem( share = share, + title = title, type = type, onSelectShare = { selected -> editorInitialPreset = null @@ -201,6 +205,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share @Composable private fun ShareItem( share: Share, + title: String, type: ShareItemType, onSelectShare: (Share) -> Unit, onCustomizeShare: (Share) -> Unit, @@ -208,7 +213,6 @@ private fun ShareItem( onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit ) { - val context = LocalContext.current var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) } val haptics = LocalHapticFeedback.current @@ -256,7 +260,7 @@ private fun ShareItem( .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), headlineContent = { Text( - text = share.getHeadline(context), + text = title, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 3dbb8311..b28f82b6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -62,17 +62,17 @@ data class Share( } } - fun getHeadline(context: Context): String { - return if (belongsAnyoneTab) { - // TODO: maybe backend can provide this - // FIXME: Share link 1,2,3 show this in UI + fun getHeadline(context: Context, shares: List): String { + if (!belongsAnyoneTab) { + return recipients.firstOrNull()?.displayName ?: "" + } + + val publicLinks = shares.filter { it.belongsAnyoneTab } + return if (publicLinks.size <= 1) { context.getString(R.string.share_view_public_link) } else { - if (recipients.isNotEmpty()) { - recipients.first().displayName - } else { - "" - } + val position = publicLinks.sortedBy { it.lastUpdated }.indexOfFirst { it.id == id } + 1 + context.getString(R.string.share_view_public_link_numbered, position) } } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index e44220bb..faa52182 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -10,6 +10,7 @@ Cancel Share link + Share link (%1$d) Create a new share Share %s From 1759cf4cbaa6021fd8b0601d248329f2294c1f27 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 15:14:41 +0200 Subject: [PATCH 101/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 11 ++++++----- .../bottomsheet/AddOrEditShareBottomSheet.kt | 9 +++++++-- .../common/ui/share/model/ui/ShareEditorEntry.kt | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index ded8c048..68a241fb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -82,6 +82,7 @@ import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirm import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption +import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState @@ -97,7 +98,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current - var editorInitialPreset by remember { mutableStateOf(null) } + var editorEntry by remember { mutableStateOf(ShareEditorEntry.EDIT) } val context = LocalContext.current LaunchedEffect(errorMessageId) { @@ -166,11 +167,11 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share title = title, type = type, onSelectShare = { selected -> - editorInitialPreset = null + editorEntry = ShareEditorEntry.EDIT viewModel.setActiveShare(selected) }, onCustomizeShare = { selected -> - editorInitialPreset = PermissionPresetOption.CUSTOM + editorEntry = ShareEditorEntry.CUSTOMIZE_PERMISSION viewModel.setActiveShare(selected) }, onChangePreset = { selected, preset -> @@ -178,7 +179,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share }, onDeleteShare = { viewModel.deleteShare(it.id) }, onSendEmail = { selected -> - editorInitialPreset = null + editorEntry = ShareEditorEntry.SEND_EMAIL viewModel.setActiveShare(selected) } ) @@ -193,7 +194,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share share = activeShareObject, internalLink = internalLink, viewModel = viewModel, - initialPresetOption = editorInitialPreset, + entry = editorEntry, onDismissDraft = { draftShare -> viewModel.deleteShare(draftShare.id) viewModel.setActiveShare(null) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 55911083..a72d4975 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -67,6 +67,7 @@ import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory +import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.repository.MockShareRepository import kotlinx.coroutines.launch @@ -76,7 +77,7 @@ fun AddOrEditShareBottomSheet( share: Share, internalLink: String, viewModel: ShareViewModel, - initialPresetOption: PermissionPresetOption? = null, + entry: ShareEditorEntry = ShareEditorEntry.EDIT, onDismissDraft: (Share) -> Unit = {} ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -84,7 +85,11 @@ fun AddOrEditShareBottomSheet( var selectedCategory by remember(share.id) { mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) } - var showAdvancedSettings by remember { mutableStateOf(false) } + val initialPresetOption = when (entry) { + ShareEditorEntry.CUSTOMIZE_PERMISSION -> PermissionPresetOption.CUSTOM + else -> null + } + var showAdvancedSettings by remember(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } val context = LocalContext.current val propertyErrors by viewModel.propertyErrors.collectAsState() val hasPropertyErrors = propertyErrors.values.any { it != null } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt new file mode 100644 index 00000000..b4208f34 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +enum class ShareEditorEntry { + EDIT, + CUSTOMIZE_PERMISSION, + SEND_EMAIL +} From 1569b32f552b0d0dd06d98f979ff510ef668b184 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 13 Jul 2026 10:36:53 +0200 Subject: [PATCH 102/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 58 +++++++++++++++---- .../property/datepicker/ShareDatePicker.kt | 1 + .../share/model/api/permission/Permission.kt | 2 + .../ui/share/model/api/property/Property.kt | 6 ++ .../common/ui/share/model/api/share/Share.kt | 16 +++++ .../share/repository/MockShareRepository.kt | 7 +++ 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index a72d4975..f45fb550 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -141,14 +141,17 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) - if (share.properties.isNotEmpty() || share.customLinkRecipient != null) { - AdvancedSettingsSection( - share = share, - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings }, - viewModel = viewModel - ) - } + BasicSettingsSection( + share = share, + viewModel = viewModel + ) + + AdvancedSettingsSection( + share = share, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + viewModel = viewModel + ) if (hasPropertyErrors) { Text( @@ -279,6 +282,28 @@ private fun PermissionPresetDropdown( } } +@Composable +private fun BasicSettingsSection( + share: Share, + viewModel: ShareViewModel +) { + if (!share.isBasicSectionAvailable) { + return + } + + Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + share.basicProperties.forEach { property -> + key(property.clazz) { + SharePropertyView( + shareId = share.id, + property = property, + viewModel = viewModel + ) + } + } + } +} + @Composable private fun AdvancedSettingsSection( share: Share, @@ -286,12 +311,16 @@ private fun AdvancedSettingsSection( onToggle: () -> Unit, viewModel: ShareViewModel ) { + if (!share.isAdvancedSectionAvailable) { + return + } + CollapsibleShareSection( label = stringResource(R.string.share_view_advanced_settings), isExpanded = isExpanded, onToggle = onToggle ) { - share.properties.sortedBy { it.priority }.forEach { property -> + share.advancedProperties.forEach { property -> key(property.clazz) { SharePropertyView( shareId = share.id, @@ -450,18 +479,27 @@ private val previewShare = Share( ) ), properties = listOf( - PropertyString(clazz = "note", displayName = "Note", priority = 10, required = false, value = "") + PropertyString( + clazz = "note", + displayName = "Note", + priority = 10, + required = false, + advanced = false, + value = "" + ) ), permissions = listOf( Permission( clazz = "read", displayName = "Read", + priority = 10, presets = listOf(PermissionPreset.VIEW, PermissionPreset.EDIT), enabled = true ), Permission( clazz = "update", displayName = "Update", + priority = 20, presets = listOf(PermissionPreset.EDIT), enabled = false ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 0e8c76c9..6c8b6f51 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -115,6 +115,7 @@ private fun ShareDatePickerPreview() { displayName = "Expiration Date", priority = 1, required = false, + advanced = false, value = "2026-12-31", minDate = "2026-01-01", maxDate = "2027-12-31" diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt index d568ae08..3dd22299 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -23,6 +23,8 @@ data class Permission( val hint: String? = null, + val priority: Int, + val presets: List, val enabled: Boolean diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 27522128..8be8eaf1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -21,6 +21,7 @@ sealed class Property { abstract val hint: String? abstract val priority: Int abstract val required: Boolean + abstract val advanced: Boolean abstract val value: String? } @@ -32,6 +33,7 @@ data class PropertyBoolean( override val hint: String? = null, override val priority: Int, override val required: Boolean, + override val advanced: Boolean, override val value: String? = null ) : Property() { fun isTrue(): Boolean = value == "true" @@ -45,6 +47,7 @@ data class PropertyDate( override val hint: String? = null, override val priority: Int, override val required: Boolean, + override val advanced: Boolean, override val value: String? = null, @SerialName("min_date") val minDate: String? = null, @SerialName("max_date") val maxDate: String? = null @@ -58,6 +61,7 @@ data class PropertyEnum( override val hint: String? = null, override val priority: Int, override val required: Boolean, + override val advanced: Boolean, override val value: String? = null, @SerialName("valid_values") val validValues: List ) : Property() @@ -70,6 +74,7 @@ data class PropertyPassword( override val hint: String? = null, override val priority: Int, override val required: Boolean, + override val advanced: Boolean, override val value: String? = null ) : Property() @@ -81,6 +86,7 @@ data class PropertyString( override val hint: String? = null, override val priority: Int, override val required: Boolean, + override val advanced: Boolean, override val value: String? = null, @SerialName("min_length") val minLength: Int? = null, @SerialName("max_length") val maxLength: Int? = null diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index b28f82b6..a2882efc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -47,6 +47,22 @@ data class Share( @SerialName("permission_preset") val permissionPreset: PermissionPreset? = null ) { + val basicProperties: List + get() = properties.filterNot { it.advanced }.sortedBy { it.priority } + + val advancedProperties: List + get() = properties.filter { it.advanced }.sortedBy { it.priority } + + val isBasicSectionAvailable: Boolean + get() { + return basicProperties.isNotEmpty() + } + + val isAdvancedSectionAvailable: Boolean + get() { + return advancedProperties.isNotEmpty() || customLinkRecipient != null + } + fun toActiveShare(): ActiveShareState { return ActiveShareState.Editing(this) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 96c43283..ac024215 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -45,18 +45,21 @@ class MockShareRepository : ShareRepository { Permission( clazz = "read", displayName = "Read", + priority = 10, presets = listOf(PermissionPreset.VIEW, PermissionPreset.EDIT), enabled = true ), Permission( clazz = "update", displayName = "Update", + priority = 20, presets = listOf(PermissionPreset.EDIT), enabled = false ), Permission( clazz = "delete", displayName = "Delete", + priority = 30, presets = listOf(PermissionPreset.EDIT), enabled = false ) @@ -68,6 +71,7 @@ class MockShareRepository : ShareRepository { displayName = "Note", priority = 10, required = false, + advanced = false, value = "Design review – please check latest changes" ), PropertyPassword( @@ -75,6 +79,7 @@ class MockShareRepository : ShareRepository { displayName = "Password", priority = 20, required = false, + advanced = false, value = null ), PropertyDate( @@ -82,6 +87,7 @@ class MockShareRepository : ShareRepository { displayName = "Expiration date", priority = 30, required = false, + advanced = true, value = null, minDate = "2026-01-01", maxDate = "2027-01-01" @@ -91,6 +97,7 @@ class MockShareRepository : ShareRepository { displayName = "Hide download", priority = 40, required = false, + advanced = true, value = "false" ) ) From 87a0ac4840645e6e5fdea935e9d338c49b524679 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 13 Jul 2026 10:48:08 +0200 Subject: [PATCH 103/174] wip Signed-off-by: alperozturk96 --- .../ui/network/http/NextcloudHttpClient.kt | 33 +++++++++++-------- .../android/common/ui/share/ShareViewModel.kt | 5 +-- .../share/repository/MockShareRepository.kt | 11 +++++-- .../share/repository/ShareRemoteRepository.kt | 21 +++++++----- .../ui/share/repository/ShareRepository.kt | 5 +-- 5 files changed, 46 insertions(+), 29 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 29b462e6..32d2dcc7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -10,6 +10,7 @@ package com.nextcloud.android.common.ui.network.http import com.nextcloud.android.common.ui.BuildConfig import com.nextcloud.android.common.ui.network.auth.AuthInterceptor import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.model.Meta import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.Ocs import com.nextcloud.android.common.ui.network.model.OcsResponse @@ -31,6 +32,7 @@ class NextcloudHttpClient private constructor( private const val READ_TIMEOUT_SECONDS = 90L private const val WRITE_TIMEOUT_SECONDS = 90L private const val OCS_OK = "ok" + private const val OCS_FAILURE = "failure" fun create( credentials: ServerCredentials @@ -48,6 +50,8 @@ class NextcloudHttpClient private constructor( private val baseUrl get() = credentials.baseURL.trimEnd('/') + private fun errorMeta(code: Int): Meta = Meta(status = OCS_FAILURE, statusCode = code, message = "") + suspend fun executeRequest( endpoint: String, method: HttpMethod, @@ -59,24 +63,25 @@ class NextcloudHttpClient private constructor( val response = okHttpClient.newCall(request).execute() val responseBody = response.body.string() - if (!response.isSuccessful) { - debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = true) - return@withContext NetworkResult.ServerError( - OCSSerializer.json.decodeFromString>(responseBody) - ) - } - if (responseBody.isBlank()) { - debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = false) - return@withContext NetworkResult.Success(parse(responseBody)) + val isError = !response.isSuccessful + debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = isError) + return@withContext if (isError) { + NetworkResult.ServerError(OcsResponse(Ocs(errorMeta(response.code), ""))) + } else { + NetworkResult.Success(parse(responseBody)) + } } - val envelope = OCSSerializer.json.decodeFromString>(responseBody) - if (envelope.ocs.meta.status != OCS_OK) { - debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = true) - NetworkResult.ServerError(OcsResponse(Ocs(envelope.ocs.meta, envelope.ocs.meta.message))) + val meta = OCSSerializer.json + .decodeFromString>(responseBody) + .ocs.meta + val isError = !response.isSuccessful || meta.status != OCS_OK + debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = isError) + + if (isError) { + NetworkResult.ServerError(OcsResponse(Ocs(meta, meta.message))) } else { - debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = false) NetworkResult.Success(parse(responseBody)) } } catch (e: Exception) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 610f4b61..87f6808f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -108,7 +108,8 @@ class ShareViewModel( // region shares list fun fetchShares( - sourceClass: String? = null, + filterSourceTypeClass: String? = null, + filterSourceTypeValue: String? = null, lastShareID: String? = null, limit: Int = 50 ) { @@ -116,7 +117,7 @@ class ShareViewModel( _state.update { ShareScreenState.Loading } _errorMessageId.update { null } - val result = repository.fetchShares(sourceClass, lastShareID, limit) + val result = repository.fetchShares(filterSourceTypeClass, filterSourceTypeValue, lastShareID, limit) val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } ?: return@launch _state.update { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index ac024215..4de05ce4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -323,14 +323,19 @@ class MockShareRepository : ShareRepository { } override suspend fun fetchShares( - sourceClass: String?, + filterSourceTypeClass: String?, + filterSourceTypeValue: String?, lastShareID: String?, limit: Int ): NetworkResult> { var result = mockShares.toList() - if (sourceClass != null) { - result = result.filter { share -> share.sources.any { it.clazz == sourceClass } } + if (filterSourceTypeClass != null) { + result = result.filter { share -> + share.sources.any { + it.clazz == filterSourceTypeClass && (filterSourceTypeValue == null || it.value == filterSourceTypeValue) + } + } } if (lastShareID != null) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 71eccd8f..8a841e81 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -24,6 +24,9 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipi import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share import okhttp3.RequestBody.Companion.toRequestBody +import java.net.URLEncoder + +private fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name()) class ShareRemoteRepository( private val client: NextcloudHttpClient, @@ -44,8 +47,8 @@ class ShareRemoteRepository( offset: Int ): NetworkResult> { val queryParams = buildString { - append("?query=$query&limit=$limit&offset=$offset") - recipientTypeClasses?.forEach { append("&recipientTypeClasses[]=$it") } + append("?query=${query.urlEncoded()}&limit=$limit&offset=$offset") + recipientTypeClasses?.forEach { append("&recipientTypeClasses[]=${it.urlEncoded()}") } } return client.executeRequest( endpoint = "$RECIPIENTS_ENDPOINT$queryParams", @@ -83,14 +86,16 @@ class ShareRemoteRepository( ) { } override suspend fun fetchShares( - sourceClass: String?, + filterSourceTypeClass: String?, + filterSourceTypeValue: String?, lastShareID: String?, limit: Int ): NetworkResult> { val queryParams = buildString { append("?limit=$limit") - sourceClass?.let { append("&sourceClass=$it") } - lastShareID?.let { append("&lastShareID=$it") } + filterSourceTypeClass?.let { append("&filterSourceTypeClass=${it.urlEncoded()}") } + filterSourceTypeValue?.let { append("&filterSourceTypeValue=${it.urlEncoded()}") } + lastShareID?.let { append("&lastShareID=${it.urlEncoded()}") } } return client.executeRequest( endpoint = "$SHARES_ENDPOINT$queryParams", @@ -130,7 +135,7 @@ class ShareRemoteRepository( value: String ): NetworkResult = client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id/source?class=$clazz&value=$value", + endpoint = "$SHARE_ENDPOINT/$id/source?class=${clazz.urlEncoded()}&value=${value.urlEncoded()}", method = HttpMethod.DELETE ) { body -> json.decodeFromString>(body).ocs.data @@ -155,8 +160,8 @@ class ShareRemoteRepository( instance: String? ): NetworkResult { val queryParams = buildString { - append("?class=$clazz&value=$value") - instance?.let { append("&instance=$it") } + append("?class=${clazz.urlEncoded()}&value=${value.urlEncoded()}") + instance?.let { append("&instance=${it.urlEncoded()}") } } return client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/recipient$queryParams", diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 51bd49d2..b6c9d6a0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -37,8 +37,9 @@ interface ShareRepository { suspend fun deleteShare(id: String): NetworkResult suspend fun fetchShares( - sourceClass: String?, - lastShareID: String?, + filterSourceTypeClass: String? = null, + filterSourceTypeValue: String? = null, + lastShareID: String? = null, limit: Int ): NetworkResult> From 11745bcce0e6dc95037dec802394ad1c7ae83d90 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 13 Jul 2026 10:54:14 +0200 Subject: [PATCH 104/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/network/http/NextcloudHttpClient.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 32d2dcc7..2cbfecc8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -59,6 +59,7 @@ class NextcloudHttpClient private constructor( parse: (String) -> T ): NetworkResult = withContext(Dispatchers.IO) { val request = buildOcsRequest("$baseUrl$endpoint", method, body) + debugLogger.logRequest(endpoint, method, body) try { val response = okHttpClient.newCall(request).execute() val responseBody = response.body.string() From e8939d5a643257a9238918cf29ae865fabf115db Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 12:36:21 +0200 Subject: [PATCH 105/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 35 ++++++++++++------- .../android/common/ui/share/ShareViewModel.kt | 5 ++- .../bottomsheet/AddOrEditShareBottomSheet.kt | 26 ++++++++------ .../QuickSharePermissionBottomSheet.kt | 6 ++-- .../api/capabilities/SharingCapabilities.kt | 7 ++-- .../share/model/api/permission/Permission.kt | 5 ++- .../model/api/permission/PermissionPreset.kt | 14 ++++---- .../share/model/api/request/ShareRequests.kt | 3 +- .../common/ui/share/model/api/share/Share.kt | 3 +- .../share/model/ui/PermissionPresetOption.kt | 31 ++++++++++------ .../share/repository/MockShareRepository.kt | 9 +++-- ui/src/main/res/values/strings.xml | 2 -- 12 files changed, 89 insertions(+), 57 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 68a241fb..d18348bc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -87,11 +87,17 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.filtered +import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import kotlinx.coroutines.launch @Composable -private fun ShareScreen(sourceId: String, internalLink: String, viewModel: ShareViewModel) { +private fun ShareScreen( + sourceId: String, + internalLink: String, + viewModel: ShareViewModel, + permissionPresets: List +) { val errorMessageId by viewModel.errorMessageId.collectAsState() val screenState by viewModel.state.collectAsState() val activeShare by viewModel.activeShare.collectAsState() @@ -166,6 +172,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share share = share, title = title, type = type, + permissionPresets = permissionPresets, onSelectShare = { selected -> editorEntry = ShareEditorEntry.EDIT viewModel.setActiveShare(selected) @@ -194,6 +201,7 @@ private fun ShareScreen(sourceId: String, internalLink: String, viewModel: Share share = activeShareObject, internalLink = internalLink, viewModel = viewModel, + permissionPresets = permissionPresets, entry = editorEntry, onDismissDraft = { draftShare -> viewModel.deleteShare(draftShare.id) @@ -208,9 +216,10 @@ private fun ShareItem( share: Share, title: String, type: ShareItemType, + permissionPresets: List, onSelectShare: (Share) -> Unit, onCustomizeShare: (Share) -> Unit, - onChangePreset: (Share, PermissionPreset) -> Unit, + onChangePreset: (Share, String) -> Unit, onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit ) { @@ -220,12 +229,13 @@ private fun ShareItem( when (overlayState) { is ShareItemOverlayState.QuickShare -> { QuickSharePermissionBottomSheet( - selectedOption = PermissionPresetOption.from(share.permissionPreset), + options = PermissionPresetOption.optionsFor(share, permissionPresets), + selectedOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), onOptionSelected = { option -> overlayState = ShareItemOverlayState.None - val preset = option.preset - if (preset != null) { - onChangePreset(share, preset) + val presetClass = option.presetClass + if (presetClass != null) { + onChangePreset(share, presetClass) } else { onCustomizeShare(share) } @@ -282,7 +292,7 @@ private fun ShareItem( }, supportingContent = { val chipHorizontalPadding = 10.dp - val selectedLabelRes = PermissionPresetOption.from(share.permissionPreset).labelRes + val selectedLabel = PermissionPresetOption.from(share.permissionPreset, permissionPresets).label() Row( modifier = Modifier @@ -294,9 +304,9 @@ private fun ShareItem( verticalAlignment = Alignment.CenterVertically ) { Box(contentAlignment = Alignment.CenterStart) { - PermissionPresetOption.entries.forEach { option -> + PermissionPresetOption.optionsFor(share, permissionPresets).forEach { option -> Text( - text = stringResource(option.labelRes), + text = option.label(), style = MaterialTheme.typography.bodyMedium, maxLines = 1, modifier = Modifier @@ -306,7 +316,7 @@ private fun ShareItem( } Text( - text = stringResource(selectedLabelRes), + text = selectedLabel, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary, maxLines = 1 @@ -390,7 +400,8 @@ fun ComposeView.initShareScreen( sourceId: String, internalLink: String, credentials: ServerCredentials, - colorScheme: ColorScheme + colorScheme: ColorScheme, + permissionPresets: List = emptyList() ) { val nextcloudHttpClient = NextcloudHttpClient.create(credentials) val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) @@ -405,7 +416,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(sourceId, internalLink, viewModel) + ShareScreen(sourceId, internalLink, viewModel, permissionPresets) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 87f6808f..234b74c2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -12,7 +12,6 @@ import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.dataOrElse -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest @@ -308,9 +307,9 @@ class ShareViewModel( } } - fun updatePermissionPreset(id: String, preset: PermissionPreset, updateActiveShare: Boolean) { + fun updatePermissionPreset(id: String, presetClass: String, updateActiveShare: Boolean) { viewModelScope.launch(Dispatchers.IO) { - val result = repository.updateSharePermissionPreset(id, UpdateSharePermissionPresetRequest(preset)) + val result = repository.updateSharePermissionPreset(id, UpdateSharePermissionPresetRequest(presetClass)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch if (updateActiveShare) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index f45fb550..e6d25be6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -68,6 +68,7 @@ import com.nextcloud.android.common.ui.share.model.api.user.User import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry +import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.MockShareRepository import kotlinx.coroutines.launch @@ -77,6 +78,7 @@ fun AddOrEditShareBottomSheet( share: Share, internalLink: String, viewModel: ShareViewModel, + permissionPresets: List = emptyList(), entry: ShareEditorEntry = ShareEditorEntry.EDIT, onDismissDraft: (Share) -> Unit = {} ) { @@ -86,7 +88,7 @@ fun AddOrEditShareBottomSheet( mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) } val initialPresetOption = when (entry) { - ShareEditorEntry.CUSTOMIZE_PERMISSION -> PermissionPresetOption.CUSTOM + ShareEditorEntry.CUSTOMIZE_PERMISSION -> PermissionPresetOption.Custom else -> null } var showAdvancedSettings by remember(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } @@ -138,6 +140,7 @@ fun AddOrEditShareBottomSheet( PermissionsView( share = share, initialPresetOption = initialPresetOption, + permissionPresets = permissionPresets, viewModel = viewModel ) @@ -210,17 +213,19 @@ private fun ShareCategorySelector( private fun PermissionsView( share: Share, initialPresetOption: PermissionPresetOption?, + permissionPresets: List, viewModel: ShareViewModel ) { var selectedPreset by remember(share.id) { - mutableStateOf(if (initialPresetOption != null) initialPresetOption.preset else share.permissionPreset) + mutableStateOf(if (initialPresetOption != null) initialPresetOption.presetClass else share.permissionPreset) } PermissionPresetDropdown( - selectedOption = PermissionPresetOption.from(selectedPreset), + options = PermissionPresetOption.optionsFor(share, permissionPresets), + selectedOption = PermissionPresetOption.from(selectedPreset, permissionPresets), onOptionSelected = { option -> - selectedPreset = option.preset - option.preset?.let { viewModel.updatePermissionPreset(share.id, it, true) } + selectedPreset = option.presetClass + option.presetClass?.let { viewModel.updatePermissionPreset(share.id, it, true) } } ) @@ -244,6 +249,7 @@ private fun PermissionsView( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PermissionPresetDropdown( + options: List, selectedOption: PermissionPresetOption, onOptionSelected: (PermissionPresetOption) -> Unit ) { @@ -255,7 +261,7 @@ private fun PermissionPresetDropdown( onExpandedChange = { expanded = it } ) { OutlinedTextField( - value = stringResource(selectedOption.labelRes), + value = selectedOption.label(), onValueChange = {}, readOnly = true, label = { Text(stringResource(R.string.share_view_permission_preset_label)) }, @@ -269,9 +275,9 @@ private fun PermissionPresetDropdown( expanded = expanded, onDismissRequest = { expanded = false } ) { - PermissionPresetOption.entries.forEach { option -> + options.forEach { option -> DropdownMenuItem( - text = { Text(stringResource(option.labelRes)) }, + text = { Text(option.label()) }, onClick = { expanded = false onOptionSelected(option) @@ -493,14 +499,14 @@ private val previewShare = Share( clazz = "read", displayName = "Read", priority = 10, - presets = listOf(PermissionPreset.VIEW, PermissionPreset.EDIT), + presets = listOf("view", "edit"), enabled = true ), Permission( clazz = "update", displayName = "Update", priority = 20, - presets = listOf(PermissionPreset.EDIT), + presets = listOf("edit"), enabled = false ) ), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt index 0fdbbe75..399910d6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt @@ -28,10 +28,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption +import com.nextcloud.android.common.ui.share.model.ui.label @OptIn(ExperimentalMaterial3Api::class) @Composable fun QuickSharePermissionBottomSheet( + options: List, selectedOption: PermissionPresetOption, onOptionSelected: (PermissionPresetOption) -> Unit, onDismiss: () -> Unit @@ -55,10 +57,10 @@ fun QuickSharePermissionBottomSheet( modifier = Modifier.padding(16.dp) ) - PermissionPresetOption.entries.forEach { option -> + options.forEach { option -> ListItem( modifier = Modifier.clickable { onOptionSelected(option) }, - headlineContent = { Text(stringResource(option.labelRes)) }, + headlineContent = { Text(option.label()) }, trailingContent = { if (option == selectedOption) { Icon( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 04eb864d..e8da6884 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -7,14 +7,17 @@ package com.nextcloud.android.common.ui.share.model.api.capabilities +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class SharingCapabilities( @SerialName("api_versions") - val apiVersions: List, + val apiVersions: List = emptyList(), val legacy: Legacy? = null, @SerialName("source_types") - val sourceTypes: List, + val sourceTypes: List = emptyList(), + @SerialName("permission_presets") + val permissionPresets: List = emptyList(), ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt index 3dd22299..ccd405a5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -10,6 +10,9 @@ package com.nextcloud.android.common.ui.share.model.api.permission import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +/** + * [presets] holds the class identifiers of the [PermissionPreset]s this permission belongs to. + */ @Serializable data class Permission( @SerialName("class") @@ -25,7 +28,7 @@ data class Permission( val priority: Int, - val presets: List, + val presets: List, val enabled: Boolean ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt index fb4f321c..eb78df95 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt @@ -11,10 +11,12 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -enum class PermissionPreset { - @SerialName("view") - VIEW, +data class PermissionPreset( + @SerialName("class") + val clazz: String, - @SerialName("edit") - EDIT -} + @SerialName("display_name") + val displayName: String, + + val hint: String? = null +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt index c39733b3..3ca61b45 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt @@ -7,7 +7,6 @@ package com.nextcloud.android.common.ui.share.model.api.request -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.state.ShareState import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -61,7 +60,7 @@ data class UpdateSharePermissionRequest( @Serializable data class UpdateSharePermissionPresetRequest( - val permissionPreset: PermissionPreset + val permissionPresetClass: String ) @Serializable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index a2882efc..4a6b0482 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.toClipEntry import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.permission.Permission -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source @@ -45,7 +44,7 @@ data class Share( val permissions: List, @SerialName("permission_preset") - val permissionPreset: PermissionPreset? = null + val permissionPreset: String? = null ) { val basicProperties: List get() = properties.filterNot { it.advanced }.sortedBy { it.priority } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt index 886a0f75..73afcc48 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt @@ -7,19 +7,30 @@ package com.nextcloud.android.common.ui.share.model.ui -import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.share.Share -enum class PermissionPresetOption( - val preset: PermissionPreset?, - val labelRes: Int -) { - CUSTOM(null, R.string.share_view_permission_preset_custom), - VIEW(PermissionPreset.VIEW, R.string.share_view_permission_preset_view), - EDIT(PermissionPreset.EDIT, R.string.share_view_permission_preset_edit); +sealed interface PermissionPresetOption { + + val presetClass: String? + + data object Custom : PermissionPresetOption { + override val presetClass: String? = null + } + + data class Preset(val preset: PermissionPreset) : PermissionPresetOption { + override val presetClass: String get() = preset.clazz + } companion object { - fun from(preset: PermissionPreset?): PermissionPresetOption = - entries.firstOrNull { it.preset == preset } ?: CUSTOM + fun optionsFor(share: Share, presets: List): List { + val applicableClasses = share.permissions.flatMap { it.presets }.toSet() + return presets.filter { it.clazz in applicableClasses }.map { Preset(it) } + Custom + } + + fun from(presetClass: String?, presets: List): PermissionPresetOption { + if (presetClass == null) return Custom + return presets.firstOrNull { it.clazz == presetClass }?.let { Preset(it) } ?: Custom + } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 4de05ce4..ea33a9b5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -10,7 +10,6 @@ package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.permission.Permission -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword @@ -46,21 +45,21 @@ class MockShareRepository : ShareRepository { clazz = "read", displayName = "Read", priority = 10, - presets = listOf(PermissionPreset.VIEW, PermissionPreset.EDIT), + presets = listOf("view", "edit"), enabled = true ), Permission( clazz = "update", displayName = "Update", priority = 20, - presets = listOf(PermissionPreset.EDIT), + presets = listOf("edit"), enabled = false ), Permission( clazz = "delete", displayName = "Delete", priority = 30, - presets = listOf(PermissionPreset.EDIT), + presets = listOf("edit"), enabled = false ) ) @@ -474,7 +473,7 @@ class MockShareRepository : ShareRepository { val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) val updated = current.copy( - permissionPreset = request.permissionPreset, + permissionPreset = request.permissionPresetClass, lastUpdated = System.currentTimeMillis() ) if (index >= 0) mockShares[index] = updated diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index faa52182..032954aa 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -48,8 +48,6 @@ Change permission Participants Can… - Can view - Can edit Invited people Anyone From c2d21fbaf3638bdd59198ff7a648eb2b44472a9b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 12:36:28 +0200 Subject: [PATCH 106/174] wip Signed-off-by: alperozturk96 --- .../capabilities/SharingCapabilitiesParser.kt | 19 +++++++++++++++++++ .../model/ui/PermissionPresetOptionLabel.kt | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt new file mode 100644 index 00000000..19a2f692 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.capabilities + +import com.nextcloud.android.common.ui.network.serialization.OCSSerializer + +object SharingCapabilitiesParser { + fun parse(sharingJson: String?): SharingCapabilities? { + if (sharingJson.isNullOrEmpty()) return null + return runCatching { + OCSSerializer.json.decodeFromString(sharingJson) + }.getOrNull() + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt new file mode 100644 index 00000000..f36a5e28 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.nextcloud.android.common.ui.R + +@Composable +fun PermissionPresetOption.label(): String = when (this) { + PermissionPresetOption.Custom -> stringResource(R.string.share_view_permission_preset_custom) + is PermissionPresetOption.Preset -> preset.displayName +} From e9df3bc087265e530f6afdaf93c6b0fab77f8c73 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 12:45:42 +0200 Subject: [PATCH 107/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 9 ++++----- .../android/common/ui/share/ShareViewModel.kt | 14 ++++++++++++++ .../capabilities/SharingCapabilitiesParser.kt | 19 ------------------- .../share/repository/MockShareRepository.kt | 12 ++++++++++++ .../share/repository/ShareRemoteRepository.kt | 17 +++++++++++++++++ .../ui/share/repository/ShareRepository.kt | 3 +++ 6 files changed, 50 insertions(+), 24 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index d18348bc..0729b38a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -95,12 +95,12 @@ import kotlinx.coroutines.launch private fun ShareScreen( sourceId: String, internalLink: String, - viewModel: ShareViewModel, - permissionPresets: List + viewModel: ShareViewModel ) { val errorMessageId by viewModel.errorMessageId.collectAsState() val screenState by viewModel.state.collectAsState() val activeShare by viewModel.activeShare.collectAsState() + val permissionPresets by viewModel.permissionPresets.collectAsState() val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current @@ -400,8 +400,7 @@ fun ComposeView.initShareScreen( sourceId: String, internalLink: String, credentials: ServerCredentials, - colorScheme: ColorScheme, - permissionPresets: List = emptyList() + colorScheme: ColorScheme ) { val nextcloudHttpClient = NextcloudHttpClient.create(credentials) val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) @@ -416,7 +415,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(sourceId, internalLink, viewModel, permissionPresets) + ShareScreen(sourceId, internalLink, viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 234b74c2..53d1134b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.dataOrElse +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest @@ -63,6 +64,9 @@ class ShareViewModel( private val _recipientSearchResults = MutableStateFlow>(emptyList()) val recipientSearchResults: StateFlow> = _recipientSearchResults + private val _permissionPresets = MutableStateFlow>(emptyList()) + val permissionPresets: StateFlow> = _permissionPresets + private val _errorMessageId = MutableStateFlow(null) val errorMessageId: StateFlow = _errorMessageId @@ -79,6 +83,16 @@ class ShareViewModel( init { fetchShares() initSearchQuery() + fetchSharingCapabilities() + } + + private fun fetchSharingCapabilities() { + viewModelScope.launch(Dispatchers.IO) { + val result = repository.fetchSharingCapabilities() + if (result is NetworkResult.Success) { + _permissionPresets.update { result.data.permissionPresets } + } + } } // region search query diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt deleted file mode 100644 index 19a2f692..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilitiesParser.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.capabilities - -import com.nextcloud.android.common.ui.network.serialization.OCSSerializer - -object SharingCapabilitiesParser { - fun parse(sharingJson: String?): SharingCapabilities? { - if (sharingJson.isNullOrEmpty()) return null - return runCatching { - OCSSerializer.json.decodeFromString(sharingJson) - }.getOrNull() - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index ea33a9b5..25876385 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -8,8 +8,10 @@ package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.PropertyBoolean import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword @@ -479,4 +481,14 @@ class MockShareRepository : ShareRepository { if (index >= 0) mockShares[index] = updated return NetworkResult.Success(updated) } + + override suspend fun fetchSharingCapabilities(): NetworkResult = + NetworkResult.Success( + SharingCapabilities( + permissionPresets = listOf( + PermissionPreset(clazz = "view", displayName = "Can view"), + PermissionPreset(clazz = "edit", displayName = "Can edit") + ) + ) + ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 8a841e81..d81dc7b5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -13,6 +13,7 @@ import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.OcsResponse import com.nextcloud.android.common.ui.network.serialization.OCSSerializer +import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest @@ -23,6 +24,9 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareProper import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject import okhttp3.RequestBody.Companion.toRequestBody import java.net.URLEncoder @@ -38,6 +42,9 @@ class ShareRemoteRepository( private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares" private const val RECIPIENTS_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/recipients" private const val SECRET_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/secret" + private const val CAPABILITIES_ENDPOINT = "/ocs/v2.php/cloud/capabilities" + private const val NODE_CAPABILITIES = "capabilities" + private const val NODE_SHARING = "sharing" } override suspend fun fetchRecipients( @@ -226,4 +233,14 @@ class ShareRemoteRepository( ) { body -> json.decodeFromString>(body).ocs.data } + + override suspend fun fetchSharingCapabilities(): NetworkResult = + client.executeRequest( + endpoint = CAPABILITIES_ENDPOINT, + method = HttpMethod.GET + ) { body -> + val sharing = json.decodeFromString>(body) + .ocs.data[NODE_CAPABILITIES]?.jsonObject?.get(NODE_SHARING) + sharing?.let { json.decodeFromJsonElement(it) } ?: SharingCapabilities() + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index b6c9d6a0..118311e7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share.repository import com.nextcloud.android.common.ui.network.model.NetworkResult +import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest @@ -92,4 +93,6 @@ interface ShareRepository { ): NetworkResult suspend fun generateSecret(): NetworkResult + + suspend fun fetchSharingCapabilities(): NetworkResult } From d8be4a51efbac3c20473b0aab010f5b96f210407 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 14:18:17 +0200 Subject: [PATCH 108/174] wip Signed-off-by: alperozturk96 --- .../ui/network/http/NextcloudHttpClient.kt | 10 +++++++--- .../android/common/ui/share/ShareViewModel.kt | 17 +++++++++++++++-- .../bottomsheet/AddOrEditShareBottomSheet.kt | 8 ++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index 2cbfecc8..ff1e3b26 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -18,6 +18,7 @@ import com.nextcloud.android.common.ui.network.serialization.OCSSerializer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive import okhttp3.OkHttpClient import okhttp3.RequestBody import java.util.concurrent.TimeUnit @@ -74,14 +75,17 @@ class NextcloudHttpClient private constructor( } } - val meta = OCSSerializer.json + val ocs = OCSSerializer.json .decodeFromString>(responseBody) - .ocs.meta + .ocs + val meta = ocs.meta val isError = !response.isSuccessful || meta.status != OCS_OK debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = isError) if (isError) { - NetworkResult.ServerError(OcsResponse(Ocs(meta, meta.message))) + val dataMessage = (ocs.data as? JsonPrimitive)?.takeIf { it.isString }?.content + val message = dataMessage?.takeIf { it.isNotBlank() } ?: meta.message + NetworkResult.ServerError(OcsResponse(Ocs(meta, message))) } else { NetworkResult.Success(parse(responseBody)) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 53d1134b..9b6fd788 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -73,6 +73,9 @@ class ShareViewModel( private val _propertyErrors = MutableStateFlow>(emptyMap()) val propertyErrors: StateFlow> = _propertyErrors + private val _pendingProperties = MutableStateFlow>(emptySet()) + val pendingProperties: StateFlow> = _pendingProperties + private val propertyUpdateJobs = mutableMapOf() private var secretUpdateJob: Job? = null @@ -290,6 +293,10 @@ class ShareViewModel( // region properties fun updateProperty(shareId: String, clazz: String, value: String?) { propertyUpdateJobs[clazz]?.cancel() + _pendingProperties.update { it + clazz } + if (value.isNullOrEmpty()) { + _propertyErrors.update { it - clazz } + } propertyUpdateJobs[clazz] = viewModelScope.launch(Dispatchers.IO) { delay(PROPERTY_DEBOUNCE_DELAY.milliseconds) when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { @@ -300,13 +307,18 @@ class ShareViewModel( } is NetworkResult.ServerError -> { - _propertyErrors.update { it + (clazz to result.response.ocs.meta.message) } + if (!value.isNullOrEmpty()) { + _propertyErrors.update { it + (clazz to result.response.ocs.data) } + } } is NetworkResult.NetworkException -> { - _propertyErrors.update { it + (clazz to null) } + if (!value.isNullOrEmpty()) { + _propertyErrors.update { it + (clazz to null) } + } } } + _pendingProperties.update { it - clazz } } } // endregion @@ -361,6 +373,7 @@ class ShareViewModel( fun setActiveShare(value: Share?) { _propertyErrors.update { emptyMap() } + _pendingProperties.update { emptySet() } _activeShare.update { value?.toActiveShare() ?: ActiveShareState.None } } // endregion diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index e6d25be6..9607793b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -94,7 +94,9 @@ fun AddOrEditShareBottomSheet( var showAdvancedSettings by remember(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } val context = LocalContext.current val propertyErrors by viewModel.propertyErrors.collectAsState() - val hasPropertyErrors = propertyErrors.values.any { it != null } + val pendingProperties by viewModel.pendingProperties.collectAsState() + val hasPropertyErrors = propertyErrors.isNotEmpty() + val sendEnabled = !hasPropertyErrors && pendingProperties.isEmpty() Column( modifier = Modifier @@ -172,7 +174,7 @@ fun AddOrEditShareBottomSheet( share = share, internalLink = internalLink, category = selectedCategory, - sendEnabled = !hasPropertyErrors, + sendEnabled = sendEnabled, viewModel = viewModel ) } @@ -384,6 +386,7 @@ private fun ActionButtons( } } }, + enabled = category != ShareCategory.Anyone || sendEnabled, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( @@ -430,6 +433,7 @@ private fun ActionButtons( onClick = { viewModel.updateState(share.id, ShareState.ACTIVE) }, + enabled = sendEnabled, modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( From 731d4f9bd68f77f4c2e23ece210122f96134f4a7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 14:28:35 +0200 Subject: [PATCH 109/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 10 ++- .../share/component/SelectRecipientField.kt | 1 - .../component/property/SharePropertyView.kt | 86 +++++++++++-------- .../common/ui/share/model/api/share/Share.kt | 17 ++-- .../share/repository/ShareRemoteRepository.kt | 47 +++------- 5 files changed, 79 insertions(+), 82 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 0729b38a..bce5a019 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -225,12 +225,14 @@ private fun ShareItem( ) { var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) } val haptics = LocalHapticFeedback.current + val presetOptions = PermissionPresetOption.optionsFor(share, permissionPresets) + val selectedPresetOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets) when (overlayState) { is ShareItemOverlayState.QuickShare -> { QuickSharePermissionBottomSheet( - options = PermissionPresetOption.optionsFor(share, permissionPresets), - selectedOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), + options = presetOptions, + selectedOption = selectedPresetOption, onOptionSelected = { option -> overlayState = ShareItemOverlayState.None val presetClass = option.presetClass @@ -292,7 +294,7 @@ private fun ShareItem( }, supportingContent = { val chipHorizontalPadding = 10.dp - val selectedLabel = PermissionPresetOption.from(share.permissionPreset, permissionPresets).label() + val selectedLabel = selectedPresetOption.label() Row( modifier = Modifier @@ -304,7 +306,7 @@ private fun ShareItem( verticalAlignment = Alignment.CenterVertically ) { Box(contentAlignment = Alignment.CenterStart) { - PermissionPresetOption.optionsFor(share, permissionPresets).forEach { option -> + presetOptions.forEach { option -> Text( text = option.label(), style = MaterialTheme.typography.bodyMedium, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index bbb8ac83..8479e13f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -7,7 +7,6 @@ package com.nextcloud.android.common.ui.share.component -import android.graphics.Color import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index a5318ddb..fb4b9ec0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel @@ -68,43 +69,25 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM } } - is PropertyString -> { - var textValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } + is PropertyString -> ShareTextPropertyField( + shareId = shareId, + clazz = property.clazz, + displayName = property.displayName, + initialValue = property.value ?: "", + errorMessage = errorMessage, + viewModel = viewModel + ) - OutlinedTextField( - value = textValue, - onValueChange = { - textValue = it - viewModel.updateProperty(shareId, property.clazz, it) - }, - label = { Text(property.displayName) }, - isError = errorMessage != null, - supportingText = errorMessage?.let { { Text(it) } }, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - singleLine = true - ) - } - - is PropertyPassword -> { - var passwordValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } - - OutlinedTextField( - value = passwordValue, - onValueChange = { - passwordValue = it - viewModel.updateProperty(shareId, property.clazz, it) - }, - label = { Text(property.displayName) }, - placeholder = property.hint?.let { { Text(it) } }, - visualTransformation = PasswordVisualTransformation(), - isError = errorMessage != null, - supportingText = errorMessage?.let { { Text(it) } }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - singleLine = true - ) - } + is PropertyPassword -> ShareTextPropertyField( + shareId = shareId, + clazz = property.clazz, + displayName = property.displayName, + initialValue = property.value ?: "", + errorMessage = errorMessage, + viewModel = viewModel, + hint = property.hint, + visualTransformation = PasswordVisualTransformation() + ) is PropertyDate -> { ShareDatePicker(property, errorMessage = errorMessage, onDateSelected = { dateValue -> @@ -121,3 +104,34 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM } } } + +@Composable +private fun ShareTextPropertyField( + shareId: String, + clazz: String, + displayName: String, + initialValue: String, + errorMessage: String?, + viewModel: ShareViewModel, + hint: String? = null, + visualTransformation: VisualTransformation = VisualTransformation.None +) { + var value by remember(clazz) { mutableStateOf(initialValue) } + + OutlinedTextField( + value = value, + onValueChange = { + value = it + viewModel.updateProperty(shareId, clazz, it) + }, + label = { Text(displayName) }, + placeholder = hint?.let { { Text(it) } }, + visualTransformation = visualTransformation, + isError = errorMessage != null, + supportingText = errorMessage?.let { { Text(it) } }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + singleLine = true + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 4a6b0482..7d807e5a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -46,11 +46,13 @@ data class Share( @SerialName("permission_preset") val permissionPreset: String? = null ) { - val basicProperties: List - get() = properties.filterNot { it.advanced }.sortedBy { it.priority } + val basicProperties: List by lazy { + properties.filterNot { it.advanced }.sortedBy { it.priority } + } - val advancedProperties: List - get() = properties.filter { it.advanced }.sortedBy { it.priority } + val advancedProperties: List by lazy { + properties.filter { it.advanced }.sortedBy { it.priority } + } val isBasicSectionAvailable: Boolean get() { @@ -96,10 +98,9 @@ data class Share( return customLinkRecipient != null } - val customLinkRecipient: Recipient? - get() { - return recipients.firstOrNull { it.secret.updatable } - } + val customLinkRecipient: Recipient? by lazy { + recipients.firstOrNull { it.secret.updatable } + } val canSend: Boolean get() { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index d81dc7b5..a454e806 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -47,6 +47,9 @@ class ShareRemoteRepository( private const val NODE_SHARING = "sharing" } + private fun decodeShare(body: String): Share = + json.decodeFromString>(body).ocs.data + override suspend fun fetchRecipients( recipientTypeClasses: List?, query: String, @@ -70,9 +73,7 @@ class ShareRemoteRepository( endpoint = SHARE_ENDPOINT, method = HttpMethod.POST, body = ByteArray(0).toRequestBody() - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun fetchShare( id: String, @@ -82,9 +83,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id", method = HttpMethod.POST, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun deleteShare(id: String): NetworkResult = client.executeRequest( @@ -120,9 +119,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/state", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun addShareSource( id: String, @@ -132,9 +129,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/source", method = HttpMethod.POST, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun removeShareSource( id: String, @@ -144,9 +139,7 @@ class ShareRemoteRepository( client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/source?class=${clazz.urlEncoded()}&value=${value.urlEncoded()}", method = HttpMethod.DELETE - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun addShareRecipient( id: String, @@ -156,9 +149,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/recipient", method = HttpMethod.POST, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun removeShareRecipient( id: String, @@ -173,9 +164,7 @@ class ShareRemoteRepository( return client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/recipient$queryParams", method = HttpMethod.DELETE - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } } override suspend fun updateShareProperty( @@ -186,9 +175,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/property", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun updateSharePermission( id: String, @@ -198,9 +185,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/permission", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun updateSharePermissionPreset( id: String, @@ -210,9 +195,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/permission/preset", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun updateShareRecipientSecret( id: String, @@ -222,9 +205,7 @@ class ShareRemoteRepository( endpoint = "$SHARE_ENDPOINT/$id/recipient/secret", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { body -> - json.decodeFromString>(body).ocs.data - } + ) { decodeShare(it) } override suspend fun generateSecret(): NetworkResult = client.executeRequest( From 53975ac0d7bf96f366d572b6c253fb567612a222 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 12:27:18 +0200 Subject: [PATCH 110/174] wip Signed-off-by: alperozturk96 --- .../ui/share/model/api/capabilities/Legacy.kt | 19 ------------------- .../api/capabilities/SharingCapabilities.kt | 1 - 2 files changed, 20 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt deleted file mode 100644 index cef0c5b9..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/Legacy.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.capabilities - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Legacy( - @SerialName("max_sources") - val maxSources: Long, - @SerialName("max_recipients") - val maxRecipients: Long, -) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index e8da6884..27c52b3b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -15,7 +15,6 @@ import kotlinx.serialization.Serializable data class SharingCapabilities( @SerialName("api_versions") val apiVersions: List = emptyList(), - val legacy: Legacy? = null, @SerialName("source_types") val sourceTypes: List = emptyList(), @SerialName("permission_presets") From 1173ce6af07d45a93cbf7190bd0696bc311309ea Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 13:33:18 +0200 Subject: [PATCH 111/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 4 +++- .../android/common/ui/share/model/api/share/Share.kt | 11 +++++++++++ .../ui/share/model/ui/PermissionPresetOption.kt | 6 +++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 9b6fd788..263d492f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -93,7 +93,9 @@ class ShareViewModel( viewModelScope.launch(Dispatchers.IO) { val result = repository.fetchSharingCapabilities() if (result is NetworkResult.Success) { - _permissionPresets.update { result.data.permissionPresets } + _permissionPresets.update { + result.data.permissionPresets + } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 7d807e5a..072884ab 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -102,6 +102,17 @@ data class Share( recipients.firstOrNull { it.secret.updatable } } + val defaultPermissionPresetClass: String? by lazy { + val enabledClasses = permissions.filter { it.enabled }.map { it.clazz }.toSet() + if (enabledClasses.isEmpty()) { + null + } else { + permissions.flatMap { it.presets }.distinct().firstOrNull { presetClass -> + permissions.filter { presetClass in it.presets }.map { it.clazz }.toSet() == enabledClasses + } + } + } + val canSend: Boolean get() { return sources.isNotEmpty() && diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt index 73afcc48..13f1435d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt @@ -25,7 +25,11 @@ sealed interface PermissionPresetOption { companion object { fun optionsFor(share: Share, presets: List): List { val applicableClasses = share.permissions.flatMap { it.presets }.toSet() - return presets.filter { it.clazz in applicableClasses }.map { Preset(it) } + Custom + val defaultClass = share.defaultPermissionPresetClass + return presets + .filter { it.clazz in applicableClasses } + .sortedByDescending { it.clazz == defaultClass } + .map { Preset(it) } + Custom } fun from(presetClass: String?, presets: List): PermissionPresetOption { From ff1cde0dfc7023cc2fd1379b311196465c00bc0a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 13:41:52 +0200 Subject: [PATCH 112/174] wip Signed-off-by: alperozturk96 --- .../property/datepicker/ShareDatePicker.kt | 5 ++++- .../datepicker/util/NoPastSelectableDates.kt | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 6c8b6f51..153e6f51 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.component.property.datepicker.util.NoPastSelectableDates import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate @@ -80,7 +81,9 @@ private fun DatePickerModal( onDateSelected: (displayDate: String?, isoDate: String?) -> Unit, onDismiss: () -> Unit ) { - val datePickerState = rememberDatePickerState() + val datePickerState = rememberDatePickerState( + selectableDates = remember { NoPastSelectableDates() } + ) DatePickerDialog( onDismissRequest = onDismiss, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt new file mode 100644 index 00000000..0fdc2c3c --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component.property.datepicker.util + +import androidx.compose.material3.SelectableDates +import java.time.LocalDate +import java.time.ZoneOffset + +class NoPastSelectableDates : SelectableDates { + + private val today: LocalDate = LocalDate.now(ZoneOffset.UTC) + private val todayUtcMillis: Long = today.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() + + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= todayUtcMillis + + override fun isSelectableYear(year: Int): Boolean = year >= today.year +} From 300c10284e9db901258af8238e1ebe406471137b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 13:51:56 +0200 Subject: [PATCH 113/174] wip Signed-off-by: alperozturk96 --- .../datepicker/util/ShareDateFormatterTest.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt diff --git a/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt b/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt new file mode 100644 index 00000000..c4661a51 --- /dev/null +++ b/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt @@ -0,0 +1,82 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component.property.datepicker.util + +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Before +import org.junit.Test +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale + +class ShareDateFormatterTest { + + private lateinit var originalLocale: Locale + + @Before + fun setUp() { + originalLocale = Locale.getDefault() + } + + @After + fun tearDown() { + Locale.setDefault(originalLocale) + } + + @Test + fun `displays expiration date with Turkish format when device locale is Turkish`() { + assertLocalizedDisplay(TURKISH) + } + + @Test + fun `displays expiration date with English format when device locale is English`() { + assertLocalizedDisplay(ENGLISH) + } + + @Test + fun `displays expiration date with device locale format for other languages`() { + assertLocalizedDisplay(GERMAN) + } + + @Test + fun `expiration date display differs per device locale`() { + val turkish = displayFor(TURKISH) + val english = displayFor(ENGLISH) + val german = displayFor(GERMAN) + + assertNotEquals(turkish, english) + assertNotEquals(turkish, german) + assertNotEquals(english, german) + } + + private fun assertLocalizedDisplay(locale: Locale) { + val expected = DateTimeFormatter + .ofLocalizedDate(FormatStyle.SHORT) + .withLocale(locale) + .format(DATE) + + assertEquals(expected, displayFor(locale)) + } + + private fun displayFor(locale: Locale): String { + Locale.setDefault(locale) + return ShareDateFormatter().formatIsoForDisplay(ISO_DATE) + } + + private companion object { + private val TURKISH: Locale = Locale.forLanguageTag("tr") + private val ENGLISH: Locale = Locale.forLanguageTag("en") + private val GERMAN: Locale = Locale.forLanguageTag("de") + + private const val ISO_DATE = "2026-12-31" + private val DATE: LocalDate = LocalDate.of(2026, 12, 31) + } +} From cafbdf6b877f18c330122560f99405031ea4a454 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 14:44:21 +0200 Subject: [PATCH 114/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 4 - .../common/ui/share/component/CustomLink.kt | 83 ++++++++++++------- .../component/property/SharePropertyView.kt | 17 ++-- 3 files changed, 67 insertions(+), 37 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 263d492f..6fb916bc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -32,7 +32,6 @@ import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce @@ -50,7 +49,6 @@ class ShareViewModel( companion object { private const val SEARCH_DEBOUNCE_DELAY = 300L - private const val PROPERTY_DEBOUNCE_DELAY = 1000L } private val _state = MutableStateFlow(ShareScreenState.Loading) @@ -270,7 +268,6 @@ class ShareViewModel( fun updateRecipientSecret(shareId: String, recipient: Recipient, secret: String) { secretUpdateJob?.cancel() secretUpdateJob = viewModelScope.launch(Dispatchers.IO) { - delay(PROPERTY_DEBOUNCE_DELAY.milliseconds) val request = UpdateShareRecipientSecretRequest( clazz = recipient.clazz, value = recipient.value, @@ -300,7 +297,6 @@ class ShareViewModel( _propertyErrors.update { it - clazz } } propertyUpdateJobs[clazz] = viewModelScope.launch(Dispatchers.IO) { - delay(PROPERTY_DEBOUNCE_DELAY.milliseconds) when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { is NetworkResult.Success -> { _propertyErrors.update { it - clazz } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index 8c581c52..e747b63e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -39,13 +40,11 @@ fun CustomLink( onGenerateSecret: suspend () -> String?, onTokenChange: (String) -> Unit ) { - val scope = rememberCoroutineScope() val prefix = remember(recipient.secret.url, recipient.secret.value) { val url = recipient.secret.url.orEmpty() val token = recipient.secret.value.orEmpty() if (token.isNotEmpty() && url.endsWith(token)) url.removeSuffix(token) else url } - var token by remember(recipient.value) { mutableStateOf(recipient.secret.value ?: "") } Column( modifier = Modifier @@ -66,32 +65,60 @@ fun CustomLink( modifier = Modifier.padding(top = 4.dp, bottom = 8.dp) ) - OutlinedTextField( - value = token, - onValueChange = { newValue -> - val trimmed = newValue.take(MAX_TOKEN_LENGTH) - token = trimmed - if (trimmed.isNotBlank()) onTokenChange(trimmed) - }, - label = { Text(prefix) }, - singleLine = true, - trailingIcon = { - IconButton( - onClick = { - scope.launch { - val generated = onGenerateSecret() ?: return@launch - token = generated - onTokenChange(generated) - } - } - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = stringResource(R.string.share_view_custom_link_refresh) - ) - } - }, - modifier = Modifier.fillMaxWidth() + CustomLinkTokenField( + initialToken = recipient.secret.value ?: "", + prefix = prefix, + onGenerateSecret = onGenerateSecret, + onCommit = onTokenChange ) } } + +@Composable +private fun CustomLinkTokenField( + initialToken: String, + prefix: String, + onGenerateSecret: suspend () -> String?, + onCommit: (String) -> Unit +) { + val scope = rememberCoroutineScope() + var token by remember(initialToken) { mutableStateOf(initialToken) } + var committedToken by remember(initialToken) { mutableStateOf(initialToken) } + var wasFocused by remember(initialToken) { mutableStateOf(false) } + + fun commit() { + if (token == committedToken || token.isBlank()) return + committedToken = token + onCommit(token) + } + + OutlinedTextField( + value = token, + onValueChange = { newValue -> token = newValue.take(MAX_TOKEN_LENGTH) }, + label = { Text(prefix) }, + singleLine = true, + trailingIcon = { + IconButton( + onClick = { + scope.launch { + val generated = onGenerateSecret() ?: return@launch + token = generated + committedToken = generated + onCommit(generated) + } + } + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(R.string.share_view_custom_link_refresh) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { focusState -> + if (wasFocused && !focusState.isFocused) commit() + wasFocused = focusState.isFocused + } + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index fb4b9ec0..c181e5d4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -117,13 +118,12 @@ private fun ShareTextPropertyField( visualTransformation: VisualTransformation = VisualTransformation.None ) { var value by remember(clazz) { mutableStateOf(initialValue) } + var committedValue by remember(clazz) { mutableStateOf(initialValue) } + var wasFocused by remember(clazz) { mutableStateOf(false) } OutlinedTextField( value = value, - onValueChange = { - value = it - viewModel.updateProperty(shareId, clazz, it) - }, + onValueChange = { value = it }, label = { Text(displayName) }, placeholder = hint?.let { { Text(it) } }, visualTransformation = visualTransformation, @@ -131,7 +131,14 @@ private fun ShareTextPropertyField( supportingText = errorMessage?.let { { Text(it) } }, modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), + .padding(vertical = 4.dp) + .onFocusChanged { focusState -> + if (wasFocused && !focusState.isFocused && value != committedValue) { + committedValue = value + viewModel.updateProperty(shareId, clazz, value) + } + wasFocused = focusState.isFocused + }, singleLine = true ) } From 4d1e76f38f6da7d9186a578a44ef2272255c2355 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Jul 2026 15:40:34 +0200 Subject: [PATCH 115/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 3 +- .../common/ui/share/model/api/share/Share.kt | 21 ++-- .../share/model/ui/PermissionPresetOption.kt | 6 +- .../ShareDefaultPermissionPresetOptionTest.kt | 102 ++++++++++++++++++ 4 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 9607793b..bea931a0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -219,7 +219,8 @@ private fun PermissionsView( viewModel: ShareViewModel ) { var selectedPreset by remember(share.id) { - mutableStateOf(if (initialPresetOption != null) initialPresetOption.presetClass else share.permissionPreset) + val default = share.getDefaultPermissionPresetOption(permissionPresets).presetClass + mutableStateOf(initialPresetOption?.presetClass ?: share.permissionPreset ?: default) } PermissionPresetDropdown( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 072884ab..0f3a303d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -13,12 +13,14 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.toClipEntry import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -102,15 +104,18 @@ data class Share( recipients.firstOrNull { it.secret.updatable } } - val defaultPermissionPresetClass: String? by lazy { - val enabledClasses = permissions.filter { it.enabled }.map { it.clazz }.toSet() - if (enabledClasses.isEmpty()) { - null - } else { - permissions.flatMap { it.presets }.distinct().firstOrNull { presetClass -> - permissions.filter { presetClass in it.presets }.map { it.clazz }.toSet() == enabledClasses - } + // The preset matching the enabled permissions dictated by the server admin: + // only the read permission -> "Can view", every permission -> "Can edit", any other subset -> "Can ...". + fun getDefaultPermissionPresetOption(permissionPresets: List): PermissionPresetOption { + val presetControlled = permissions.filter { it.presets.isNotEmpty() } + val enabledClasses = presetControlled.filter { it.enabled }.map { it.clazz }.toSet() + if (enabledClasses.isEmpty()) return PermissionPresetOption.Custom + + val matchingPreset = permissionPresets.firstOrNull { preset -> + presetControlled.filter { preset.clazz in it.presets }.map { it.clazz }.toSet() == enabledClasses } + + return matchingPreset?.let { PermissionPresetOption.Preset(it) } ?: PermissionPresetOption.Custom } val canSend: Boolean diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt index 13f1435d..73afcc48 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt @@ -25,11 +25,7 @@ sealed interface PermissionPresetOption { companion object { fun optionsFor(share: Share, presets: List): List { val applicableClasses = share.permissions.flatMap { it.presets }.toSet() - val defaultClass = share.defaultPermissionPresetClass - return presets - .filter { it.clazz in applicableClasses } - .sortedByDescending { it.clazz == defaultClass } - .map { Preset(it) } + Custom + return presets.filter { it.clazz in applicableClasses }.map { Preset(it) } + Custom } fun from(presetClass: String?, presets: List): PermissionPresetOption { diff --git a/ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt b/ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt new file mode 100644 index 00000000..ec43d7a5 --- /dev/null +++ b/ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt @@ -0,0 +1,102 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.share + +import com.nextcloud.android.common.ui.share.model.api.icon.Icon +import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.api.user.User +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption +import org.junit.Assert.assertEquals +import org.junit.Test + +class ShareDefaultPermissionPresetOptionTest { + + @Test + fun `returns view preset when only read is enabled`() { + val share = shareWithEnabled(read = true, update = false, delete = false) + + val result = share.getDefaultPermissionPresetOption(PRESETS) + + assertEquals(PermissionPresetOption.Preset(VIEW_PRESET), result) + } + + @Test + fun `returns edit preset when all permissions are enabled`() { + val share = shareWithEnabled(read = true, update = true, delete = true) + + val result = share.getDefaultPermissionPresetOption(PRESETS) + + assertEquals(PermissionPresetOption.Preset(EDIT_PRESET), result) + } + + @Test + fun `returns custom when only some permissions are enabled`() { + val share = shareWithEnabled(read = true, update = true, delete = false) + + val result = share.getDefaultPermissionPresetOption(PRESETS) + + assertEquals(PermissionPresetOption.Custom, result) + } + + @Test + fun `returns custom when no permission is enabled`() { + val share = shareWithEnabled(read = false, update = false, delete = false) + + val result = share.getDefaultPermissionPresetOption(PRESETS) + + assertEquals(PermissionPresetOption.Custom, result) + } + + @Test + fun `ignores enabled permissions that belong to no preset`() { + val share = shareWith( + permission("read", listOf("view", "edit"), enabled = true), + permission("update", listOf("edit"), enabled = false), + permission("delete", listOf("edit"), enabled = false), + permission("download", emptyList(), enabled = true) + ) + + val result = share.getDefaultPermissionPresetOption(PRESETS) + + assertEquals(PermissionPresetOption.Preset(VIEW_PRESET), result) + } + + private fun shareWithEnabled(read: Boolean, update: Boolean, delete: Boolean) = shareWith( + permission("read", listOf("view", "edit"), read), + permission("update", listOf("edit"), update), + permission("delete", listOf("edit"), delete) + ) + + private fun shareWith(vararg permissions: Permission) = Share( + id = "1", + owner = OWNER, + lastUpdated = 0L, + shareState = ShareState.DRAFT, + sources = emptyList(), + recipients = emptyList(), + properties = emptyList(), + permissions = permissions.toList() + ) + + private fun permission(clazz: String, presets: List, enabled: Boolean) = Permission( + clazz = clazz, + displayName = clazz, + priority = 1, + presets = presets, + enabled = enabled + ) + + private companion object { + private val VIEW_PRESET = PermissionPreset(clazz = "view", displayName = "Can view") + private val EDIT_PRESET = PermissionPreset(clazz = "edit", displayName = "Can edit") + private val PRESETS = listOf(VIEW_PRESET, EDIT_PRESET) + private val OWNER = User(userId = "owner", displayName = "Owner", icon = Icon()) + } +} From c0dd3fa6d1eb7d18a281b437d38ded5fa571598a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 10:49:19 +0200 Subject: [PATCH 116/174] wip Signed-off-by: alperozturk96 --- ui/build.gradle | 6 + .../common/ui/share/ShareSavedState.kt | 44 ++++ .../android/common/ui/share/ShareScreen.kt | 48 ++--- .../android/common/ui/share/ShareViewModel.kt | 193 ++++++++++++------ .../common/ui/share/ShareViewModelFactory.kt | 32 +++ .../share/component/SelectRecipientField.kt | 13 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 16 +- .../component/property/SharePropertyView.kt | 4 +- .../share/model/ui/ShareItemOverlayState.kt | 10 +- 9 files changed, 250 insertions(+), 116 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt diff --git a/ui/build.gradle b/ui/build.gradle index 7a9745c0..e180ed50 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -59,6 +59,12 @@ dependencies { implementation("androidx.compose.material:material-icons-core") // endregion + // region lifecycle + api "androidx.lifecycle:lifecycle-viewmodel:$lifecycleVersion" + api "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion" + // endregion + implementation("io.coil-kt.coil3:coil-compose:3.5.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.5.0") implementation("io.coil-kt.coil3:coil-svg:3.5.0") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt new file mode 100644 index 00000000..63753bcf --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share + +import androidx.lifecycle.SavedStateHandle +import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry +import kotlinx.coroutines.flow.StateFlow + +class ShareSavedState( + private val handle: SavedStateHandle +) { + companion object { + private const val KEY_ACTIVE_SHARE_ID = "active_share_id" + private const val KEY_EDITOR_ENTRY = "editor_entry" + private const val KEY_SEARCH_QUERY = "search_query" + } + + var activeShareId: String? + get() = handle[KEY_ACTIVE_SHARE_ID] + set(value) { + handle[KEY_ACTIVE_SHARE_ID] = value + } + + var editorEntry: ShareEditorEntry + get() = + handle + .get(KEY_EDITOR_ENTRY) + ?.let { name -> ShareEditorEntry.entries.firstOrNull { it.name == name } } + ?: ShareEditorEntry.EDIT + set(value) { + handle[KEY_EDITOR_ENTRY] = value.name + } + + val searchQuery: StateFlow = handle.getStateFlow(KEY_SEARCH_QUERY, "") + + fun updateSearchQuery(query: String) { + handle[KEY_SEARCH_QUERY] = query + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index bce5a019..58f09f27 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -46,11 +46,10 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -67,6 +66,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.ImageLoader import coil3.compose.setSingletonImageLoaderFactory import coil3.svg.SvgDecoder @@ -89,7 +91,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository -import kotlinx.coroutines.launch @Composable private fun ShareScreen( @@ -97,14 +98,13 @@ private fun ShareScreen( internalLink: String, viewModel: ShareViewModel ) { - val errorMessageId by viewModel.errorMessageId.collectAsState() - val screenState by viewModel.state.collectAsState() - val activeShare by viewModel.activeShare.collectAsState() - val permissionPresets by viewModel.permissionPresets.collectAsState() - val scope = rememberCoroutineScope() + val errorMessageId by viewModel.errorMessageId.collectAsStateWithLifecycle() + val screenState by viewModel.state.collectAsStateWithLifecycle() + val activeShare by viewModel.activeShare.collectAsStateWithLifecycle() + val permissionPresets by viewModel.permissionPresets.collectAsStateWithLifecycle() + val editorEntry by viewModel.editorEntry.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current - var editorEntry by remember { mutableStateOf(ShareEditorEntry.EDIT) } val context = LocalContext.current LaunchedEffect(errorMessageId) { @@ -117,13 +117,7 @@ private fun ShareScreen( Scaffold( floatingActionButton = { FloatingActionButton( - onClick = { - scope.launch { - viewModel.createDraftShare()?.let { - viewModel.addSource(it.id, sourceId) - } - } - }, + onClick = { viewModel.createDraftShare(sourceId) }, ) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") } @@ -174,20 +168,17 @@ private fun ShareScreen( type = type, permissionPresets = permissionPresets, onSelectShare = { selected -> - editorEntry = ShareEditorEntry.EDIT - viewModel.setActiveShare(selected) + viewModel.setActiveShare(selected, ShareEditorEntry.EDIT) }, onCustomizeShare = { selected -> - editorEntry = ShareEditorEntry.CUSTOMIZE_PERMISSION - viewModel.setActiveShare(selected) + viewModel.setActiveShare(selected, ShareEditorEntry.CUSTOMIZE_PERMISSION) }, onChangePreset = { selected, preset -> viewModel.updatePermissionPreset(selected.id, preset, updateActiveShare = false) }, onDeleteShare = { viewModel.deleteShare(it.id) }, onSendEmail = { selected -> - editorEntry = ShareEditorEntry.SEND_EMAIL - viewModel.setActiveShare(selected) + viewModel.setActiveShare(selected, ShareEditorEntry.SEND_EMAIL) } ) } @@ -223,13 +214,13 @@ private fun ShareItem( onDeleteShare: (Share) -> Unit, onSendEmail: (Share) -> Unit ) { - var overlayState by remember { mutableStateOf(ShareItemOverlayState.None) } + var overlayState by rememberSaveable(share.id) { mutableStateOf(ShareItemOverlayState.None) } val haptics = LocalHapticFeedback.current val presetOptions = PermissionPresetOption.optionsFor(share, permissionPresets) val selectedPresetOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets) when (overlayState) { - is ShareItemOverlayState.QuickShare -> { + ShareItemOverlayState.QuickShare -> { QuickSharePermissionBottomSheet( options = presetOptions, selectedOption = selectedPresetOption, @@ -246,7 +237,7 @@ private fun ShareItem( ) } - is ShareItemOverlayState.DeleteConfirmation -> { + ShareItemOverlayState.DeleteConfirmation -> { DeleteShareConfirmationDialog( onConfirm = { overlayState = ShareItemOverlayState.None @@ -399,13 +390,16 @@ private fun ShareItem( } fun ComposeView.initShareScreen( + viewModelStoreOwner: ViewModelStoreOwner, sourceId: String, internalLink: String, credentials: ServerCredentials, colorScheme: ColorScheme ) { - val nextcloudHttpClient = NextcloudHttpClient.create(credentials) - val viewModel = ShareViewModel(repository = ShareRemoteRepository(nextcloudHttpClient)) + val factory = ShareViewModelFactory { + ShareRemoteRepository(NextcloudHttpClient.create(credentials)) + } + val viewModel = ViewModelProvider.create(viewModelStoreOwner, factory)[ShareViewModel::class] setContent { setSingletonImageLoaderFactory { context -> diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 6fb916bc..d83337e8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -7,6 +7,7 @@ package com.nextcloud.android.common.ui.share +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R @@ -26,17 +27,22 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory +import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -44,23 +50,38 @@ import java.util.UUID import kotlin.time.Duration.Companion.milliseconds class ShareViewModel( - private val repository: ShareRepository + private val repository: ShareRepository, + savedStateHandle: SavedStateHandle ) : ViewModel() { companion object { private const val SEARCH_DEBOUNCE_DELAY = 300L + private const val SEARCH_SUBSCRIPTION_TIMEOUT = 5_000L + private const val RECIPIENT_SEARCH_LIMIT = 10 + private const val RECIPIENT_SEARCH_OFFSET = 0 + private const val SHARES_PAGE_SIZE = 50 } + private val savedState = ShareSavedState(savedStateHandle) + private val _state = MutableStateFlow(ShareScreenState.Loading) val state: StateFlow = _state private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare - private val _searchQuery = MutableStateFlow("") + private val _editorEntry = MutableStateFlow(savedState.editorEntry) + val editorEntry: StateFlow = _editorEntry + + val searchQuery: StateFlow = savedState.searchQuery - private val _recipientSearchResults = MutableStateFlow>(emptyList()) - val recipientSearchResults: StateFlow> = _recipientSearchResults + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) + val recipientSearchResults: StateFlow> = searchQuery + .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) + .distinctUntilChanged() + .filter { it.isNotBlank() } + .mapLatest { query -> searchRecipients(query) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SEARCH_SUBSCRIPTION_TIMEOUT), emptyList()) private val _permissionPresets = MutableStateFlow>(emptyList()) val permissionPresets: StateFlow> = _permissionPresets @@ -82,43 +103,48 @@ class ShareViewModel( get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() init { - fetchShares() - initSearchQuery() - fetchSharingCapabilities() + loadInitialData() } - private fun fetchSharingCapabilities() { + private fun loadInitialData() { viewModelScope.launch(Dispatchers.IO) { - val result = repository.fetchSharingCapabilities() - if (result is NetworkResult.Success) { - _permissionPresets.update { - result.data.permissionPresets - } - } + launch { loadSharingCapabilities() } + launch { restoreActiveShare() } + loadShares() } } - // region search query - @OptIn(FlowPreview::class) - private fun initSearchQuery() { - viewModelScope.launch { - _searchQuery - .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) - .distinctUntilChanged() - .filter { it.isNotBlank() } - .collect { query -> executeSearch(query) } + private suspend fun loadSharingCapabilities() { + val result = repository.fetchSharingCapabilities() + if (result is NetworkResult.Success) { + _permissionPresets.update { + result.data.permissionPresets + } } } + private suspend fun restoreActiveShare() { + if (_activeShare.value != ActiveShareState.None) return + val restored = savedState.activeShareId?.let { fetchRestorableShare(it) } ?: return + + updateActiveShare(ActiveShareState.Editing(restored)) + replaceInList(restored) + } + + private suspend fun fetchRestorableShare(id: String): Share? { + val result = repository.fetchShare(id) + if (result !is NetworkResult.Success) return null + return result.data.takeIf { savedState.activeShareId == id } + } + + // region search query fun onSearchQueryChanged(query: String) { - _searchQuery.value = query + savedState.updateSearchQuery(query) } - private suspend fun executeSearch(query: String) { - val result = repository.fetchRecipients(null, query, 10, 0) - if (result is NetworkResult.Success) { - _recipientSearchResults.value = result.data - } + private suspend fun searchRecipients(query: String): List { + val result = repository.fetchRecipients(null, query, RECIPIENT_SEARCH_LIMIT, RECIPIENT_SEARCH_OFFSET) + return (result as? NetworkResult.Success)?.data ?: emptyList() } // endregion @@ -127,19 +153,28 @@ class ShareViewModel( filterSourceTypeClass: String? = null, filterSourceTypeValue: String? = null, lastShareID: String? = null, - limit: Int = 50 + limit: Int = SHARES_PAGE_SIZE ) { viewModelScope.launch(Dispatchers.IO) { - _state.update { ShareScreenState.Loading } - _errorMessageId.update { null } + loadShares(filterSourceTypeClass, filterSourceTypeValue, lastShareID, limit) + } + } - val result = repository.fetchShares(filterSourceTypeClass, filterSourceTypeValue, lastShareID, limit) - val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } - ?: return@launch - _state.update { - if (fetched.filtered().isEmpty()) ShareScreenState.Empty - else ShareScreenState.Loaded(fetched) - } + private suspend fun loadShares( + filterSourceTypeClass: String? = null, + filterSourceTypeValue: String? = null, + lastShareID: String? = null, + limit: Int = SHARES_PAGE_SIZE + ) { + _state.update { ShareScreenState.Loading } + _errorMessageId.update { null } + + val result = repository.fetchShares(filterSourceTypeClass, filterSourceTypeValue, lastShareID, limit) + val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } + ?: return + _state.update { + if (fetched.filtered().isEmpty()) ShareScreenState.Empty + else ShareScreenState.Loaded(fetched) } } @@ -151,26 +186,27 @@ class ShareViewModel( val share = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } ?: return@launch - _activeShare.update { share.toActiveShare() } + updateActiveShare(share.toActiveShare()) replaceInList(share) } } // endregion // region create - suspend fun createDraftShare(): Share? = withContext(Dispatchers.IO) { - _errorMessageId.update { null } - - val result = repository.createDraftShare() + fun createDraftShare(sourceValue: String) { + viewModelScope.launch(Dispatchers.IO) { + _errorMessageId.update { null } - val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } + val result = repository.createDraftShare() + val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } + ?: return@launch - if (draft != null) { - _activeShare.update { draft.toActiveShare() } + updateEditorEntry(ShareEditorEntry.EDIT) + updateActiveShare(draft.toActiveShare()) _state.update { ShareScreenState.Loaded(listOf(draft) + currentShares) } - } - draft + applySource(draft.id, sourceValue) + } } // endregion @@ -182,12 +218,12 @@ class ShareViewModel( ?: return@launch if (shareState == ShareState.ACTIVE) { if (updateAndDontDismiss) { - _activeShare.update { ActiveShareState.Activating(updated) } + refreshActiveShare(ActiveShareState.Activating(updated)) } else { - _activeShare.update { ActiveShareState.None } + updateActiveShare(ActiveShareState.None) } } else { - _activeShare.update { updated.toActiveShare() } + refreshActiveShare(updated.toActiveShare()) } replaceInList(updated) } @@ -197,22 +233,26 @@ class ShareViewModel( // region sources fun addSource(id: String, value: String) { viewModelScope.launch(Dispatchers.IO) { - // TODO pass from the clients this may vary depends on the client so notes and files for now uses this - val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" - val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch - _activeShare.update { updated.toActiveShare() } - replaceInList(updated) + applySource(id, value) } } + private suspend fun applySource(id: String, value: String) { + // TODO pass from the clients this may vary depends on the client so notes and files for now uses this + val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" + val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) + val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return + refreshActiveShare(updated.toActiveShare()) + replaceInList(updated) + } + fun removeSource(id: String, clazz: String, value: String) { viewModelScope.launch(Dispatchers.IO) { val result = repository.removeShareSource(id, clazz, value) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated.toActiveShare() } + refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } } @@ -224,7 +264,7 @@ class ShareViewModel( val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated.toActiveShare() } + refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } } @@ -260,7 +300,7 @@ class ShareViewModel( val result = repository.removeShareRecipient(id, clazz, value, instance) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated.toActiveShare() } + refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } } @@ -277,7 +317,7 @@ class ShareViewModel( val result = repository.updateShareRecipientSecret(shareId, request) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch - _activeShare.update { updated.toActiveShare() } + refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } } @@ -300,7 +340,7 @@ class ShareViewModel( when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { is NetworkResult.Success -> { _propertyErrors.update { it - clazz } - _activeShare.update { result.data.toActiveShare() } + refreshActiveShare(result.data.toActiveShare()) replaceInList(result.data) } @@ -337,7 +377,7 @@ class ShareViewModel( val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch if (updateActiveShare) { - _activeShare.update { updated.toActiveShare() } + refreshActiveShare(updated.toActiveShare()) } replaceInList(updated) } @@ -358,7 +398,7 @@ class ShareViewModel( val editingShare = _activeShare.value if (editingShare is ActiveShareState.Editing && editingShare.share.id == id) { - _activeShare.update { ActiveShareState.None } + updateActiveShare(ActiveShareState.None) } } } @@ -369,14 +409,31 @@ class ShareViewModel( _errorMessageId.update { value } } - fun setActiveShare(value: Share?) { + fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } - _activeShare.update { value?.toActiveShare() ?: ActiveShareState.None } + updateEditorEntry(entry) + updateActiveShare(value?.toActiveShare() ?: ActiveShareState.None) } // endregion // region private + private fun updateEditorEntry(entry: ShareEditorEntry) { + _editorEntry.value = entry + savedState.editorEntry = entry + } + + private fun updateActiveShare(value: ActiveShareState) { + _activeShare.value = value + savedState.activeShareId = value.shareOrNull?.id + } + + private fun refreshActiveShare(value: ActiveShareState) { + val id = value.shareOrNull?.id ?: return + if (_activeShare.value.shareOrNull?.id != id) return + updateActiveShare(value) + } + private fun replaceInList(updated: Share) { val shares = currentShares.ifEmpty { return } _state.update { ShareScreenState.Loaded(shares.map { if (it.id == updated.id) updated else it }) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt new file mode 100644 index 00000000..aa737406 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.CreationExtras +import com.nextcloud.android.common.ui.share.repository.ShareRepository + +class ShareViewModelFactory( + private val repositoryProvider: () -> ShareRepository +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + extras: CreationExtras + ): T { + require(modelClass.isAssignableFrom(ShareViewModel::class.java)) { + "ShareViewModelFactory cannot create ${modelClass.name}" + } + + val savedStateHandle: SavedStateHandle = extras.createSavedStateHandle() + return ShareViewModel(repositoryProvider(), savedStateHandle) as T + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 8479e13f..7031cdbd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -24,16 +24,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.svg.SvgDecoder @@ -96,9 +96,9 @@ private fun SelectedRecipient(recipient: Recipient, onClear: () -> Unit) { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun RecipientSearch(share: Share, viewModel: ShareViewModel) { - var query by remember { mutableStateOf("") } - var expanded by remember { mutableStateOf(false) } - val results by viewModel.recipientSearchResults.collectAsState() + val query by viewModel.searchQuery.collectAsStateWithLifecycle() + var expanded by rememberSaveable { mutableStateOf(false) } + val results by viewModel.recipientSearchResults.collectAsStateWithLifecycle() ExposedDropdownMenuBox( modifier = Modifier @@ -110,7 +110,6 @@ private fun RecipientSearch(share: Share, viewModel: ShareViewModel) { OutlinedTextField( value = query, onValueChange = { - query = it expanded = true viewModel.onSearchQueryChanged(it) }, @@ -130,7 +129,7 @@ private fun RecipientSearch(share: Share, viewModel: ShareViewModel) { results = results, onSelect = { recipient -> viewModel.addRecipient(share.id, recipient.clazz, recipient.value) - query = "" + viewModel.onSearchQueryChanged("") expanded = false } ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index bea931a0..5b9c8863 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -35,12 +35,12 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard @@ -49,6 +49,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection @@ -84,17 +86,17 @@ fun AddOrEditShareBottomSheet( ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val categories = remember { ShareCategory.entries.toList() } - var selectedCategory by remember(share.id) { + var selectedCategory by rememberSaveable(share.id) { mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) } val initialPresetOption = when (entry) { ShareEditorEntry.CUSTOMIZE_PERMISSION -> PermissionPresetOption.Custom else -> null } - var showAdvancedSettings by remember(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } + var showAdvancedSettings by rememberSaveable(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } val context = LocalContext.current - val propertyErrors by viewModel.propertyErrors.collectAsState() - val pendingProperties by viewModel.pendingProperties.collectAsState() + val propertyErrors by viewModel.propertyErrors.collectAsStateWithLifecycle() + val pendingProperties by viewModel.pendingProperties.collectAsStateWithLifecycle() val hasPropertyErrors = propertyErrors.isNotEmpty() val sendEnabled = !hasPropertyErrors && pendingProperties.isEmpty() @@ -218,7 +220,7 @@ private fun PermissionsView( permissionPresets: List, viewModel: ShareViewModel ) { - var selectedPreset by remember(share.id) { + var selectedPreset by rememberSaveable(share.id) { val default = share.getDefaultPermissionPresetOption(permissionPresets).presetClass mutableStateOf(initialPresetOption?.presetClass ?: share.permissionPreset ?: default) } @@ -458,7 +460,7 @@ private fun AddOrEditShareBottomSheetPreview() { AddOrEditShareBottomSheet( share = previewShare, internalLink = "internal_link", - viewModel = ShareViewModel(MockShareRepository()) + viewModel = ShareViewModel(MockShareRepository(), SavedStateHandle()) ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index c181e5d4..5d7347c9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -25,6 +24,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.ShareViewModel import com.nextcloud.android.common.ui.share.component.ShareSwitch @@ -38,7 +38,7 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyString @Composable fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { - val propertyErrors by viewModel.propertyErrors.collectAsState() + val propertyErrors by viewModel.propertyErrors.collectAsStateWithLifecycle() val fallbackError = stringResource(R.string.share_view_update_error_message) val errorMessage = if (propertyErrors.containsKey(property.clazz)) { propertyErrors[property.clazz] ?: fallbackError diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt index 0b47eff0..b5c73b97 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt @@ -7,9 +7,9 @@ package com.nextcloud.android.common.ui.share.model.ui -sealed class ShareItemOverlayState { - data object None : ShareItemOverlayState() - data object ContextMenu : ShareItemOverlayState() - data object QuickShare : ShareItemOverlayState() - data object DeleteConfirmation : ShareItemOverlayState() +enum class ShareItemOverlayState { + None, + ContextMenu, + QuickShare, + DeleteConfirmation } From 5275ffd887d4807ad16919be771a9745023ffcb3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 11:20:16 +0200 Subject: [PATCH 117/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 132 +++++++++++------- .../android/common/ui/share/ShareViewModel.kt | 61 ++------ .../common/ui/share/ShareViewModelFactory.kt | 3 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 2 +- .../share/repository/MockShareRepository.kt | 17 +-- .../share/repository/ShareRemoteRepository.kt | 8 +- .../ui/share/repository/ShareRepository.kt | 6 +- 7 files changed, 112 insertions(+), 117 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 58f09f27..2671a34f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -33,6 +34,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -44,6 +46,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -92,9 +95,11 @@ import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository +private val FIRST_ITEM_TOP_SPACING = 16.dp +private val ITEM_SPACING = 2.dp + @Composable private fun ShareScreen( - sourceId: String, internalLink: String, viewModel: ShareViewModel ) { @@ -105,7 +110,6 @@ private fun ShareScreen( val editorEntry by viewModel.editorEntry.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current - val context = LocalContext.current LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -117,7 +121,7 @@ private fun ShareScreen( Scaffold( floatingActionButton = { FloatingActionButton( - onClick = { viewModel.createDraftShare(sourceId) }, + onClick = { viewModel.createDraftShare() }, ) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") } @@ -137,53 +141,19 @@ private fun ShareScreen( } } - is ShareScreenState.Empty -> { - ContentUnavailableView( - iconId = R.drawable.ic_person_add, - title = stringResource(R.string.share_view_empty_title), - ) - } - - is ShareScreenState.Loaded -> { - LazyColumn( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val shares = state.shares.filtered() - itemsIndexed(shares, key = { _, share -> share.id }) { index, share -> - val type = ShareItemType.type(index, shares.lastIndex) - val title = share.getHeadline(context, shares) - - if (index == 0) { - Spacer(modifier = Modifier.height(16.dp)) - } else { - Spacer(modifier = Modifier.height(2.dp)) - } + is ShareScreenState.Empty -> ShareList( + shares = emptyList(), + permissionPresets = permissionPresets, + paddingValues = paddingValues, + viewModel = viewModel + ) - ShareItem( - share = share, - title = title, - type = type, - permissionPresets = permissionPresets, - onSelectShare = { selected -> - viewModel.setActiveShare(selected, ShareEditorEntry.EDIT) - }, - onCustomizeShare = { selected -> - viewModel.setActiveShare(selected, ShareEditorEntry.CUSTOMIZE_PERMISSION) - }, - onChangePreset = { selected, preset -> - viewModel.updatePermissionPreset(selected.id, preset, updateActiveShare = false) - }, - onDeleteShare = { viewModel.deleteShare(it.id) }, - onSendEmail = { selected -> - viewModel.setActiveShare(selected, ShareEditorEntry.SEND_EMAIL) - } - ) - } - } - } + is ShareScreenState.Loaded -> ShareList( + shares = state.shares.filtered(), + permissionPresets = permissionPresets, + paddingValues = paddingValues, + viewModel = viewModel + ) } } @@ -202,6 +172,66 @@ private fun ShareScreen( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShareList( + shares: List, + permissionPresets: List, + paddingValues: PaddingValues, + viewModel: ShareViewModel +) { + val context = LocalContext.current + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = viewModel::refreshShares, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (shares.isEmpty()) { + item { + Box(modifier = Modifier.fillParentMaxSize()) { + ContentUnavailableView( + iconId = R.drawable.ic_person_add, + title = stringResource(R.string.share_view_empty_title), + ) + } + } + } + + itemsIndexed(shares, key = { _, share -> share.id }) { index, share -> + Spacer(modifier = Modifier.height(if (index == 0) FIRST_ITEM_TOP_SPACING else ITEM_SPACING)) + + ShareItem( + share = share, + title = share.getHeadline(context, shares), + type = ShareItemType.type(index, shares.lastIndex), + permissionPresets = permissionPresets, + onSelectShare = { selected -> + viewModel.setActiveShare(selected, ShareEditorEntry.EDIT) + }, + onCustomizeShare = { selected -> + viewModel.setActiveShare(selected, ShareEditorEntry.CUSTOMIZE_PERMISSION) + }, + onChangePreset = { selected, preset -> + viewModel.updatePermissionPreset(selected.id, preset, updateActiveShare = false) + }, + onDeleteShare = { viewModel.deleteShare(it.id) }, + onSendEmail = { selected -> + viewModel.setActiveShare(selected, ShareEditorEntry.SEND_EMAIL) + } + ) + } + } + } +} + @Composable private fun ShareItem( share: Share, @@ -396,7 +426,7 @@ fun ComposeView.initShareScreen( credentials: ServerCredentials, colorScheme: ColorScheme ) { - val factory = ShareViewModelFactory { + val factory = ShareViewModelFactory(sourceId) { ShareRemoteRepository(NextcloudHttpClient.create(credentials)) } val viewModel = ViewModelProvider.create(viewModelStoreOwner, factory)[ShareViewModel::class] @@ -411,7 +441,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(sourceId, internalLink, viewModel) + ShareScreen(internalLink, viewModel) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index d83337e8..427bc1e5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -17,7 +17,6 @@ import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPres import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest -import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest @@ -51,6 +50,7 @@ import kotlin.time.Duration.Companion.milliseconds class ShareViewModel( private val repository: ShareRepository, + private val sourceId: String, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -67,6 +67,9 @@ class ShareViewModel( private val _state = MutableStateFlow(ShareScreenState.Loading) val state: StateFlow = _state + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow = _isRefreshing + private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare @@ -149,27 +152,20 @@ class ShareViewModel( // endregion // region shares list - fun fetchShares( - filterSourceTypeClass: String? = null, - filterSourceTypeValue: String? = null, - lastShareID: String? = null, - limit: Int = SHARES_PAGE_SIZE - ) { + fun refreshShares() { + if (_isRefreshing.value) return + viewModelScope.launch(Dispatchers.IO) { - loadShares(filterSourceTypeClass, filterSourceTypeValue, lastShareID, limit) + _isRefreshing.update { true } + loadShares() + _isRefreshing.update { false } } } - private suspend fun loadShares( - filterSourceTypeClass: String? = null, - filterSourceTypeValue: String? = null, - lastShareID: String? = null, - limit: Int = SHARES_PAGE_SIZE - ) { - _state.update { ShareScreenState.Loading } + private suspend fun loadShares() { _errorMessageId.update { null } - val result = repository.fetchShares(filterSourceTypeClass, filterSourceTypeValue, lastShareID, limit) + val result = repository.fetchShares(filterSourceTypeValue = sourceId, limit = SHARES_PAGE_SIZE) val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } ?: return _state.update { @@ -177,23 +173,10 @@ class ShareViewModel( else ShareScreenState.Loaded(fetched) } } - - fun fetchShare(id: String, request: GetShareRequest = GetShareRequest()) { - viewModelScope.launch(Dispatchers.IO) { - _errorMessageId.update { null } - - val result = repository.fetchShare(id, request) - val share = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } - ?: return@launch - - updateActiveShare(share.toActiveShare()) - replaceInList(share) - } - } // endregion // region create - fun createDraftShare(sourceValue: String) { + fun createDraftShare() { viewModelScope.launch(Dispatchers.IO) { _errorMessageId.update { null } @@ -205,7 +188,7 @@ class ShareViewModel( updateActiveShare(draft.toActiveShare()) _state.update { ShareScreenState.Loaded(listOf(draft) + currentShares) } - applySource(draft.id, sourceValue) + applySource(draft.id, sourceId) } } // endregion @@ -231,12 +214,6 @@ class ShareViewModel( // endregion // region sources - fun addSource(id: String, value: String) { - viewModelScope.launch(Dispatchers.IO) { - applySource(id, value) - } - } - private suspend fun applySource(id: String, value: String) { // TODO pass from the clients this may vary depends on the client so notes and files for now uses this val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" @@ -246,16 +223,6 @@ class ShareViewModel( refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } - - fun removeSource(id: String, clazz: String, value: String) { - viewModelScope.launch(Dispatchers.IO) { - val result = repository.removeShareSource(id, clazz, value) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch - refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - } - } // endregion // region recipients diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt index aa737406..b6fa9307 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.viewmodel.CreationExtras import com.nextcloud.android.common.ui.share.repository.ShareRepository class ShareViewModelFactory( + private val sourceId: String, private val repositoryProvider: () -> ShareRepository ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -27,6 +28,6 @@ class ShareViewModelFactory( } val savedStateHandle: SavedStateHandle = extras.createSavedStateHandle() - return ShareViewModel(repositoryProvider(), savedStateHandle) as T + return ShareViewModel(repositoryProvider(), sourceId, savedStateHandle) as T } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 5b9c8863..77e123a7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -460,7 +460,7 @@ private fun AddOrEditShareBottomSheetPreview() { AddOrEditShareBottomSheet( share = previewShare, internalLink = "internal_link", - viewModel = ShareViewModel(MockShareRepository(), SavedStateHandle()) + viewModel = ShareViewModel(MockShareRepository(), "preview-source", SavedStateHandle()) ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 25876385..1a2bb62d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -324,18 +324,15 @@ class MockShareRepository : ShareRepository { } override suspend fun fetchShares( + filterSourceTypeValue: String, + limit: Int, filterSourceTypeClass: String?, - filterSourceTypeValue: String?, - lastShareID: String?, - limit: Int + lastShareID: String? ): NetworkResult> { - var result = mockShares.toList() - - if (filterSourceTypeClass != null) { - result = result.filter { share -> - share.sources.any { - it.clazz == filterSourceTypeClass && (filterSourceTypeValue == null || it.value == filterSourceTypeValue) - } + var result = mockShares.filter { share -> + share.sources.any { + it.value == filterSourceTypeValue && + (filterSourceTypeClass == null || it.clazz == filterSourceTypeClass) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index a454e806..0e20df77 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -92,15 +92,15 @@ class ShareRemoteRepository( ) { } override suspend fun fetchShares( + filterSourceTypeValue: String, + limit: Int, filterSourceTypeClass: String?, - filterSourceTypeValue: String?, - lastShareID: String?, - limit: Int + lastShareID: String? ): NetworkResult> { val queryParams = buildString { append("?limit=$limit") + append("&filterSourceTypeValue=${filterSourceTypeValue.urlEncoded()}") filterSourceTypeClass?.let { append("&filterSourceTypeClass=${it.urlEncoded()}") } - filterSourceTypeValue?.let { append("&filterSourceTypeValue=${it.urlEncoded()}") } lastShareID?.let { append("&lastShareID=${it.urlEncoded()}") } } return client.executeRequest( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 118311e7..6f5cae0d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -38,10 +38,10 @@ interface ShareRepository { suspend fun deleteShare(id: String): NetworkResult suspend fun fetchShares( + filterSourceTypeValue: String, + limit: Int, filterSourceTypeClass: String? = null, - filterSourceTypeValue: String? = null, - lastShareID: String? = null, - limit: Int + lastShareID: String? = null ): NetworkResult> suspend fun updateShareState( From 721105133726c4abe4a9626928f7cea04e4190eb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 11:51:10 +0200 Subject: [PATCH 118/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 57 ++++++++--- .../share/component/SelectRecipientField.kt | 98 +++++++++++-------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 55 ++++++----- .../property/SharePropertyEnumField.kt | 5 +- .../property/SharePropertyMessage.kt | 35 +++++++ .../component/property/SharePropertyView.kt | 78 ++++++++------- .../property/datepicker/ShareDatePicker.kt | 7 +- .../share/model/api/recipients/Recipient.kt | 3 + .../common/ui/share/model/api/share/Share.kt | 39 +++++--- .../ui/share/model/api/source/Source.kt | 6 +- 10 files changed, 248 insertions(+), 135 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 427bc1e5..7dc1d645 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -23,6 +23,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareProper import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory @@ -70,6 +71,12 @@ class ShareViewModel( private val _isRefreshing = MutableStateFlow(false) val isRefreshing: StateFlow = _isRefreshing + private val _isPreparingLink = MutableStateFlow(false) + val isPreparingLink: StateFlow = _isPreparingLink + + private val _shareReadyToCopy = MutableStateFlow(null) + val shareReadyToCopy: StateFlow = _shareReadyToCopy + private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare @@ -165,7 +172,11 @@ class ShareViewModel( private suspend fun loadShares() { _errorMessageId.update { null } - val result = repository.fetchShares(filterSourceTypeValue = sourceId, limit = SHARES_PAGE_SIZE) + val result = repository.fetchShares( + filterSourceTypeValue = sourceId, + limit = SHARES_PAGE_SIZE, + filterSourceTypeClass = Source.NODE_SOURCE_CLASS + ) val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } ?: return _state.update { @@ -194,30 +205,49 @@ class ShareViewModel( // endregion // region state - fun updateState(id: String, shareState: ShareState, updateAndDontDismiss: Boolean = false) { + fun updateState(id: String, shareState: ShareState) { viewModelScope.launch(Dispatchers.IO) { - val result = repository.updateShareState(id, UpdateShareStateRequest(shareState)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch + val updated = applyState(id, shareState) ?: return@launch + if (shareState == ShareState.ACTIVE) { - if (updateAndDontDismiss) { - refreshActiveShare(ActiveShareState.Activating(updated)) - } else { - updateActiveShare(ActiveShareState.None) - } + updateActiveShare(ActiveShareState.None) } else { refreshActiveShare(updated.toActiveShare()) } replaceInList(updated) } } + + fun prepareLinkForCopy(id: String) { + if (_isPreparingLink.value) return + + viewModelScope.launch(Dispatchers.IO) { + _isPreparingLink.update { true } + + val updated = applyState(id, ShareState.ACTIVE) + if (updated != null) { + refreshActiveShare(ActiveShareState.Activating(updated)) + replaceInList(updated) + _shareReadyToCopy.update { updated } + } + + _isPreparingLink.update { false } + } + } + + fun onLinkCopied() { + _shareReadyToCopy.update { null } + } + + private suspend fun applyState(id: String, shareState: ShareState): Share? { + val result = repository.updateShareState(id, UpdateShareStateRequest(shareState)) + return result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + } // endregion // region sources private suspend fun applySource(id: String, value: String) { - // TODO pass from the clients this may vary depends on the client so notes and files for now uses this - val clazz = "OCA\\Files\\Sharing\\Source\\NodeShareSourceType" - val result = repository.addShareSource(id, AddSourceRequest(clazz, value)) + val result = repository.addShareSource(id, AddSourceRequest(Source.NODE_SOURCE_CLASS, value)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return refreshActiveShare(updated.toActiveShare()) @@ -379,6 +409,7 @@ class ShareViewModel( fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } + _shareReadyToCopy.update { null } updateEditorEntry(entry) updateActiveShare(value?.toActiveShare() ?: ActiveShareState.None) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 7031cdbd..4d1a0f3b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -8,7 +8,11 @@ package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -19,7 +23,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -43,67 +47,79 @@ import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share +private val FIELD_PADDING = 12.dp +private val CHIP_SPACING = 8.dp +private val CHIP_AVATAR_SIZE = 20.dp +private val CHIP_CLOSE_ICON_SIZE = 18.dp +private val RESULT_ICON_SIZE = 20.dp + @Composable fun SelectRecipientField( share: Share, viewModel: ShareViewModel ) { - val selectedRecipient = share.recipients.firstOrNull { it.clazz != Recipient.TOKEN_RECIPIENT_CLASS } + val selected = share.invitedRecipients - if (selectedRecipient == null) { - RecipientSearch(share, viewModel) - return - } + Column(modifier = Modifier.fillMaxWidth()) { + RecipientChips( + recipients = selected, + onRemove = { recipient -> + viewModel.removeRecipient( + id = share.id, + clazz = recipient.clazz, + value = recipient.value, + instance = recipient.instance + ) + } + ) - SelectedRecipient( - recipient = selectedRecipient, - onClear = { - viewModel.removeRecipient( - id = share.id, - clazz = selectedRecipient.clazz, - value = selectedRecipient.value, - instance = selectedRecipient.instance - ) - } - ) + RecipientSearch(share = share, selected = selected, viewModel = viewModel) + } } +@OptIn(ExperimentalLayoutApi::class) @Composable -private fun SelectedRecipient(recipient: Recipient, onClear: () -> Unit) { - OutlinedTextField( - value = recipient.displayName, - onValueChange = {}, - readOnly = true, - singleLine = true, +private fun RecipientChips(recipients: List, onRemove: (Recipient) -> Unit) { + if (recipients.isEmpty()) return + + FlowRow( modifier = Modifier .fillMaxWidth() - .padding(12.dp), - label = { Text(stringResource(R.string.share_view_select_recipient_label)) }, - leadingIcon = { - recipient.icon?.let { RecipientIcon(icon = it, modifier = Modifier.size(24.dp)) } - }, - trailingIcon = { - IconButton(onClick = onClear) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.share_view_select_recipient_clear) - ) - } + .padding(horizontal = FIELD_PADDING), + horizontalArrangement = Arrangement.spacedBy(CHIP_SPACING) + ) { + recipients.forEach { recipient -> + InputChip( + selected = false, + onClick = { onRemove(recipient) }, + label = { Text(recipient.displayName) }, + avatar = { + recipient.icon?.let { RecipientIcon(icon = it, modifier = Modifier.size(CHIP_AVATAR_SIZE)) } + }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.share_view_select_recipient_clear), + modifier = Modifier.size(CHIP_CLOSE_ICON_SIZE) + ) + } + ) } - ) + } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun RecipientSearch(share: Share, viewModel: ShareViewModel) { +private fun RecipientSearch(share: Share, selected: List, viewModel: ShareViewModel) { val query by viewModel.searchQuery.collectAsStateWithLifecycle() var expanded by rememberSaveable { mutableStateOf(false) } val results by viewModel.recipientSearchResults.collectAsStateWithLifecycle() + val suggestions = results.filterNot { result -> selected.any { it.isSameAs(result) } } ExposedDropdownMenuBox( modifier = Modifier .fillMaxWidth() - .padding(12.dp), + .padding(FIELD_PADDING), expanded = expanded && query.isNotBlank(), onExpandedChange = { expanded = it } ) { @@ -126,9 +142,9 @@ private fun RecipientSearch(share: Share, viewModel: ShareViewModel) { onDismissRequest = { expanded = false } ) { RecipientDropdownContent( - results = results, + results = suggestions, onSelect = { recipient -> - viewModel.addRecipient(share.id, recipient.clazz, recipient.value) + viewModel.addRecipient(share.id, recipient.clazz, recipient.value, recipient.instance) viewModel.onSearchQueryChanged("") expanded = false } @@ -162,7 +178,7 @@ private fun RecipientDropdownContent( recipient.icon?.let { RecipientIcon( icon = it, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(RESULT_ICON_SIZE) ) } }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 77e123a7..4e7019c3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuAnchorType @@ -35,6 +36,7 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf @@ -361,35 +363,34 @@ private fun ActionButtons( sendEnabled: Boolean, viewModel: ShareViewModel ) { + val localClipboard = LocalClipboard.current + val scope = rememberCoroutineScope() + val isPreparingLink by viewModel.isPreparingLink.collectAsStateWithLifecycle() + val shareReadyToCopy by viewModel.shareReadyToCopy.collectAsStateWithLifecycle() + + LaunchedEffect(shareReadyToCopy) { + val ready = shareReadyToCopy ?: return@LaunchedEffect + ready.getClipEntry(internalLink, category)?.let { localClipboard.setClipEntry(it) } + viewModel.onLinkCopied() + } + Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - val localClipboard = LocalClipboard.current - val scope = rememberCoroutineScope() - Button( onClick = { - scope.launch { - share.getClipEntry(internalLink, category)?.let { - if (category == ShareCategory.Anyone) { - - // recompose ActionButtons - viewModel.updateState(share.id, ShareState.ACTIVE, updateAndDontDismiss = true) - - // share object is updated active share - if (share.shareState == ShareState.ACTIVE) { - localClipboard.setClipEntry(it) - } - } else { - localClipboard.setClipEntry(it) - } + if (category == ShareCategory.Anyone) { + viewModel.prepareLinkForCopy(share.id) + } else { + scope.launch { + share.getClipEntry(internalLink, category)?.let { localClipboard.setClipEntry(it) } } } }, - enabled = category != ShareCategory.Anyone || sendEnabled, + enabled = !isPreparingLink && (category != ShareCategory.Anyone || sendEnabled), modifier = Modifier.weight(1f), shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( @@ -397,11 +398,19 @@ private fun ActionButtons( contentColor = MaterialTheme.colorScheme.onSurface, ), ) { - Icon( - painter = painterResource(R.drawable.ic_link), - contentDescription = null, - modifier = Modifier.size(20.dp), - ) + if (isPreparingLink) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onSurface + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_link), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } Text( text = stringResource(category.copyLinkTitleId), modifier = Modifier.padding(start = 8.dp), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt index 301bd373..22fb837d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt @@ -29,7 +29,7 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum @Composable fun SharePropertyEnumField( property: PropertyEnum, - errorMessage: String?, + isError: Boolean, onValueSelected: (String) -> Unit ) { var expanded by remember { mutableStateOf(false) } @@ -49,8 +49,7 @@ fun SharePropertyEnumField( singleLine = true, label = { Text(property.displayName) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - isError = errorMessage != null, - supportingText = errorMessage?.let { { Text(it) } }, + isError = isError, modifier = Modifier .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true) .fillMaxWidth() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt new file mode 100644 index 00000000..77363da8 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component.property + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +private val MESSAGE_START_PADDING = 16.dp +private val MESSAGE_BOTTOM_PADDING = 4.dp + +@Composable +fun SharePropertyMessage(errorMessage: String?, hint: String?) { + val message = errorMessage ?: hint?.takeIf { it.isNotBlank() } ?: return + val color = if (errorMessage != null) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + + Text( + text = message, + color = color, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = MESSAGE_START_PADDING, bottom = MESSAGE_BOTTOM_PADDING) + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 5d7347c9..fcf0fcd9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -10,7 +10,6 @@ package com.nextcloud.android.common.ui.share.component.property import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -46,28 +45,37 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM null } + Column { + SharePropertyField( + shareId = shareId, + property = property, + isError = errorMessage != null, + viewModel = viewModel + ) + + SharePropertyMessage(errorMessage = errorMessage, hint = property.hint) + } +} + +@Composable +private fun SharePropertyField( + shareId: String, + property: Property, + isError: Boolean, + viewModel: ShareViewModel +) { when (property) { is PropertyBoolean -> { var checkedValue by remember(property.clazz) { mutableStateOf(property.isTrue()) } - Column { - ShareSwitch( - label = property.displayName, - checked = checkedValue, - onCheckedChange = { isChecked -> - checkedValue = isChecked - viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) - } - ) - if (errorMessage != null) { - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 4.dp, bottom = 4.dp) - ) + ShareSwitch( + label = property.displayName, + checked = checkedValue, + onCheckedChange = { isChecked -> + checkedValue = isChecked + viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) } - } + ) } is PropertyString -> ShareTextPropertyField( @@ -75,7 +83,7 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM clazz = property.clazz, displayName = property.displayName, initialValue = property.value ?: "", - errorMessage = errorMessage, + isError = isError, viewModel = viewModel ) @@ -84,25 +92,22 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM clazz = property.clazz, displayName = property.displayName, initialValue = property.value ?: "", - errorMessage = errorMessage, + isError = isError, viewModel = viewModel, - hint = property.hint, visualTransformation = PasswordVisualTransformation() ) - is PropertyDate -> { - ShareDatePicker(property, errorMessage = errorMessage, onDateSelected = { dateValue -> - viewModel.updateProperty(shareId, property.clazz, dateValue) - }) - } + is PropertyDate -> ShareDatePicker( + property = property, + isError = isError, + onDateSelected = { dateValue -> viewModel.updateProperty(shareId, property.clazz, dateValue) } + ) - is PropertyEnum -> { - SharePropertyEnumField( - property = property, - errorMessage = errorMessage, - onValueSelected = { value -> viewModel.updateProperty(shareId, property.clazz, value) } - ) - } + is PropertyEnum -> SharePropertyEnumField( + property = property, + isError = isError, + onValueSelected = { value -> viewModel.updateProperty(shareId, property.clazz, value) } + ) } } @@ -112,9 +117,8 @@ private fun ShareTextPropertyField( clazz: String, displayName: String, initialValue: String, - errorMessage: String?, + isError: Boolean, viewModel: ShareViewModel, - hint: String? = null, visualTransformation: VisualTransformation = VisualTransformation.None ) { var value by remember(clazz) { mutableStateOf(initialValue) } @@ -125,10 +129,8 @@ private fun ShareTextPropertyField( value = value, onValueChange = { value = it }, label = { Text(displayName) }, - placeholder = hint?.let { { Text(it) } }, visualTransformation = visualTransformation, - isError = errorMessage != null, - supportingText = errorMessage?.let { { Text(it) } }, + isError = isError, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 153e6f51..5bf623b3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -33,7 +33,7 @@ import com.nextcloud.android.common.ui.share.component.property.datepicker.util. import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate @Composable -fun ShareDatePicker(property: PropertyDate, errorMessage: String?, onDateSelected: (String) -> Unit) { +fun ShareDatePicker(property: PropertyDate, isError: Boolean, onDateSelected: (String) -> Unit) { val formatter = remember { ShareDateFormatter() } var showDatePicker by remember { mutableStateOf(false) } var dateValue by remember(property.clazz) { mutableStateOf(formatter.formatIsoForDisplay(property.value)) } @@ -44,8 +44,7 @@ fun ShareDatePicker(property: PropertyDate, errorMessage: String?, onDateSelecte onValueChange = {}, readOnly = true, enabled = true, - isError = errorMessage != null, - supportingText = errorMessage?.let { { Text(it) } }, + isError = isError, modifier = Modifier.fillMaxWidth(), label = { Text(formatter.getDisplayName(property)) }, trailingIcon = { @@ -123,7 +122,7 @@ private fun ShareDatePickerPreview() { minDate = "2026-01-01", maxDate = "2027-12-31" ), - errorMessage = null, + isError = false, onDateSelected = {} ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index d5f967d5..601dabcc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -34,4 +34,7 @@ data class Recipient( companion object { const val TOKEN_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\TokenShareRecipientType""" } + + fun isSameAs(other: Recipient): Boolean = + clazz == other.clazz && value == other.value && instance == other.instance } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 0f3a303d..d52a3e34 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -48,6 +48,10 @@ data class Share( @SerialName("permission_preset") val permissionPreset: String? = null ) { + companion object { + private const val LABEL_PROPERTY_CLASS = "label" + } + val basicProperties: List by lazy { properties.filterNot { it.advanced }.sortedBy { it.priority } } @@ -71,14 +75,12 @@ data class Share( } fun getClipEntry(internalLink: String, category: ShareCategory): ClipEntry? { - val recipient = recipients.firstOrNull() ?: return null - - return if (category == ShareCategory.Anyone) { - val link = recipient.secret.url ?: return null - ClipData.newPlainText(recipient.displayName, link).toClipEntry() - } else { - ClipData.newPlainText(recipient.displayName, internalLink).toClipEntry() + if (category == ShareCategory.Anyone) { + val recipient = customLinkRecipient ?: return null + return recipient.secret.url?.let { ClipData.newPlainText(recipient.displayName, it).toClipEntry() } } + + return recipients.firstOrNull()?.let { ClipData.newPlainText(it.displayName, internalLink).toClipEntry() } } fun getHeadline(context: Context, shares: List): String { @@ -86,15 +88,25 @@ data class Share( return recipients.firstOrNull()?.displayName ?: "" } + return label ?: publicLinkHeadline(context, shares) + } + + private fun publicLinkHeadline(context: Context, shares: List): String { val publicLinks = shares.filter { it.belongsAnyoneTab } - return if (publicLinks.size <= 1) { - context.getString(R.string.share_view_public_link) - } else { - val position = publicLinks.sortedBy { it.lastUpdated }.indexOfFirst { it.id == id } + 1 - context.getString(R.string.share_view_public_link_numbered, position) + if (publicLinks.size <= 1) { + return context.getString(R.string.share_view_public_link) } + + val position = publicLinks.sortedBy { it.lastUpdated }.indexOfFirst { it.id == id } + 1 + return context.getString(R.string.share_view_public_link_numbered, position) } + val label: String? + get() = properties + .firstOrNull { it.clazz.contains(LABEL_PROPERTY_CLASS, ignoreCase = true) } + ?.value + ?.takeIf { it.isNotBlank() } + val belongsAnyoneTab: Boolean get() { return customLinkRecipient != null @@ -104,6 +116,9 @@ data class Share( recipients.firstOrNull { it.secret.updatable } } + val invitedRecipients: List + get() = recipients.filterNot { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } + // The preset matching the enabled permissions dictated by the server admin: // only the read permission -> "Can view", every permission -> "Can edit", any other subset -> "Can ...". fun getDefaultPermissionPresetOption(permissionPresets: List): PermissionPresetOption { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt index 0635a4bd..f4c66311 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt @@ -22,4 +22,8 @@ data class Source( val displayName: String, val icon: Icon? = null -) +) { + companion object { + const val NODE_SOURCE_CLASS = """OCA\Files\Sharing\Source\NodeShareSourceType""" + } +} From bbcaaf3cf6e189ee69b0b47fee7d6a301d47d78d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 11:58:31 +0200 Subject: [PATCH 119/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 7dc1d645..1a56e9eb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -60,7 +60,8 @@ class ShareViewModel( private const val SEARCH_SUBSCRIPTION_TIMEOUT = 5_000L private const val RECIPIENT_SEARCH_LIMIT = 10 private const val RECIPIENT_SEARCH_OFFSET = 0 - private const val SHARES_PAGE_SIZE = 50 + private const val SHARES_PAGE_SIZE = 100 + private const val MAX_SHARE_PAGES = 20 } private val savedState = ShareSavedState(savedStateHandle) @@ -172,18 +173,39 @@ class ShareViewModel( private suspend fun loadShares() { _errorMessageId.update { null } - val result = repository.fetchShares( - filterSourceTypeValue = sourceId, - limit = SHARES_PAGE_SIZE, - filterSourceTypeClass = Source.NODE_SOURCE_CLASS - ) - val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } - ?: return + val fetched = fetchAllShares() ?: return _state.update { if (fetched.filtered().isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(fetched) } } + + // The server pages drafts and active shares together while the list only renders active ones, + // so a file with a page worth of abandoned drafts would otherwise hide every real share. + private suspend fun fetchAllShares(): List? { + val shares = mutableListOf() + var cursor: String? = null + var page = 0 + + while (page < MAX_SHARE_PAGES) { + val result = repository.fetchShares( + filterSourceTypeValue = sourceId, + limit = SHARES_PAGE_SIZE, + filterSourceTypeClass = Source.NODE_SOURCE_CLASS, + lastShareID = cursor + ) + val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } + ?: return null + + shares += fetched + if (fetched.size < SHARES_PAGE_SIZE) break + + cursor = fetched.last().id + page++ + } + + return shares + } // endregion // region create From 22d4ce69c43a48e39793f4619cea256c20473dcd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 15:53:56 +0200 Subject: [PATCH 120/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 3 +- .../android/common/ui/share/ShareViewModel.kt | 36 ++++++++++++------- .../ui/share/model/ui/ShareScreenState.kt | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 2671a34f..78d256ec 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -91,7 +91,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState -import com.nextcloud.android.common.ui.share.model.ui.filtered import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository @@ -149,7 +148,7 @@ private fun ShareScreen( ) is ShareScreenState.Loaded -> ShareList( - shares = state.shares.filtered(), + shares = state.shares, permissionPresets = permissionPresets, paddingValues = paddingValues, viewModel = viewModel diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 1a56e9eb..8ecaaf03 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -29,7 +29,7 @@ import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState -import com.nextcloud.android.common.ui.share.model.ui.filtered +import com.nextcloud.android.common.ui.share.model.ui.activeOnly import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -174,10 +174,7 @@ class ShareViewModel( _errorMessageId.update { null } val fetched = fetchAllShares() ?: return - _state.update { - if (fetched.filtered().isEmpty()) ShareScreenState.Empty - else ShareScreenState.Loaded(fetched) - } + publishShares(fetched.activeOnly()) } // The server pages drafts and active shares together while the list only renders active ones, @@ -219,7 +216,6 @@ class ShareViewModel( updateEditorEntry(ShareEditorEntry.EDIT) updateActiveShare(draft.toActiveShare()) - _state.update { ShareScreenState.Loaded(listOf(draft) + currentShares) } applySource(draft.id, sourceId) } @@ -409,11 +405,7 @@ class ShareViewModel( val result = repository.deleteShare(id) result.dataOrElse { _errorMessageId.update { R.string.share_view_delete_error_message } } ?: return@launch - val remaining = currentShares.filterNot { it.id == id } - _state.update { - if (remaining.filtered().isEmpty()) ShareScreenState.Empty - else ShareScreenState.Loaded(remaining) - } + publishShares(currentShares.filterNot { it.id == id }) val editingShare = _activeShare.value if (editingShare is ActiveShareState.Editing && editingShare.share.id == id) { @@ -454,9 +446,27 @@ class ShareViewModel( updateActiveShare(value) } + // The list only ever holds active shares, so a draft being edited never reaches it and an + // activated one is inserted rather than silently dropped. private fun replaceInList(updated: Share) { - val shares = currentShares.ifEmpty { return } - _state.update { ShareScreenState.Loaded(shares.map { if (it.id == updated.id) updated else it }) } + val current = currentShares + val index = current.indexOfFirst { it.id == updated.id } + val isActive = updated.shareState == ShareState.ACTIVE + + val shares = when { + index >= 0 && isActive -> current.toMutableList().apply { this[index] = updated } + index >= 0 -> current.filterNot { it.id == updated.id } + isActive -> listOf(updated) + current + else -> return + } + + publishShares(shares) + } + + private fun publishShares(shares: List) { + _state.update { + if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares) + } } // endregion } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index 1b912e17..728af1a8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -16,4 +16,4 @@ sealed class ShareScreenState { data class Loaded(val shares: List): ShareScreenState() } -fun List.filtered(): List = filter { it.shareState != ShareState.DRAFT } +fun List.activeOnly(): List = filter { it.shareState == ShareState.ACTIVE } From 917577a071aa2dbde59a66e0e807349a2b5a673e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 16:04:54 +0200 Subject: [PATCH 121/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 32 ++++-- .../common/ui/share/model/api/share/Share.kt | 16 --- .../ShareDefaultPermissionPresetOptionTest.kt | 102 ------------------ 3 files changed, 21 insertions(+), 129 deletions(-) delete mode 100644 ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 4e7019c3..cd21d05d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -76,6 +76,8 @@ import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.MockShareRepository import kotlinx.coroutines.launch +private const val CUSTOM_SELECTION = "custom" + @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddOrEditShareBottomSheet( @@ -91,10 +93,6 @@ fun AddOrEditShareBottomSheet( var selectedCategory by rememberSaveable(share.id) { mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) } - val initialPresetOption = when (entry) { - ShareEditorEntry.CUSTOMIZE_PERMISSION -> PermissionPresetOption.Custom - else -> null - } var showAdvancedSettings by rememberSaveable(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } val context = LocalContext.current val propertyErrors by viewModel.propertyErrors.collectAsStateWithLifecycle() @@ -145,7 +143,7 @@ fun AddOrEditShareBottomSheet( PermissionsView( share = share, - initialPresetOption = initialPresetOption, + entry = entry, permissionPresets = permissionPresets, viewModel = viewModel ) @@ -218,20 +216,30 @@ private fun ShareCategorySelector( @Composable private fun PermissionsView( share: Share, - initialPresetOption: PermissionPresetOption?, + entry: ShareEditorEntry, permissionPresets: List, viewModel: ShareViewModel ) { - var selectedPreset by rememberSaveable(share.id) { - val default = share.getDefaultPermissionPresetOption(permissionPresets).presetClass - mutableStateOf(initialPresetOption?.presetClass ?: share.permissionPreset ?: default) + // Null follows the preset the backend reports, which is only known once the draft has a source. + // Anything else is the user's explicit pick and wins from then on, so staying on "Can…" is sticky + // even when the toggles they flip happen to add up to a preset. + var userSelection by rememberSaveable(share.id) { + mutableStateOf( + if (entry == ShareEditorEntry.CUSTOMIZE_PERMISSION) CUSTOM_SELECTION else null + ) + } + + val selectedPreset = when (userSelection) { + null -> share.permissionPreset + CUSTOM_SELECTION -> null + else -> userSelection } PermissionPresetDropdown( options = PermissionPresetOption.optionsFor(share, permissionPresets), selectedOption = PermissionPresetOption.from(selectedPreset, permissionPresets), onOptionSelected = { option -> - selectedPreset = option.presetClass + userSelection = option.presetClass ?: CUSTOM_SELECTION option.presetClass?.let { viewModel.updatePermissionPreset(share.id, it, true) } } ) @@ -240,7 +248,9 @@ private fun PermissionsView( share.permissions.forEach { permission -> key(permission.clazz) { - var checked by remember { mutableStateOf(permission.enabled) } + var checked by remember(permission.clazz, permission.enabled) { + mutableStateOf(permission.enabled) + } ShareSwitch( label = permission.displayName, checked = checked, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index d52a3e34..5d62adbb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -13,14 +13,12 @@ import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.toClipEntry import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.permission.Permission -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState -import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -119,20 +117,6 @@ data class Share( val invitedRecipients: List get() = recipients.filterNot { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } - // The preset matching the enabled permissions dictated by the server admin: - // only the read permission -> "Can view", every permission -> "Can edit", any other subset -> "Can ...". - fun getDefaultPermissionPresetOption(permissionPresets: List): PermissionPresetOption { - val presetControlled = permissions.filter { it.presets.isNotEmpty() } - val enabledClasses = presetControlled.filter { it.enabled }.map { it.clazz }.toSet() - if (enabledClasses.isEmpty()) return PermissionPresetOption.Custom - - val matchingPreset = permissionPresets.firstOrNull { preset -> - presetControlled.filter { preset.clazz in it.presets }.map { it.clazz }.toSet() == enabledClasses - } - - return matchingPreset?.let { PermissionPresetOption.Preset(it) } ?: PermissionPresetOption.Custom - } - val canSend: Boolean get() { return sources.isNotEmpty() && diff --git a/ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt b/ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt deleted file mode 100644 index ec43d7a5..00000000 --- a/ui/src/test/java/com/nextcloud/android/common/ui/share/model/api/share/ShareDefaultPermissionPresetOptionTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -package com.nextcloud.android.common.ui.share.model.api.share - -import com.nextcloud.android.common.ui.share.model.api.icon.Icon -import com.nextcloud.android.common.ui.share.model.api.permission.Permission -import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset -import com.nextcloud.android.common.ui.share.model.api.state.ShareState -import com.nextcloud.android.common.ui.share.model.api.user.User -import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption -import org.junit.Assert.assertEquals -import org.junit.Test - -class ShareDefaultPermissionPresetOptionTest { - - @Test - fun `returns view preset when only read is enabled`() { - val share = shareWithEnabled(read = true, update = false, delete = false) - - val result = share.getDefaultPermissionPresetOption(PRESETS) - - assertEquals(PermissionPresetOption.Preset(VIEW_PRESET), result) - } - - @Test - fun `returns edit preset when all permissions are enabled`() { - val share = shareWithEnabled(read = true, update = true, delete = true) - - val result = share.getDefaultPermissionPresetOption(PRESETS) - - assertEquals(PermissionPresetOption.Preset(EDIT_PRESET), result) - } - - @Test - fun `returns custom when only some permissions are enabled`() { - val share = shareWithEnabled(read = true, update = true, delete = false) - - val result = share.getDefaultPermissionPresetOption(PRESETS) - - assertEquals(PermissionPresetOption.Custom, result) - } - - @Test - fun `returns custom when no permission is enabled`() { - val share = shareWithEnabled(read = false, update = false, delete = false) - - val result = share.getDefaultPermissionPresetOption(PRESETS) - - assertEquals(PermissionPresetOption.Custom, result) - } - - @Test - fun `ignores enabled permissions that belong to no preset`() { - val share = shareWith( - permission("read", listOf("view", "edit"), enabled = true), - permission("update", listOf("edit"), enabled = false), - permission("delete", listOf("edit"), enabled = false), - permission("download", emptyList(), enabled = true) - ) - - val result = share.getDefaultPermissionPresetOption(PRESETS) - - assertEquals(PermissionPresetOption.Preset(VIEW_PRESET), result) - } - - private fun shareWithEnabled(read: Boolean, update: Boolean, delete: Boolean) = shareWith( - permission("read", listOf("view", "edit"), read), - permission("update", listOf("edit"), update), - permission("delete", listOf("edit"), delete) - ) - - private fun shareWith(vararg permissions: Permission) = Share( - id = "1", - owner = OWNER, - lastUpdated = 0L, - shareState = ShareState.DRAFT, - sources = emptyList(), - recipients = emptyList(), - properties = emptyList(), - permissions = permissions.toList() - ) - - private fun permission(clazz: String, presets: List, enabled: Boolean) = Permission( - clazz = clazz, - displayName = clazz, - priority = 1, - presets = presets, - enabled = enabled - ) - - private companion object { - private val VIEW_PRESET = PermissionPreset(clazz = "view", displayName = "Can view") - private val EDIT_PRESET = PermissionPreset(clazz = "edit", displayName = "Can edit") - private val PRESETS = listOf(VIEW_PRESET, EDIT_PRESET) - private val OWNER = User(userId = "owner", displayName = "Owner", icon = Icon()) - } -} From f1ac8464fec492a5fe8beddaaf8749834e38bd5e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 16:06:02 +0200 Subject: [PATCH 122/174] wip Signed-off-by: alperozturk96 --- .../share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index cd21d05d..88676dbc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -220,9 +220,6 @@ private fun PermissionsView( permissionPresets: List, viewModel: ShareViewModel ) { - // Null follows the preset the backend reports, which is only known once the draft has a source. - // Anything else is the user's explicit pick and wins from then on, so staying on "Can…" is sticky - // even when the toggles they flip happen to add up to a preset. var userSelection by rememberSaveable(share.id) { mutableStateOf( if (entry == ShareEditorEntry.CUSTOMIZE_PERMISSION) CUSTOM_SELECTION else null From 41f5e1c8271561f040d22e5ab116e09b513054d2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 16:19:50 +0200 Subject: [PATCH 123/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 9 +++ .../bottomsheet/AddOrEditShareBottomSheet.kt | 45 ++++++++++++-- .../dialog/DiscardShareConfirmationDialog.kt | 58 +++++++++++++++++++ .../component/property/SharePropertyView.kt | 5 +- .../ui/share/model/api/property/Property.kt | 6 ++ .../common/ui/share/model/api/share/Share.kt | 4 ++ ui/src/main/res/values/strings.xml | 7 +++ 7 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 8ecaaf03..8ef85565 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -106,6 +106,9 @@ class ShareViewModel( private val _pendingProperties = MutableStateFlow>(emptySet()) val pendingProperties: StateFlow> = _pendingProperties + private val _unsavedProperties = MutableStateFlow>(emptySet()) + val unsavedProperties: StateFlow> = _unsavedProperties + private val propertyUpdateJobs = mutableMapOf() private var secretUpdateJob: Job? = null @@ -345,9 +348,14 @@ class ShareViewModel( // endregion // region properties + fun onPropertyEdited(clazz: String, unsaved: Boolean) { + _unsavedProperties.update { if (unsaved) it + clazz else it - clazz } + } + fun updateProperty(shareId: String, clazz: String, value: String?) { propertyUpdateJobs[clazz]?.cancel() _pendingProperties.update { it + clazz } + _unsavedProperties.update { it - clazz } if (value.isNullOrEmpty()) { _propertyErrors.update { it - clazz } } @@ -423,6 +431,7 @@ class ShareViewModel( fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } + _unsavedProperties.update { emptySet() } _shareReadyToCopy.update { null } updateEditorEntry(entry) updateActiveShare(value?.toActiveShare() ?: ActiveShareState.None) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 88676dbc..bc4b0dd5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SheetValue import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -42,6 +43,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -59,6 +61,7 @@ import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.CustomLink import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch +import com.nextcloud.android.common.ui.share.component.dialog.DiscardShareConfirmationDialog import com.nextcloud.android.common.ui.share.component.property.SharePropertyView import com.nextcloud.android.common.ui.share.model.api.permission.Permission import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset @@ -88,7 +91,22 @@ fun AddOrEditShareBottomSheet( entry: ShareEditorEntry = ShareEditorEntry.EDIT, onDismissDraft: (Share) -> Unit = {} ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val unsavedProperties by viewModel.unsavedProperties.collectAsStateWithLifecycle() + + val hasUnsavedWork by rememberUpdatedState(share.hasUnsentInput || unsavedProperties.isNotEmpty()) + var showDiscardConfirmation by rememberSaveable(share.id) { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { value -> + val blocked = value == SheetValue.Hidden && hasUnsavedWork + if (blocked) { + showDiscardConfirmation = true + } + !blocked + } + ) val categories = remember { ShareCategory.entries.toList() } var selectedCategory by rememberSaveable(share.id) { mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) @@ -106,10 +124,14 @@ fun AddOrEditShareBottomSheet( ) { ModalBottomSheet( onDismissRequest = { - if (share.shareState == ShareState.DRAFT) { - onDismissDraft(share) - } else { - viewModel.setActiveShare(null) + when { + hasUnsavedWork -> { + showDiscardConfirmation = true + scope.launch { sheetState.show() } + } + + share.shareState == ShareState.DRAFT -> onDismissDraft(share) + else -> viewModel.setActiveShare(null) } }, sheetState = sheetState, @@ -182,6 +204,19 @@ fun AddOrEditShareBottomSheet( } } } + + if (showDiscardConfirmation) { + val isDraft = share.shareState == ShareState.DRAFT + + DiscardShareConfirmationDialog( + isDraft = isDraft, + onConfirm = { + showDiscardConfirmation = false + if (isDraft) onDismissDraft(share) else viewModel.setActiveShare(null) + }, + onDismiss = { showDiscardConfirmation = false } + ) + } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt new file mode 100644 index 00000000..4876d6c8 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component.dialog + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.nextcloud.android.common.ui.R + +@Composable +fun DiscardShareConfirmationDialog( + isDraft: Boolean, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val titleId = if (isDraft) { + R.string.share_view_discard_draft_title + } else { + R.string.share_view_discard_changes_title + } + val messageId = if (isDraft) { + R.string.share_view_discard_draft_message + } else { + R.string.share_view_discard_changes_message + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(titleId)) }, + text = { Text(stringResource(messageId)) }, + confirmButton = { + FilledTonalButton( + onClick = onConfirm, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { + Text(stringResource(R.string.share_view_discard_confirm_action)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.share_view_discard_keep_editing_action)) + } + } + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index fcf0fcd9..8fa9c687 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -127,7 +127,10 @@ private fun ShareTextPropertyField( OutlinedTextField( value = value, - onValueChange = { value = it }, + onValueChange = { + value = it + viewModel.onPropertyEdited(clazz, it != committedValue) + }, label = { Text(displayName) }, visualTransformation = visualTransformation, isError = isError, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 8be8eaf1..2889d5e0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -23,6 +23,9 @@ sealed class Property { abstract val required: Boolean abstract val advanced: Boolean abstract val value: String? + + open val isFilledIn: Boolean + get() = !value.isNullOrBlank() } @Serializable @@ -37,6 +40,9 @@ data class PropertyBoolean( override val value: String? = null ) : Property() { fun isTrue(): Boolean = value == "true" + + override val isFilledIn: Boolean + get() = isTrue() } @Serializable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 5d62adbb..e6c65ec1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -117,6 +117,10 @@ data class Share( val invitedRecipients: List get() = recipients.filterNot { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } + val hasUnsentInput: Boolean + get() = shareState == ShareState.DRAFT && + (invitedRecipients.isNotEmpty() || properties.any { it.isFilledIn }) + val canSend: Boolean get() { return sources.isNotEmpty() && diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 032954aa..ab720197 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -39,6 +39,13 @@ Delete share? This share will be permanently removed. + Discard this share? + It has not been shared yet, so everything you entered will be lost. + Discard your changes? + What you updated has not been saved yet and will be lost. + Discard + Keep editing + Failed to fetch shares. Failed to delete share. Failed to create share. From cd43ad9f4460b04c8946b2fe468feec8d97dfca8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 16:27:26 +0200 Subject: [PATCH 124/174] wip Signed-off-by: alperozturk96 --- .../component/property/SharePropertyView.kt | 35 +++++++++++-------- .../ui/share/model/api/property/Property.kt | 9 ++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 8fa9c687..3aaf4853 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -35,6 +35,10 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum import com.nextcloud.android.common.ui.share.model.api.property.PropertyPassword import com.nextcloud.android.common.ui.share.model.api.property.PropertyString +private const val SINGLE_LINE_COUNT = 1 +private const val MULTILINE_MIN_LINES = 1 +private const val MULTILINE_MAX_LINES = 3 + @Composable fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { val propertyErrors by viewModel.propertyErrors.collectAsStateWithLifecycle() @@ -80,18 +84,14 @@ private fun SharePropertyField( is PropertyString -> ShareTextPropertyField( shareId = shareId, - clazz = property.clazz, - displayName = property.displayName, - initialValue = property.value ?: "", + property = property, isError = isError, viewModel = viewModel ) is PropertyPassword -> ShareTextPropertyField( shareId = shareId, - clazz = property.clazz, - displayName = property.displayName, - initialValue = property.value ?: "", + property = property, isError = isError, viewModel = viewModel, visualTransformation = PasswordVisualTransformation() @@ -114,24 +114,29 @@ private fun SharePropertyField( @Composable private fun ShareTextPropertyField( shareId: String, - clazz: String, - displayName: String, - initialValue: String, + property: Property, isError: Boolean, viewModel: ShareViewModel, visualTransformation: VisualTransformation = VisualTransformation.None ) { + val stringProperty = property as? PropertyString + val isMultiline = stringProperty?.isMultiline == true + val maxLength = stringProperty?.maxLength + val clazz = property.clazz + val initialValue = property.value ?: "" var value by remember(clazz) { mutableStateOf(initialValue) } var committedValue by remember(clazz) { mutableStateOf(initialValue) } var wasFocused by remember(clazz) { mutableStateOf(false) } OutlinedTextField( value = value, - onValueChange = { - value = it - viewModel.onPropertyEdited(clazz, it != committedValue) + onValueChange = { newValue -> + if (maxLength == null || newValue.length <= maxLength) { + value = newValue + viewModel.onPropertyEdited(clazz, newValue != committedValue) + } }, - label = { Text(displayName) }, + label = { Text(property.displayName) }, visualTransformation = visualTransformation, isError = isError, modifier = Modifier @@ -144,6 +149,8 @@ private fun ShareTextPropertyField( } wasFocused = focusState.isFocused }, - singleLine = true + singleLine = !isMultiline, + minLines = if (isMultiline) MULTILINE_MIN_LINES else SINGLE_LINE_COUNT, + maxLines = if (isMultiline) MULTILINE_MAX_LINES else SINGLE_LINE_COUNT ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 2889d5e0..07f3aeb7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -96,4 +96,11 @@ data class PropertyString( override val value: String? = null, @SerialName("min_length") val minLength: Int? = null, @SerialName("max_length") val maxLength: Int? = null -) : Property() +) : Property() { + companion object { + private const val SINGLE_LINE_MAX_LENGTH = 255 + } + + val isMultiline: Boolean + get() = maxLength == null || maxLength > SINGLE_LINE_MAX_LENGTH +} From 25dfe6ce3bd8ed91c582802cb5750e210b648b82 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 11:02:50 +0200 Subject: [PATCH 125/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 98 ++++++++++--------- .../dialog/DiscardShareConfirmationDialog.kt | 58 ----------- ui/src/main/res/values/strings.xml | 7 -- 3 files changed, 52 insertions(+), 111 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index bc4b0dd5..3d463b1b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.size 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.Close import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator @@ -27,8 +29,10 @@ import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -43,14 +47,15 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle @@ -61,7 +66,6 @@ import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.CustomLink import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch -import com.nextcloud.android.common.ui.share.component.dialog.DiscardShareConfirmationDialog import com.nextcloud.android.common.ui.share.component.property.SharePropertyView import com.nextcloud.android.common.ui.share.model.api.permission.Permission import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset @@ -91,21 +95,11 @@ fun AddOrEditShareBottomSheet( entry: ShareEditorEntry = ShareEditorEntry.EDIT, onDismissDraft: (Share) -> Unit = {} ) { - val unsavedProperties by viewModel.unsavedProperties.collectAsStateWithLifecycle() - - val hasUnsavedWork by rememberUpdatedState(share.hasUnsentInput || unsavedProperties.isNotEmpty()) - var showDiscardConfirmation by rememberSaveable(share.id) { mutableStateOf(false) } + var dismissAllowed by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, - confirmValueChange = { value -> - val blocked = value == SheetValue.Hidden && hasUnsavedWork - if (blocked) { - showDiscardConfirmation = true - } - !blocked - } + confirmValueChange = { value -> dismissAllowed || value != SheetValue.Hidden } ) val categories = remember { ShareCategory.entries.toList() } var selectedCategory by rememberSaveable(share.id) { @@ -123,18 +117,10 @@ fun AddOrEditShareBottomSheet( .fillMaxSize() ) { ModalBottomSheet( - onDismissRequest = { - when { - hasUnsavedWork -> { - showDiscardConfirmation = true - scope.launch { sheetState.show() } - } - - share.shareState == ShareState.DRAFT -> onDismissDraft(share) - else -> viewModel.setActiveShare(null) - } - }, + onDismissRequest = {}, sheetState = sheetState, + dragHandle = null, + properties = ModalBottomSheetProperties(shouldDismissOnBackPress = false), containerColor = MaterialTheme.colorScheme.surface, ) { Column( @@ -142,12 +128,37 @@ fun AddOrEditShareBottomSheet( .fillMaxWidth() .verticalScroll(rememberScrollState()) ) { - Text( - text = share.title(context), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(16.dp) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 4.dp, top = 12.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = share.title(context), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + + IconButton(onClick = { + dismissAllowed = true + scope.launch { + sheetState.hide() + if (share.shareState == ShareState.DRAFT) onDismissDraft(share) else viewModel.setActiveShare( + null + ) + } + }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "close bottom sheet", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } ShareCategorySelector( share = share, @@ -204,19 +215,6 @@ fun AddOrEditShareBottomSheet( } } } - - if (showDiscardConfirmation) { - val isDraft = share.shareState == ShareState.DRAFT - - DiscardShareConfirmationDialog( - isDraft = isDraft, - onConfirm = { - showDiscardConfirmation = false - if (isDraft) onDismissDraft(share) else viewModel.setActiveShare(null) - }, - onDismiss = { showDiscardConfirmation = false } - ) - } } } @@ -231,7 +229,11 @@ private fun ShareCategorySelector( // only allow user to select between taps if it is draft share if (share.shareState != ShareState.DRAFT) return - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth().padding(8.dp)) { + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { categories.forEachIndexed { index, category -> SegmentedButton( selected = selectedCategory == category, @@ -346,7 +348,11 @@ private fun BasicSettingsSection( return } - Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { share.basicProperties.forEach { property -> key(property.clazz) { SharePropertyView( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt deleted file mode 100644 index 4876d6c8..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DiscardShareConfirmationDialog.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.android.common.ui.share.component.dialog - -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.nextcloud.android.common.ui.R - -@Composable -fun DiscardShareConfirmationDialog( - isDraft: Boolean, - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - val titleId = if (isDraft) { - R.string.share_view_discard_draft_title - } else { - R.string.share_view_discard_changes_title - } - val messageId = if (isDraft) { - R.string.share_view_discard_draft_message - } else { - R.string.share_view_discard_changes_message - } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(titleId)) }, - text = { Text(stringResource(messageId)) }, - confirmButton = { - FilledTonalButton( - onClick = onConfirm, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) - ) { - Text(stringResource(R.string.share_view_discard_confirm_action)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.share_view_discard_keep_editing_action)) - } - } - ) -} diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index ab720197..032954aa 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -39,13 +39,6 @@ Delete share? This share will be permanently removed. - Discard this share? - It has not been shared yet, so everything you entered will be lost. - Discard your changes? - What you updated has not been saved yet and will be lost. - Discard - Keep editing - Failed to fetch shares. Failed to delete share. Failed to create share. From 2c8a92f93db26ecb4c88510b54c223536c1aae40 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 30 Jul 2026 11:14:57 +0200 Subject: [PATCH 126/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 11 ----------- ui/src/main/res/values/strings.xml | 1 - 2 files changed, 12 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 3d463b1b..9be66861 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -193,17 +193,6 @@ fun AddOrEditShareBottomSheet( viewModel = viewModel ) - if (hasPropertyErrors) { - Text( - text = stringResource(R.string.share_view_property_error_warning), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp) - ) - } - if (share.canSend) { ActionButtons( share = share, diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 032954aa..38bb745f 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -43,7 +43,6 @@ Failed to delete share. Failed to create share. Failed to update share. - Some settings couldn\'t be saved. Fix the highlighted fields before sharing. Change permission Participants From 620c05a99f5df5b1e1c5997504e15aaddb6d4712 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 12:19:16 +0200 Subject: [PATCH 127/174] wip Signed-off-by: alperozturk96 --- .../property/datepicker/ShareDatePicker.kt | 66 ++++++++++++------- .../ui/util/extensions/StringExtensions.kt | 12 ++++ ui/src/main/res/drawable/ic_cancel.xml | 20 ++++++ 3 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/StringExtensions.kt create mode 100644 ui/src/main/res/drawable/ic_cancel.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 5bf623b3..a8187db6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -7,18 +7,20 @@ package com.nextcloud.android.common.ui.share.component.property.datepicker -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,6 +33,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.component.property.datepicker.util.NoPastSelectableDates import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate +import com.nextcloud.android.common.ui.util.extensions.Strings @Composable fun ShareDatePicker(property: PropertyDate, isError: Boolean, onDateSelected: (String) -> Unit) { @@ -38,36 +41,53 @@ fun ShareDatePicker(property: PropertyDate, isError: Boolean, onDateSelected: (S var showDatePicker by remember { mutableStateOf(false) } var dateValue by remember(property.clazz) { mutableStateOf(formatter.formatIsoForDisplay(property.value)) } - Box(modifier = Modifier.fillMaxWidth()) { - OutlinedTextField( - value = dateValue, - onValueChange = {}, - readOnly = true, - enabled = true, - isError = isError, - modifier = Modifier.fillMaxWidth(), - label = { Text(formatter.getDisplayName(property)) }, - trailingIcon = { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + if (interaction is PressInteraction.Release) { + showDatePicker = true + } + } + } + + OutlinedTextField( + value = dateValue, + onValueChange = {}, + readOnly = true, + enabled = true, + isError = isError, + modifier = Modifier.fillMaxWidth(), + interactionSource = interactionSource, + label = { Text(formatter.getDisplayName(property)) }, + trailingIcon = { + val isDateBlank = dateValue.isBlank() + val iconId = if (isDateBlank) { + R.drawable.ic_calendar + } else { + R.drawable.ic_cancel + } + IconButton(onClick = { + if (isDateBlank) { + showDatePicker = true + } else { + dateValue = Strings.EMPTY + onDateSelected(Strings.EMPTY) + } + }) { Icon( - painter = painterResource(R.drawable.ic_calendar), + painter = painterResource(iconId), contentDescription = null ) } - ) - - Box( - modifier = Modifier - .matchParentSize() - .clickable { showDatePicker = true } - ) - } + } + ) if (showDatePicker) { DatePickerModal( formatter = formatter, onDateSelected = { displayDate, isoDate -> - dateValue = displayDate ?: "" - onDateSelected(isoDate ?: "") + dateValue = displayDate ?: Strings.EMPTY + onDateSelected(isoDate ?: Strings.EMPTY) }, onDismiss = { showDatePicker = false } ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/StringExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/StringExtensions.kt new file mode 100644 index 00000000..954245fe --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/StringExtensions.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.util.extensions + +object Strings { + const val EMPTY = "" +} diff --git a/ui/src/main/res/drawable/ic_cancel.xml b/ui/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 00000000..5d968727 --- /dev/null +++ b/ui/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,20 @@ + + + + + + From e0474415b48d573a537a07b99651b81b9bfdcfe1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 12:28:29 +0200 Subject: [PATCH 128/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 4 ++- .../android/common/ui/share/ShareViewModel.kt | 25 +++++++------------ .../component/property/SharePropertyView.kt | 1 - .../ui/share/model/ui/ShareScreenState.kt | 2 +- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 78d256ec..4f2e89f0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -141,6 +141,7 @@ private fun ShareScreen( } is ShareScreenState.Empty -> ShareList( + isRefreshing = false, shares = emptyList(), permissionPresets = permissionPresets, paddingValues = paddingValues, @@ -148,6 +149,7 @@ private fun ShareScreen( ) is ShareScreenState.Loaded -> ShareList( + isRefreshing = state.refreshing, shares = state.shares, permissionPresets = permissionPresets, paddingValues = paddingValues, @@ -174,13 +176,13 @@ private fun ShareScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShareList( + isRefreshing: Boolean, shares: List, permissionPresets: List, paddingValues: PaddingValues, viewModel: ShareViewModel ) { val context = LocalContext.current - val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() PullToRefreshBox( isRefreshing = isRefreshing, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 8ef85565..f281071f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -69,9 +69,6 @@ class ShareViewModel( private val _state = MutableStateFlow(ShareScreenState.Loading) val state: StateFlow = _state - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow = _isRefreshing - private val _isPreparingLink = MutableStateFlow(false) val isPreparingLink: StateFlow = _isPreparingLink @@ -106,9 +103,6 @@ class ShareViewModel( private val _pendingProperties = MutableStateFlow>(emptySet()) val pendingProperties: StateFlow> = _pendingProperties - private val _unsavedProperties = MutableStateFlow>(emptySet()) - val unsavedProperties: StateFlow> = _unsavedProperties - private val propertyUpdateJobs = mutableMapOf() private var secretUpdateJob: Job? = null @@ -164,12 +158,17 @@ class ShareViewModel( // region shares list fun refreshShares() { - if (_isRefreshing.value) return + val state = _state.value as? ShareScreenState.Loaded ?: return + if (state.refreshing) return viewModelScope.launch(Dispatchers.IO) { - _isRefreshing.update { true } + _state.update { + state.copy(refreshing = true) + } loadShares() - _isRefreshing.update { false } + _state.update { + state.copy(refreshing = false) + } } } @@ -348,14 +347,9 @@ class ShareViewModel( // endregion // region properties - fun onPropertyEdited(clazz: String, unsaved: Boolean) { - _unsavedProperties.update { if (unsaved) it + clazz else it - clazz } - } - fun updateProperty(shareId: String, clazz: String, value: String?) { propertyUpdateJobs[clazz]?.cancel() _pendingProperties.update { it + clazz } - _unsavedProperties.update { it - clazz } if (value.isNullOrEmpty()) { _propertyErrors.update { it - clazz } } @@ -431,7 +425,6 @@ class ShareViewModel( fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } - _unsavedProperties.update { emptySet() } _shareReadyToCopy.update { null } updateEditorEntry(entry) updateActiveShare(value?.toActiveShare() ?: ActiveShareState.None) @@ -474,7 +467,7 @@ class ShareViewModel( private fun publishShares(shares: List) { _state.update { - if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares) + if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares, false) } } // endregion diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 3aaf4853..20c191c1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -133,7 +133,6 @@ private fun ShareTextPropertyField( onValueChange = { newValue -> if (maxLength == null || newValue.length <= maxLength) { value = newValue - viewModel.onPropertyEdited(clazz, newValue != committedValue) } }, label = { Text(property.displayName) }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index 728af1a8..81fb4038 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -13,7 +13,7 @@ import com.nextcloud.android.common.ui.share.model.api.state.ShareState sealed class ShareScreenState { data object Empty: ShareScreenState() data object Loading: ShareScreenState() - data class Loaded(val shares: List): ShareScreenState() + data class Loaded(val shares: List, val refreshing: Boolean): ShareScreenState() } fun List.activeOnly(): List = filter { it.shareState == ShareState.ACTIVE } From d92691b9cf3dab545d63f5b25500294edb0e3959 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 13:37:45 +0200 Subject: [PATCH 129/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index f281071f..19d34117 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -162,21 +162,22 @@ class ShareViewModel( if (state.refreshing) return viewModelScope.launch(Dispatchers.IO) { - _state.update { - state.copy(refreshing = true) - } + setRefreshing(true) loadShares() - _state.update { - state.copy(refreshing = false) - } + setRefreshing(false) + } + } + + private fun setRefreshing(refreshing: Boolean) { + _state.update { + if (it is ShareScreenState.Loaded) it.copy(refreshing = refreshing) else it } } private suspend fun loadShares() { _errorMessageId.update { null } - - val fetched = fetchAllShares() ?: return - publishShares(fetched.activeOnly()) + val fetched = fetchAllShares() + publishShares(fetched?.activeOnly() ?: currentShares) } // The server pages drafts and active shares together while the list only renders active ones, From f7243a8a1edcd95d94d7d35f5611a8b714684d4f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 13:42:31 +0200 Subject: [PATCH 130/174] wip Signed-off-by: alperozturk96 --- .../nextcloud/android/common/ui/share/ShareViewModel.kt | 8 ++------ .../component/bottomsheet/AddOrEditShareBottomSheet.kt | 7 ++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index 19d34117..cc6b44cb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -72,9 +72,6 @@ class ShareViewModel( private val _isPreparingLink = MutableStateFlow(false) val isPreparingLink: StateFlow = _isPreparingLink - private val _shareReadyToCopy = MutableStateFlow(null) - val shareReadyToCopy: StateFlow = _shareReadyToCopy - private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare @@ -249,7 +246,6 @@ class ShareViewModel( if (updated != null) { refreshActiveShare(ActiveShareState.Activating(updated)) replaceInList(updated) - _shareReadyToCopy.update { updated } } _isPreparingLink.update { false } @@ -257,7 +253,8 @@ class ShareViewModel( } fun onLinkCopied() { - _shareReadyToCopy.update { null } + val activating = _activeShare.value as? ActiveShareState.Activating ?: return + updateActiveShare(ActiveShareState.Editing(activating.share)) } private suspend fun applyState(id: String, shareState: ShareState): Share? { @@ -426,7 +423,6 @@ class ShareViewModel( fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } - _shareReadyToCopy.update { null } updateEditorEntry(entry) updateActiveShare(value?.toActiveShare() ?: ActiveShareState.None) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 9be66861..9a567bc7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -76,6 +76,7 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User +import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry @@ -403,10 +404,10 @@ private fun ActionButtons( val localClipboard = LocalClipboard.current val scope = rememberCoroutineScope() val isPreparingLink by viewModel.isPreparingLink.collectAsStateWithLifecycle() - val shareReadyToCopy by viewModel.shareReadyToCopy.collectAsStateWithLifecycle() + val activeShare by viewModel.activeShare.collectAsStateWithLifecycle() - LaunchedEffect(shareReadyToCopy) { - val ready = shareReadyToCopy ?: return@LaunchedEffect + LaunchedEffect(activeShare) { + val ready = (activeShare as? ActiveShareState.Activating)?.share ?: return@LaunchedEffect ready.getClipEntry(internalLink, category)?.let { localClipboard.setClipEntry(it) } viewModel.onLinkCopied() } From 872aaba3432337b8ce1e291a207bfdea18ee9b8f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 13:43:00 +0200 Subject: [PATCH 131/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareViewModel.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt index cc6b44cb..1b822a59 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt @@ -78,16 +78,6 @@ class ShareViewModel( private val _editorEntry = MutableStateFlow(savedState.editorEntry) val editorEntry: StateFlow = _editorEntry - val searchQuery: StateFlow = savedState.searchQuery - - @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - val recipientSearchResults: StateFlow> = searchQuery - .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) - .distinctUntilChanged() - .filter { it.isNotBlank() } - .mapLatest { query -> searchRecipients(query) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SEARCH_SUBSCRIPTION_TIMEOUT), emptyList()) - private val _permissionPresets = MutableStateFlow>(emptyList()) val permissionPresets: StateFlow> = _permissionPresets @@ -100,12 +90,22 @@ class ShareViewModel( private val _pendingProperties = MutableStateFlow>(emptySet()) val pendingProperties: StateFlow> = _pendingProperties + private val currentShares: List + get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() + private val propertyUpdateJobs = mutableMapOf() - private var secretUpdateJob: Job? = null + val searchQuery: StateFlow = savedState.searchQuery - private val currentShares: List - get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) + val recipientSearchResults: StateFlow> = searchQuery + .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) + .distinctUntilChanged() + .filter { it.isNotBlank() } + .mapLatest { query -> searchRecipients(query) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SEARCH_SUBSCRIPTION_TIMEOUT), emptyList()) + + private var secretUpdateJob: Job? = null init { loadInitialData() From af7a4dc2923b30d60f766f1b965fb2fe107caa4c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 13:46:31 +0200 Subject: [PATCH 132/174] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/android/common/ui/share/ShareScreen.kt | 2 -- .../ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 4f2e89f0..88c7fa4b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -106,7 +106,6 @@ private fun ShareScreen( val screenState by viewModel.state.collectAsStateWithLifecycle() val activeShare by viewModel.activeShare.collectAsStateWithLifecycle() val permissionPresets by viewModel.permissionPresets.collectAsStateWithLifecycle() - val editorEntry by viewModel.editorEntry.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current @@ -164,7 +163,6 @@ private fun ShareScreen( internalLink = internalLink, viewModel = viewModel, permissionPresets = permissionPresets, - entry = editorEntry, onDismissDraft = { draftShare -> viewModel.deleteShare(draftShare.id) viewModel.setActiveShare(null) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 9a567bc7..00ade30e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -93,9 +93,9 @@ fun AddOrEditShareBottomSheet( internalLink: String, viewModel: ShareViewModel, permissionPresets: List = emptyList(), - entry: ShareEditorEntry = ShareEditorEntry.EDIT, onDismissDraft: (Share) -> Unit = {} ) { + val entry by viewModel.editorEntry.collectAsStateWithLifecycle() var dismissAllowed by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState( From 27e6d0102ddfc34c6f76d355a5d0b9f8ce734ff4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 13:52:27 +0200 Subject: [PATCH 133/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/model/api/share/Share.kt | 11 ++--------- ui/src/main/res/values/strings.xml | 3 +-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index e6c65ec1..e77041ea 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -117,10 +117,6 @@ data class Share( val invitedRecipients: List get() = recipients.filterNot { it.clazz == Recipient.TOKEN_RECIPIENT_CLASS } - val hasUnsentInput: Boolean - get() = shareState == ShareState.DRAFT && - (invitedRecipients.isNotEmpty() || properties.any { it.isFilledIn }) - val canSend: Boolean get() { return sources.isNotEmpty() && @@ -130,10 +126,7 @@ data class Share( } fun title(context: Context): String { - return if (shareState == ShareState.DRAFT) { - context.getString(R.string.share_view_bottom_sheet_new_title) - } else { - context.getString(R.string.share_view_bottom_sheet_edit_title, sources.firstOrNull()?.displayName) - } + val firstRecipient = sources.firstOrNull()?.displayName ?: "..." + return context.getString(R.string.share_view_bottom_sheet_title, firstRecipient) } } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 38bb745f..a74584db 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -12,8 +12,7 @@ Share link Share link (%1$d) - Create a new share - Share %s + Share "%s" Settings From 1f781e351b13409e16cbe2d9e43e1631c7089f25 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 13:53:44 +0200 Subject: [PATCH 134/174] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/android/common/ui/share/ShareScreen.kt | 2 ++ .../android/common/ui/share/component/SelectRecipientField.kt | 2 +- .../ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 2 +- .../common/ui/share/component/property/SharePropertyView.kt | 2 +- .../android/common/ui/share/{ => viewmodel}/ShareSavedState.kt | 2 +- .../android/common/ui/share/{ => viewmodel}/ShareViewModel.kt | 2 +- .../common/ui/share/{ => viewmodel}/ShareViewModelFactory.kt | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/share/{ => viewmodel}/ShareSavedState.kt (95%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/{ => viewmodel}/ShareViewModel.kt (99%) rename ui/src/main/java/com/nextcloud/android/common/ui/share/{ => viewmodel}/ShareViewModelFactory.kt (95%) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 88c7fa4b..e0f046fe 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -93,6 +93,8 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareItemType import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModelFactory private val FIRST_ITEM_TOP_SPACING = 16.dp private val ITEM_SPACING = 2.dp diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 4d1a0f3b..05521c60 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -42,7 +42,7 @@ import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.svg.SvgDecoder import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 00ade30e..8b51b58f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -61,7 +61,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.CustomLink import com.nextcloud.android.common.ui.share.component.SelectRecipientField diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 20c191c1..6a1a3173 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.ShareViewModel +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.datepicker.ShareDatePicker import com.nextcloud.android.common.ui.share.model.api.property.Property diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt similarity index 95% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt index 63753bcf..3b746273 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareSavedState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share +package com.nextcloud.android.common.ui.share.viewmodel import androidx.lifecycle.SavedStateHandle import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt similarity index 99% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 1b822a59..2683aa0e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share +package com.nextcloud.android.common.ui.share.viewmodel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt similarity index 95% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt index b6fa9307..cea0cdda 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareViewModelFactory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -package com.nextcloud.android.common.ui.share +package com.nextcloud.android.common.ui.share.viewmodel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel From 8098e0f937b60810ef1bec49a57893954f185448 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 14:22:28 +0200 Subject: [PATCH 135/174] wip Signed-off-by: alperozturk96 --- .../ui/share/avatar/ShareAvatarRepository.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt new file mode 100644 index 00000000..6316f315 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt @@ -0,0 +1,37 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.avatar + +import android.util.Log +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.model.dataOrElse +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.source.Source +import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ShareAvatarRepository(private val credentials: ServerCredentials) { + companion object { + private const val TAG = "ShareAvatarRepository" + private const val SHARES_PAGE_SIZE = 3 + } + + suspend fun fetchShareAvatars(sourceId: String): List? = withContext(Dispatchers.IO) { + val client = NextcloudHttpClient.create(credentials) + val shareRepository = ShareRemoteRepository(client) + val result = shareRepository.fetchShares( + filterSourceTypeValue = sourceId, + limit = SHARES_PAGE_SIZE, + filterSourceTypeClass = Source.NODE_SOURCE_CLASS, + lastShareID = null + ) + return@withContext result.dataOrElse { Log.e(TAG, "failed to fetch avatars") } + } +} From fd4e365ee6837b14c9db17e2f2b4ec1dd9b9de8e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:08:00 +0200 Subject: [PATCH 136/174] wip Signed-off-by: alperozturk96 --- .../share/repository/MockShareRepository.kt | 11 ++---- .../share/repository/ShareRemoteRepository.kt | 2 +- .../ui/share/repository/ShareRepository.kt | 2 +- .../ui/share/viewmodel/ShareViewModel.kt | 36 ++++--------------- 4 files changed, 11 insertions(+), 40 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 1a2bb62d..381753f9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -325,23 +325,18 @@ class MockShareRepository : ShareRepository { override suspend fun fetchShares( filterSourceTypeValue: String, - limit: Int, + limit: Int?, filterSourceTypeClass: String?, lastShareID: String? ): NetworkResult> { - var result = mockShares.filter { share -> + val result = mockShares.filter { share -> share.sources.any { it.value == filterSourceTypeValue && (filterSourceTypeClass == null || it.clazz == filterSourceTypeClass) } } - if (lastShareID != null) { - val index = result.indexOfFirst { it.id == lastShareID } - if (index >= 0) result = result.drop(index + 1) - } - - return NetworkResult.Success(result.take(limit)) + return NetworkResult.Success(result) } override suspend fun updateShareState( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 0e20df77..c0069d7a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -93,7 +93,7 @@ class ShareRemoteRepository( override suspend fun fetchShares( filterSourceTypeValue: String, - limit: Int, + limit: Int?, filterSourceTypeClass: String?, lastShareID: String? ): NetworkResult> { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 6f5cae0d..929707a0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -39,7 +39,7 @@ interface ShareRepository { suspend fun fetchShares( filterSourceTypeValue: String, - limit: Int, + limit: Int? = null, filterSourceTypeClass: String? = null, lastShareID: String? = null ): NetworkResult> diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 2683aa0e..442af66f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -60,8 +60,6 @@ class ShareViewModel( private const val SEARCH_SUBSCRIPTION_TIMEOUT = 5_000L private const val RECIPIENT_SEARCH_LIMIT = 10 private const val RECIPIENT_SEARCH_OFFSET = 0 - private const val SHARES_PAGE_SIZE = 100 - private const val MAX_SHARE_PAGES = 20 } private val savedState = ShareSavedState(savedStateHandle) @@ -173,35 +171,13 @@ class ShareViewModel( private suspend fun loadShares() { _errorMessageId.update { null } - val fetched = fetchAllShares() - publishShares(fetched?.activeOnly() ?: currentShares) - } - - // The server pages drafts and active shares together while the list only renders active ones, - // so a file with a page worth of abandoned drafts would otherwise hide every real share. - private suspend fun fetchAllShares(): List? { - val shares = mutableListOf() - var cursor: String? = null - var page = 0 - - while (page < MAX_SHARE_PAGES) { - val result = repository.fetchShares( - filterSourceTypeValue = sourceId, - limit = SHARES_PAGE_SIZE, - filterSourceTypeClass = Source.NODE_SOURCE_CLASS, - lastShareID = cursor - ) - val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } - ?: return null - - shares += fetched - if (fetched.size < SHARES_PAGE_SIZE) break - - cursor = fetched.last().id - page++ + val result = repository.fetchShares( + filterSourceTypeValue = sourceId, + filterSourceTypeClass = Source.NODE_SOURCE_CLASS + ) + result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } }?.let { fetched -> + publishShares(fetched.activeOnly()) } - - return shares } // endregion From 6e64cc6417028d0442f3384597e6f1637a72c54e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:13:30 +0200 Subject: [PATCH 137/174] wip Signed-off-by: alperozturk96 --- .../common/ui/share/repository/ShareRemoteRepository.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index c0069d7a..36384705 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -98,8 +98,8 @@ class ShareRemoteRepository( lastShareID: String? ): NetworkResult> { val queryParams = buildString { - append("?limit=$limit") - append("&filterSourceTypeValue=${filterSourceTypeValue.urlEncoded()}") + append("?filterSourceTypeValue=${filterSourceTypeValue.urlEncoded()}") + limit?.let { append("&limit=$it") } filterSourceTypeClass?.let { append("&filterSourceTypeClass=${it.urlEncoded()}") } lastShareID?.let { append("&lastShareID=${it.urlEncoded()}") } } From f598f077c4f6ec804eef4461a64c98854b97b52d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:21:02 +0200 Subject: [PATCH 138/174] wip Signed-off-by: alperozturk96 --- .../com/nextcloud/android/common/ui/share/ShareScreen.kt | 8 +++++++- ui/src/main/res/values/strings.xml | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index e0f046fe..929c0a68 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -101,6 +101,7 @@ private val ITEM_SPACING = 2.dp @Composable private fun ShareScreen( + filename: String, internalLink: String, viewModel: ShareViewModel ) { @@ -142,6 +143,7 @@ private fun ShareScreen( } is ShareScreenState.Empty -> ShareList( + filename = filename, isRefreshing = false, shares = emptyList(), permissionPresets = permissionPresets, @@ -150,6 +152,7 @@ private fun ShareScreen( ) is ShareScreenState.Loaded -> ShareList( + filename = filename, isRefreshing = state.refreshing, shares = state.shares, permissionPresets = permissionPresets, @@ -176,6 +179,7 @@ private fun ShareScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShareList( + filename: String, isRefreshing: Boolean, shares: List, permissionPresets: List, @@ -201,6 +205,7 @@ private fun ShareList( ContentUnavailableView( iconId = R.drawable.ic_person_add, title = stringResource(R.string.share_view_empty_title), + description = stringResource(R.string.share_view_empty_description, filename) ) } } @@ -423,6 +428,7 @@ private fun ShareItem( fun ComposeView.initShareScreen( viewModelStoreOwner: ViewModelStoreOwner, sourceId: String, + filename: String, internalLink: String, credentials: ServerCredentials, colorScheme: ColorScheme @@ -442,7 +448,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(internalLink, viewModel) + ShareScreen(filename, internalLink, viewModel) } ) } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index a74584db..592e8ab0 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -23,7 +23,8 @@ The link can be changed to be easy to remember, but do not set it to something that is easy to guess. Refresh link - Not shared + No shares yet + Use + button to share %s No result Copied From 2085b9952a64b37f0bf6b750815c1ef2e566afc7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:24:38 +0200 Subject: [PATCH 139/174] wip Signed-off-by: alperozturk96 --- .../share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 8b51b58f..5f2e375b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -139,8 +139,9 @@ fun AddOrEditShareBottomSheet( text = share.title(context), style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.MiddleEllipsis, modifier = Modifier.weight(1f) ) From 5716ef09f9fbb39c4c6912c91d360e82c50aa4d5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:25:51 +0200 Subject: [PATCH 140/174] wip Signed-off-by: alperozturk96 --- .../nextcloud/android/common/ui/share/ShareScreen.kt | 10 ++-------- ui/src/main/res/values/strings.xml | 2 -- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 929c0a68..8827694f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -101,7 +101,6 @@ private val ITEM_SPACING = 2.dp @Composable private fun ShareScreen( - filename: String, internalLink: String, viewModel: ShareViewModel ) { @@ -143,7 +142,6 @@ private fun ShareScreen( } is ShareScreenState.Empty -> ShareList( - filename = filename, isRefreshing = false, shares = emptyList(), permissionPresets = permissionPresets, @@ -152,7 +150,6 @@ private fun ShareScreen( ) is ShareScreenState.Loaded -> ShareList( - filename = filename, isRefreshing = state.refreshing, shares = state.shares, permissionPresets = permissionPresets, @@ -179,7 +176,6 @@ private fun ShareScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShareList( - filename: String, isRefreshing: Boolean, shares: List, permissionPresets: List, @@ -204,8 +200,7 @@ private fun ShareList( Box(modifier = Modifier.fillParentMaxSize()) { ContentUnavailableView( iconId = R.drawable.ic_person_add, - title = stringResource(R.string.share_view_empty_title), - description = stringResource(R.string.share_view_empty_description, filename) + title = stringResource(R.string.share_view_empty_title) ) } } @@ -428,7 +423,6 @@ private fun ShareItem( fun ComposeView.initShareScreen( viewModelStoreOwner: ViewModelStoreOwner, sourceId: String, - filename: String, internalLink: String, credentials: ServerCredentials, colorScheme: ColorScheme @@ -448,7 +442,7 @@ fun ComposeView.initShareScreen( MaterialTheme( colorScheme = colorScheme, content = { - ShareScreen(filename, internalLink, viewModel) + ShareScreen(internalLink, viewModel) } ) } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 592e8ab0..f07b3bdf 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -20,11 +20,9 @@ Clear recipient Custom link - The link can be changed to be easy to remember, but do not set it to something that is easy to guess. Refresh link No shares yet - Use + button to share %s No result Copied From dbc52105ad03e118c265bd4c9c13e642abd99163 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:26:57 +0200 Subject: [PATCH 141/174] wip Signed-off-by: alperozturk96 --- ui/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index f07b3bdf..c37293eb 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Clear recipient Custom link + The link can be changed to be easy to remember, but do not set it to something that is easy to guess. Refresh link No shares yet From 977592e2e92c73808cd4f88f7d23b3e77fa67833 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:47:28 +0200 Subject: [PATCH 142/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 6 +--- .../bottomsheet/AddOrEditShareBottomSheet.kt | 32 +++++++++---------- .../ui/share/viewmodel/ShareViewModel.kt | 32 +++++++++++++++---- 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 8827694f..0de17b3c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -164,11 +164,7 @@ private fun ShareScreen( share = activeShareObject, internalLink = internalLink, viewModel = viewModel, - permissionPresets = permissionPresets, - onDismissDraft = { draftShare -> - viewModel.deleteShare(draftShare.id) - viewModel.setActiveShare(null) - } + permissionPresets = permissionPresets ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 5f2e375b..3dd8386a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -36,7 +36,6 @@ import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SheetValue import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -49,6 +48,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard @@ -82,6 +82,7 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.MockShareRepository +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch private const val CUSTOM_SELECTION = "custom" @@ -92,16 +93,12 @@ fun AddOrEditShareBottomSheet( share: Share, internalLink: String, viewModel: ShareViewModel, - permissionPresets: List = emptyList(), - onDismissDraft: (Share) -> Unit = {} + permissionPresets: List = emptyList() ) { val entry by viewModel.editorEntry.collectAsStateWithLifecycle() - var dismissAllowed by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - confirmValueChange = { value -> dismissAllowed || value != SheetValue.Hidden } - ) + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val dismissSheet = { viewModel.dismissActiveShare(share.id) } val categories = remember { ShareCategory.entries.toList() } var selectedCategory by rememberSaveable(share.id) { mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) @@ -113,15 +110,22 @@ fun AddOrEditShareBottomSheet( val hasPropertyErrors = propertyErrors.isNotEmpty() val sendEnabled = !hasPropertyErrors && pendingProperties.isEmpty() + LaunchedEffect(sheetState, share.id) { + snapshotFlow { sheetState.isVisible }.first { it } + snapshotFlow { sheetState.isVisible }.first { !it } + dismissSheet() + } + Column( modifier = Modifier .fillMaxSize() ) { ModalBottomSheet( - onDismissRequest = {}, + onDismissRequest = dismissSheet, sheetState = sheetState, + sheetGesturesEnabled = false, dragHandle = null, - properties = ModalBottomSheetProperties(shouldDismissOnBackPress = false), + properties = ModalBottomSheetProperties(shouldDismissOnClickOutside = false), containerColor = MaterialTheme.colorScheme.surface, ) { Column( @@ -146,13 +150,7 @@ fun AddOrEditShareBottomSheet( ) IconButton(onClick = { - dismissAllowed = true - scope.launch { - sheetState.hide() - if (share.shareState == ShareState.DRAFT) onDismissDraft(share) else viewModel.setActiveShare( - null - ) - } + scope.launch { sheetState.hide() }.invokeOnCompletion { dismissSheet() } }) { Icon( imageVector = Icons.Default.Close, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 442af66f..a5794ae9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -70,6 +70,8 @@ class ShareViewModel( private val _isPreparingLink = MutableStateFlow(false) val isPreparingLink: StateFlow = _isPreparingLink + private val isCreatingDraft = MutableStateFlow(false) + private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare @@ -183,17 +185,23 @@ class ShareViewModel( // region create fun createDraftShare() { + if (!isCreatingDraft.compareAndSet(expect = false, update = true)) return + viewModelScope.launch(Dispatchers.IO) { - _errorMessageId.update { null } + try { + _errorMessageId.update { null } - val result = repository.createDraftShare() - val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } - ?: return@launch + val result = repository.createDraftShare() + val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } + ?: return@launch - updateEditorEntry(ShareEditorEntry.EDIT) - updateActiveShare(draft.toActiveShare()) + updateEditorEntry(ShareEditorEntry.EDIT) + updateActiveShare(draft.toActiveShare()) - applySource(draft.id, sourceId) + applySource(draft.id, sourceId) + } finally { + isCreatingDraft.value = false + } } } // endregion @@ -396,6 +404,16 @@ class ShareViewModel( _errorMessageId.update { value } } + fun dismissActiveShare(id: String) { + val active = _activeShare.value.shareOrNull ?: return + if (active.id != id) return + + if (active.shareState == ShareState.DRAFT) { + deleteShare(id) + } + setActiveShare(null) + } + fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } From 77bfabb483e1dec083d0a5b5ad9b0102608a1631 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 12:51:27 +0200 Subject: [PATCH 143/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 186 ++++++++++-------- .../bottomsheet/AddOrEditShareBottomSheet.kt | 149 +++++++------- .../component/property/SharePropertyView.kt | 42 ++-- .../share/model/ui/ShareItemOverlayState.kt | 15 -- .../common/ui/share/model/ui/ShareOverlay.kt | 45 +++++ .../ui/share/model/ui/ShareScreenState.kt | 7 +- .../ui/share/viewmodel/ShareViewModel.kt | 101 ++++++---- ui/src/main/res/values/strings.xml | 1 + 8 files changed, 307 insertions(+), 239 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 0de17b3c..7507f6d7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -88,8 +88,8 @@ import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPres import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry -import com.nextcloud.android.common.ui.share.model.ui.ShareItemOverlayState import com.nextcloud.android.common.ui.share.model.ui.ShareItemType +import com.nextcloud.android.common.ui.share.model.ui.ShareOverlay import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository @@ -110,6 +110,9 @@ private fun ShareScreen( val permissionPresets by viewModel.permissionPresets.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current + var overlay by rememberSaveable(stateSaver = ShareOverlay.Saver) { + mutableStateOf(ShareOverlay.None) + } LaunchedEffect(errorMessageId) { errorMessageId?.let { @@ -130,31 +133,21 @@ private fun ShareScreen( containerColor = Color.Transparent ) { paddingValues -> when (val state = screenState) { - is ShareScreenState.Loading -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + is ShareScreenState.Loading -> Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - is ShareScreenState.Empty -> ShareList( - isRefreshing = false, - shares = emptyList(), + else -> ShareList( + state = state, permissionPresets = permissionPresets, paddingValues = paddingValues, - viewModel = viewModel - ) - - is ShareScreenState.Loaded -> ShareList( - isRefreshing = state.refreshing, - shares = state.shares, - permissionPresets = permissionPresets, - paddingValues = paddingValues, - viewModel = viewModel + viewModel = viewModel, + onShowOverlay = { overlay = it } ) } } @@ -167,18 +160,66 @@ private fun ShareScreen( permissionPresets = permissionPresets ) } + + ShareItemOverlay( + overlay = overlay, + shares = (screenState as? ShareScreenState.Loaded)?.shares.orEmpty(), + permissionPresets = permissionPresets, + onDismiss = { overlay = ShareOverlay.None }, + viewModel = viewModel + ) +} + +@Composable +private fun ShareItemOverlay( + overlay: ShareOverlay, + shares: List, + permissionPresets: List, + onDismiss: () -> Unit, + viewModel: ShareViewModel +) { + val share = overlay.shareId?.let { id -> shares.firstOrNull { it.id == id } } ?: return + + when (overlay) { + is ShareOverlay.QuickShare -> QuickSharePermissionBottomSheet( + options = PermissionPresetOption.optionsFor(share, permissionPresets), + selectedOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), + onOptionSelected = { option -> + onDismiss() + val presetClass = option.presetClass + if (presetClass != null) { + viewModel.updatePermissionPreset(share.id, presetClass, updateActiveShare = false) + } else { + viewModel.setActiveShare(share, ShareEditorEntry.CUSTOMIZE_PERMISSION) + } + }, + onDismiss = onDismiss + ) + + is ShareOverlay.DeleteConfirmation -> DeleteShareConfirmationDialog( + onConfirm = { + onDismiss() + viewModel.deleteShare(share.id) + }, + onDismiss = onDismiss + ) + + ShareOverlay.None -> Unit + } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShareList( - isRefreshing: Boolean, - shares: List, + state: ShareScreenState, permissionPresets: List, paddingValues: PaddingValues, - viewModel: ShareViewModel + viewModel: ShareViewModel, + onShowOverlay: (ShareOverlay) -> Unit ) { val context = LocalContext.current + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + val shares = (state as? ShareScreenState.Loaded)?.shares.orEmpty() PullToRefreshBox( isRefreshing = isRefreshing, @@ -194,10 +235,7 @@ private fun ShareList( if (shares.isEmpty()) { item { Box(modifier = Modifier.fillParentMaxSize()) { - ContentUnavailableView( - iconId = R.drawable.ic_person_add, - title = stringResource(R.string.share_view_empty_title) - ) + SharePlaceholder(state) } } } @@ -209,73 +247,50 @@ private fun ShareList( share = share, title = share.getHeadline(context, shares), type = ShareItemType.type(index, shares.lastIndex), - permissionPresets = permissionPresets, + presetOptions = PermissionPresetOption.optionsFor(share, permissionPresets), + selectedPresetOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), onSelectShare = { selected -> viewModel.setActiveShare(selected, ShareEditorEntry.EDIT) }, - onCustomizeShare = { selected -> - viewModel.setActiveShare(selected, ShareEditorEntry.CUSTOMIZE_PERMISSION) - }, - onChangePreset = { selected, preset -> - viewModel.updatePermissionPreset(selected.id, preset, updateActiveShare = false) - }, - onDeleteShare = { viewModel.deleteShare(it.id) }, onSendEmail = { selected -> viewModel.setActiveShare(selected, ShareEditorEntry.SEND_EMAIL) - } + }, + onShowOverlay = onShowOverlay ) } } } } +@Composable +private fun SharePlaceholder(state: ShareScreenState) { + if (state is ShareScreenState.Error) { + ContentUnavailableView( + title = stringResource(R.string.share_view_fetch_error_message), + description = stringResource(R.string.share_view_fetch_error_description) + ) + return + } + + ContentUnavailableView( + iconId = R.drawable.ic_person_add, + title = stringResource(R.string.share_view_empty_title) + ) +} + @Composable private fun ShareItem( share: Share, title: String, type: ShareItemType, - permissionPresets: List, + presetOptions: List, + selectedPresetOption: PermissionPresetOption, onSelectShare: (Share) -> Unit, - onCustomizeShare: (Share) -> Unit, - onChangePreset: (Share, String) -> Unit, - onDeleteShare: (Share) -> Unit, - onSendEmail: (Share) -> Unit + onSendEmail: (Share) -> Unit, + onShowOverlay: (ShareOverlay) -> Unit ) { - var overlayState by rememberSaveable(share.id) { mutableStateOf(ShareItemOverlayState.None) } + var contextMenuExpanded by remember { mutableStateOf(false) } val haptics = LocalHapticFeedback.current - val presetOptions = PermissionPresetOption.optionsFor(share, permissionPresets) - val selectedPresetOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets) - - when (overlayState) { - ShareItemOverlayState.QuickShare -> { - QuickSharePermissionBottomSheet( - options = presetOptions, - selectedOption = selectedPresetOption, - onOptionSelected = { option -> - overlayState = ShareItemOverlayState.None - val presetClass = option.presetClass - if (presetClass != null) { - onChangePreset(share, presetClass) - } else { - onCustomizeShare(share) - } - }, - onDismiss = { overlayState = ShareItemOverlayState.None } - ) - } - - ShareItemOverlayState.DeleteConfirmation -> { - DeleteShareConfirmationDialog( - onConfirm = { - overlayState = ShareItemOverlayState.None - onDeleteShare(share) - }, - onDismiss = { overlayState = ShareItemOverlayState.None } - ) - } - - else -> Unit - } ListItem( modifier = Modifier @@ -285,7 +300,7 @@ private fun ShareItem( onClick = { onSelectShare(share) }, onLongClick = { haptics.performHapticFeedback(HapticFeedbackType.LongPress) - overlayState = ShareItemOverlayState.ContextMenu + contextMenuExpanded = true } ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), @@ -318,7 +333,7 @@ private fun ShareItem( modifier = Modifier .offset(x = -chipHorizontalPadding) .clip(RoundedCornerShape(percent = 50)) - .clickable { overlayState = ShareItemOverlayState.QuickShare } + .clickable { onShowOverlay(ShareOverlay.QuickShare(share.id)) } .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.08f)) .padding(horizontal = chipHorizontalPadding, vertical = 2.dp), verticalAlignment = Alignment.CenterVertically @@ -355,13 +370,13 @@ private fun ShareItem( }, trailingContent = { Box { - IconButton(onClick = { overlayState = ShareItemOverlayState.ContextMenu }) { + IconButton(onClick = { contextMenuExpanded = true }) { Icon(Icons.Default.MoreVert, contentDescription = "More options") } DropdownMenu( - expanded = overlayState == ShareItemOverlayState.ContextMenu, - onDismissRequest = { overlayState = ShareItemOverlayState.None } + expanded = contextMenuExpanded, + onDismissRequest = { contextMenuExpanded = false } ) { DropdownMenuItem( leadingIcon = { @@ -372,7 +387,7 @@ private fun ShareItem( }, text = { Text(stringResource(R.string.share_view_list_item_edit)) }, onClick = { - overlayState = ShareItemOverlayState.None + contextMenuExpanded = false onSelectShare(share) } ) @@ -386,8 +401,8 @@ private fun ShareItem( }, text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, onClick = { + contextMenuExpanded = false onSendEmail(share) - overlayState = ShareItemOverlayState.None } ) HorizontalDivider() @@ -406,7 +421,8 @@ private fun ShareItem( ) }, onClick = { - overlayState = ShareItemOverlayState.DeleteConfirmation + contextMenuExpanded = false + onShowOverlay(ShareOverlay.DeleteConfirmation(share.id)) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 3dd8386a..73a4ebb7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -11,7 +11,6 @@ package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -67,6 +66,7 @@ import com.nextcloud.android.common.ui.share.component.CustomLink import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView +import com.nextcloud.android.common.ui.share.component.property.propertyErrorMessage import com.nextcloud.android.common.ui.share.model.api.permission.Permission import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.property.PropertyString @@ -116,92 +116,89 @@ fun AddOrEditShareBottomSheet( dismissSheet() } - Column( - modifier = Modifier - .fillMaxSize() + ModalBottomSheet( + onDismissRequest = dismissSheet, + sheetState = sheetState, + sheetGesturesEnabled = false, + dragHandle = null, + properties = ModalBottomSheetProperties(shouldDismissOnClickOutside = false), + containerColor = MaterialTheme.colorScheme.surface, ) { - ModalBottomSheet( - onDismissRequest = dismissSheet, - sheetState = sheetState, - sheetGesturesEnabled = false, - dragHandle = null, - properties = ModalBottomSheetProperties(shouldDismissOnClickOutside = false), - containerColor = MaterialTheme.colorScheme.surface, + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) ) { - Column( + Row( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .padding(start = 16.dp, end = 4.dp, top = 12.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 4.dp, top = 12.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = share.title(context), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.MiddleEllipsis, - modifier = Modifier.weight(1f) + Text( + text = share.title(context), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier.weight(1f) + ) + + IconButton(onClick = { + scope.launch { sheetState.hide() }.invokeOnCompletion { dismissSheet() } + }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "close bottom sheet", + tint = MaterialTheme.colorScheme.onSurfaceVariant ) + } + } - IconButton(onClick = { - scope.launch { sheetState.hide() }.invokeOnCompletion { dismissSheet() } - }) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "close bottom sheet", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + ShareCategorySelector( + share = share, + categories = categories, + selectedCategory = selectedCategory, + onCategorySelected = { category -> + selectedCategory = category + viewModel.selectCategory(category, share) } + ) - ShareCategorySelector( - share = share, - categories = categories, - selectedCategory = selectedCategory, - onCategorySelected = { category -> - selectedCategory = category - viewModel.selectCategory(category, share) - } - ) + if (selectedCategory == ShareCategory.Invited) { + SelectRecipientField(share, viewModel) + } - if (selectedCategory == ShareCategory.Invited) { - SelectRecipientField(share, viewModel) - } + PermissionsView( + share = share, + entry = entry, + permissionPresets = permissionPresets, + viewModel = viewModel + ) - PermissionsView( - share = share, - entry = entry, - permissionPresets = permissionPresets, - viewModel = viewModel - ) + BasicSettingsSection( + share = share, + propertyErrors = propertyErrors, + viewModel = viewModel + ) - BasicSettingsSection( - share = share, - viewModel = viewModel - ) + AdvancedSettingsSection( + share = share, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + propertyErrors = propertyErrors, + viewModel = viewModel + ) - AdvancedSettingsSection( + if (share.canSend) { + ActionButtons( share = share, - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings }, + internalLink = internalLink, + category = selectedCategory, + sendEnabled = sendEnabled, viewModel = viewModel ) - - if (share.canSend) { - ActionButtons( - share = share, - internalLink = internalLink, - category = selectedCategory, - sendEnabled = sendEnabled, - viewModel = viewModel - ) - } } } } @@ -331,6 +328,7 @@ private fun PermissionPresetDropdown( @Composable private fun BasicSettingsSection( share: Share, + propertyErrors: Map, viewModel: ShareViewModel ) { if (!share.isBasicSectionAvailable) { @@ -345,9 +343,9 @@ private fun BasicSettingsSection( share.basicProperties.forEach { property -> key(property.clazz) { SharePropertyView( - shareId = share.id, property = property, - viewModel = viewModel + errorMessage = propertyErrorMessage(propertyErrors, property.clazz), + onValueChange = { value -> viewModel.updateProperty(share.id, property.clazz, value) } ) } } @@ -359,6 +357,7 @@ private fun AdvancedSettingsSection( share: Share, isExpanded: Boolean, onToggle: () -> Unit, + propertyErrors: Map, viewModel: ShareViewModel ) { if (!share.isAdvancedSectionAvailable) { @@ -373,9 +372,9 @@ private fun AdvancedSettingsSection( share.advancedProperties.forEach { property -> key(property.clazz) { SharePropertyView( - shareId = share.id, property = property, - viewModel = viewModel + errorMessage = propertyErrorMessage(propertyErrors, property.clazz), + onValueChange = { value -> viewModel.updateProperty(share.id, property.clazz, value) } ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 6a1a3173..689eb4f6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -23,9 +23,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.datepicker.ShareDatePicker import com.nextcloud.android.common.ui.share.model.api.property.Property @@ -40,21 +38,23 @@ private const val MULTILINE_MIN_LINES = 1 private const val MULTILINE_MAX_LINES = 3 @Composable -fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewModel) { - val propertyErrors by viewModel.propertyErrors.collectAsStateWithLifecycle() +fun propertyErrorMessage(propertyErrors: Map, clazz: String): String? { val fallbackError = stringResource(R.string.share_view_update_error_message) - val errorMessage = if (propertyErrors.containsKey(property.clazz)) { - propertyErrors[property.clazz] ?: fallbackError - } else { - null - } + if (!propertyErrors.containsKey(clazz)) return null + return propertyErrors[clazz] ?: fallbackError +} +@Composable +fun SharePropertyView( + property: Property, + errorMessage: String?, + onValueChange: (String?) -> Unit +) { Column { SharePropertyField( - shareId = shareId, property = property, isError = errorMessage != null, - viewModel = viewModel + onValueChange = onValueChange ) SharePropertyMessage(errorMessage = errorMessage, hint = property.hint) @@ -63,10 +63,9 @@ fun SharePropertyView(shareId: String, property: Property, viewModel: ShareViewM @Composable private fun SharePropertyField( - shareId: String, property: Property, isError: Boolean, - viewModel: ShareViewModel + onValueChange: (String?) -> Unit ) { when (property) { is PropertyBoolean -> { @@ -77,46 +76,43 @@ private fun SharePropertyField( checked = checkedValue, onCheckedChange = { isChecked -> checkedValue = isChecked - viewModel.updateProperty(shareId, property.clazz, isChecked.toString()) + onValueChange(isChecked.toString()) } ) } is PropertyString -> ShareTextPropertyField( - shareId = shareId, property = property, isError = isError, - viewModel = viewModel + onValueChange = onValueChange ) is PropertyPassword -> ShareTextPropertyField( - shareId = shareId, property = property, isError = isError, - viewModel = viewModel, + onValueChange = onValueChange, visualTransformation = PasswordVisualTransformation() ) is PropertyDate -> ShareDatePicker( property = property, isError = isError, - onDateSelected = { dateValue -> viewModel.updateProperty(shareId, property.clazz, dateValue) } + onDateSelected = onValueChange ) is PropertyEnum -> SharePropertyEnumField( property = property, isError = isError, - onValueSelected = { value -> viewModel.updateProperty(shareId, property.clazz, value) } + onValueSelected = onValueChange ) } } @Composable private fun ShareTextPropertyField( - shareId: String, property: Property, isError: Boolean, - viewModel: ShareViewModel, + onValueChange: (String?) -> Unit, visualTransformation: VisualTransformation = VisualTransformation.None ) { val stringProperty = property as? PropertyString @@ -144,7 +140,7 @@ private fun ShareTextPropertyField( .onFocusChanged { focusState -> if (wasFocused && !focusState.isFocused && value != committedValue) { committedValue = value - viewModel.updateProperty(shareId, clazz, value) + onValueChange(value) } wasFocused = focusState.isFocused }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt deleted file mode 100644 index b5c73b97..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemOverlayState.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.android.common.ui.share.model.ui - -enum class ShareItemOverlayState { - None, - ContextMenu, - QuickShare, - DeleteConfirmation -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt new file mode 100644 index 00000000..92242a11 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver + +sealed class ShareOverlay { + abstract val shareId: String? + + data object None : ShareOverlay() { + override val shareId: String? = null + } + + data class QuickShare(override val shareId: String) : ShareOverlay() + data class DeleteConfirmation(override val shareId: String) : ShareOverlay() + + companion object { + private const val QUICK_SHARE = "quick_share" + private const val DELETE_CONFIRMATION = "delete_confirmation" + + val Saver: Saver = listSaver( + save = { overlay -> + when (overlay) { + None -> emptyList() + is QuickShare -> listOf(QUICK_SHARE, overlay.shareId) + is DeleteConfirmation -> listOf(DELETE_CONFIRMATION, overlay.shareId) + } + }, + restore = { saved -> + val shareId = saved.getOrNull(1) + when (saved.firstOrNull()) { + QUICK_SHARE -> shareId?.let(::QuickShare) + DELETE_CONFIRMATION -> shareId?.let(::DeleteConfirmation) + else -> None + } ?: None + } + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index 81fb4038..fdaa7111 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -11,9 +11,10 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState sealed class ShareScreenState { - data object Empty: ShareScreenState() - data object Loading: ShareScreenState() - data class Loaded(val shares: List, val refreshing: Boolean): ShareScreenState() + data object Loading : ShareScreenState() + data object Empty : ShareScreenState() + data object Error : ShareScreenState() + data class Loaded(val shares: List) : ShareScreenState() } fun List.activeOnly(): List = filter { it.shareState == ShareState.ACTIVE } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index a5794ae9..7225726d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -31,13 +31,15 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState import com.nextcloud.android.common.ui.share.model.ui.activeOnly import com.nextcloud.android.common.ui.share.repository.ShareRepository -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -45,7 +47,6 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.UUID import kotlin.time.Duration.Companion.milliseconds @@ -65,30 +66,33 @@ class ShareViewModel( private val savedState = ShareSavedState(savedStateHandle) private val _state = MutableStateFlow(ShareScreenState.Loading) - val state: StateFlow = _state + val state: StateFlow = _state.asStateFlow() + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow = _isRefreshing.asStateFlow() private val _isPreparingLink = MutableStateFlow(false) - val isPreparingLink: StateFlow = _isPreparingLink + val isPreparingLink: StateFlow = _isPreparingLink.asStateFlow() private val isCreatingDraft = MutableStateFlow(false) private val _activeShare = MutableStateFlow(ActiveShareState.None) - val activeShare: StateFlow = _activeShare + val activeShare: StateFlow = _activeShare.asStateFlow() private val _editorEntry = MutableStateFlow(savedState.editorEntry) - val editorEntry: StateFlow = _editorEntry + val editorEntry: StateFlow = _editorEntry.asStateFlow() private val _permissionPresets = MutableStateFlow>(emptyList()) - val permissionPresets: StateFlow> = _permissionPresets + val permissionPresets: StateFlow> = _permissionPresets.asStateFlow() private val _errorMessageId = MutableStateFlow(null) - val errorMessageId: StateFlow = _errorMessageId + val errorMessageId: StateFlow = _errorMessageId.asStateFlow() private val _propertyErrors = MutableStateFlow>(emptyMap()) - val propertyErrors: StateFlow> = _propertyErrors + val propertyErrors: StateFlow> = _propertyErrors.asStateFlow() private val _pendingProperties = MutableStateFlow>(emptySet()) - val pendingProperties: StateFlow> = _pendingProperties + val pendingProperties: StateFlow> = _pendingProperties.asStateFlow() private val currentShares: List get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() @@ -111,8 +115,15 @@ class ShareViewModel( loadInitialData() } + override fun onCleared() { + super.onCleared() + + val draft = _activeShare.value.shareOrNull?.takeIf { it.shareState == ShareState.DRAFT } ?: return + CoroutineScope(SupervisorJob()).launch { repository.deleteShare(draft.id) } + } + private fun loadInitialData() { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { launch { loadSharingCapabilities() } launch { restoreActiveShare() } loadShares() @@ -155,19 +166,14 @@ class ShareViewModel( // region shares list fun refreshShares() { - val state = _state.value as? ShareScreenState.Loaded ?: return - if (state.refreshing) return + if (!_isRefreshing.compareAndSet(expect = false, update = true)) return - viewModelScope.launch(Dispatchers.IO) { - setRefreshing(true) - loadShares() - setRefreshing(false) - } - } - - private fun setRefreshing(refreshing: Boolean) { - _state.update { - if (it is ShareScreenState.Loaded) it.copy(refreshing = refreshing) else it + viewModelScope.launch { + try { + loadShares() + } finally { + _isRefreshing.value = false + } } } @@ -177,8 +183,26 @@ class ShareViewModel( filterSourceTypeValue = sourceId, filterSourceTypeClass = Source.NODE_SOURCE_CLASS ) - result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } }?.let { fetched -> - publishShares(fetched.activeOnly()) + val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } + + if (fetched == null) { + _state.update { it as? ShareScreenState.Loaded ?: ShareScreenState.Error } + return + } + + publishShares(fetched.activeOnly()) + deleteAbandonedDrafts(fetched) + } + + private fun deleteAbandonedDrafts(fetched: List) { + if (isCreatingDraft.value) return + + val openShareIds = setOfNotNull(_activeShare.value.shareOrNull?.id, savedState.activeShareId) + val abandoned = fetched.filter { it.shareState == ShareState.DRAFT && it.id !in openShareIds } + if (abandoned.isEmpty()) return + + viewModelScope.launch { + abandoned.forEach { repository.deleteShare(it.id) } } } // endregion @@ -187,7 +211,7 @@ class ShareViewModel( fun createDraftShare() { if (!isCreatingDraft.compareAndSet(expect = false, update = true)) return - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { try { _errorMessageId.update { null } @@ -208,7 +232,7 @@ class ShareViewModel( // region state fun updateState(id: String, shareState: ShareState) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val updated = applyState(id, shareState) ?: return@launch if (shareState == ShareState.ACTIVE) { @@ -223,7 +247,7 @@ class ShareViewModel( fun prepareLinkForCopy(id: String) { if (_isPreparingLink.value) return - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { _isPreparingLink.update { true } val updated = applyState(id, ShareState.ACTIVE) @@ -259,7 +283,7 @@ class ShareViewModel( // region recipients fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch @@ -295,7 +319,7 @@ class ShareViewModel( } fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val result = repository.removeShareRecipient(id, clazz, value, instance) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch @@ -306,7 +330,7 @@ class ShareViewModel( fun updateRecipientSecret(shareId: String, recipient: Recipient, secret: String) { secretUpdateJob?.cancel() - secretUpdateJob = viewModelScope.launch(Dispatchers.IO) { + secretUpdateJob = viewModelScope.launch { val request = UpdateShareRecipientSecretRequest( clazz = recipient.clazz, value = recipient.value, @@ -321,10 +345,10 @@ class ShareViewModel( } } - suspend fun generateSecret(): String? = withContext(Dispatchers.IO) { + suspend fun generateSecret(): String? { _errorMessageId.update { null } val result = repository.generateSecret() - result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + return result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } } // endregion @@ -335,7 +359,7 @@ class ShareViewModel( if (value.isNullOrEmpty()) { _propertyErrors.update { it - clazz } } - propertyUpdateJobs[clazz] = viewModelScope.launch(Dispatchers.IO) { + propertyUpdateJobs[clazz] = viewModelScope.launch { when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { is NetworkResult.Success -> { _propertyErrors.update { it - clazz } @@ -362,16 +386,17 @@ class ShareViewModel( // region permissions fun updatePermission(id: String, clazz: String, enabled: Boolean) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch + refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } } fun updatePermissionPreset(id: String, presetClass: String, updateActiveShare: Boolean) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val result = repository.updateSharePermissionPreset(id, UpdateSharePermissionPresetRequest(presetClass)) val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch @@ -385,7 +410,7 @@ class ShareViewModel( // region delete fun deleteShare(id: String) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch { val result = repository.deleteShare(id) result.dataOrElse { _errorMessageId.update { R.string.share_view_delete_error_message } } ?: return@launch @@ -458,7 +483,7 @@ class ShareViewModel( private fun publishShares(shares: List) { _state.update { - if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares, false) + if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares) } } // endregion diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index c37293eb..7af7bc20 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ This share will be permanently removed. Failed to fetch shares. + Pull down to try again. Failed to delete share. Failed to create share. Failed to update share. From 92143c1baedff521dd4675d67695aa74399ab48e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 1 Sep 2026 09:58:19 +0200 Subject: [PATCH 144/174] wip Signed-off-by: alperozturk96 --- .../ui/component/ContentUnavailableView.kt | 6 ++--- .../android/common/ui/share/ShareScreen.kt | 2 +- .../ui/share/model/api/EnumExtensions.kt | 12 +++++++++ .../common/ui/share/model/api/share/Share.kt | 4 +++ .../share/model/api/state/ShareUserStatus.kt | 23 ++++++++++++++++ .../ui/share/model/ui/ShareScreenState.kt | 3 --- .../share/repository/MockShareRepository.kt | 10 ++++--- .../share/repository/ShareRemoteRepository.kt | 4 +++ .../ui/share/repository/ShareRepository.kt | 2 ++ .../ui/share/viewmodel/ShareViewModel.kt | 26 +++++++++---------- .../res/drawable/ic_person_add_filled.xml | 20 ++++++++++++++ ui/src/main/res/values/strings.xml | 2 +- 12 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt create mode 100644 ui/src/main/res/drawable/ic_person_add_filled.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt index 3d28ffc2..7132c2b9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt @@ -41,8 +41,6 @@ fun ContentUnavailableView(title: String, description: String? = null, iconId: I ) } - Spacer(modifier = Modifier.height(16.dp)) - Text( text = title, style = MaterialTheme.typography.headlineMedium, @@ -57,8 +55,8 @@ fun ContentUnavailableView(title: String, description: String? = null, iconId: I style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center ) - } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(32.dp)) + } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 7507f6d7..e5a31cb3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -273,7 +273,7 @@ private fun SharePlaceholder(state: ShareScreenState) { } ContentUnavailableView( - iconId = R.drawable.ic_person_add, + iconId = R.drawable.ic_person_add_filled, title = stringResource(R.string.share_view_empty_title) ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt new file mode 100644 index 00000000..109c69d6 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api + +import kotlinx.serialization.serializer + +inline fun > T.apiValue(): String = serializer().descriptor.getElementName(ordinal) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index e77041ea..ae89d385 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -17,6 +17,7 @@ import com.nextcloud.android.common.ui.share.model.api.property.Property import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState +import com.nextcloud.android.common.ui.share.model.api.state.ShareUserStatus import com.nextcloud.android.common.ui.share.model.api.user.User import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory @@ -35,6 +36,9 @@ data class Share( @SerialName("state") val shareState: ShareState, + @SerialName("user_status") + val userStatus: ShareUserStatus? = null, + val sources: List, val recipients: List, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt new file mode 100644 index 00000000..b8bf6044 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.state + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +enum class ShareUserStatus { + @SerialName("pending") + PENDING, + + @SerialName("accepted") + ACCEPTED, + + @SerialName("rejected") + REJECTED +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index fdaa7111..1a1fe517 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -8,7 +8,6 @@ package com.nextcloud.android.common.ui.share.model.ui import com.nextcloud.android.common.ui.share.model.api.share.Share -import com.nextcloud.android.common.ui.share.model.api.state.ShareState sealed class ShareScreenState { data object Loading : ShareScreenState() @@ -16,5 +15,3 @@ sealed class ShareScreenState { data object Error : ShareScreenState() data class Loaded(val shares: List) : ShareScreenState() } - -fun List.activeOnly(): List = filter { it.shareState == ShareState.ACTIVE } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 381753f9..1fb3fc39 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -327,13 +327,15 @@ class MockShareRepository : ShareRepository { filterSourceTypeValue: String, limit: Int?, filterSourceTypeClass: String?, + filterState: ShareState?, lastShareID: String? ): NetworkResult> { val result = mockShares.filter { share -> - share.sources.any { - it.value == filterSourceTypeValue && - (filterSourceTypeClass == null || it.clazz == filterSourceTypeClass) - } + (filterState == null || share.shareState == filterState) && + share.sources.any { + it.value == filterSourceTypeValue && + (filterSourceTypeClass == null || it.clazz == filterSourceTypeClass) + } } return NetworkResult.Success(result) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 36384705..a525ddd6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -23,7 +23,9 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermis import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest +import com.nextcloud.android.common.ui.share.model.api.apiValue import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.state.ShareState import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonObject @@ -95,12 +97,14 @@ class ShareRemoteRepository( filterSourceTypeValue: String, limit: Int?, filterSourceTypeClass: String?, + filterState: ShareState?, lastShareID: String? ): NetworkResult> { val queryParams = buildString { append("?filterSourceTypeValue=${filterSourceTypeValue.urlEncoded()}") limit?.let { append("&limit=$it") } filterSourceTypeClass?.let { append("&filterSourceTypeClass=${it.urlEncoded()}") } + filterState?.let { append("&filterState=${it.apiValue().urlEncoded()}") } lastShareID?.let { append("&lastShareID=${it.urlEncoded()}") } } return client.executeRequest( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 929707a0..f378f9d1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -18,6 +18,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareProper import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.state.ShareState interface ShareRepository { @@ -41,6 +42,7 @@ interface ShareRepository { filterSourceTypeValue: String, limit: Int? = null, filterSourceTypeClass: String? = null, + filterState: ShareState? = null, lastShareID: String? = null ): NetworkResult> diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 7225726d..f5c90430 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -29,7 +29,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState -import com.nextcloud.android.common.ui.share.model.ui.activeOnly import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -179,10 +178,7 @@ class ShareViewModel( private suspend fun loadShares() { _errorMessageId.update { null } - val result = repository.fetchShares( - filterSourceTypeValue = sourceId, - filterSourceTypeClass = Source.NODE_SOURCE_CLASS - ) + val result = fetchShares(ShareState.ACTIVE) val fetched = result.dataOrElse { _errorMessageId.update { R.string.share_view_fetch_error_message } } if (fetched == null) { @@ -190,20 +186,22 @@ class ShareViewModel( return } - publishShares(fetched.activeOnly()) - deleteAbandonedDrafts(fetched) + publishShares(fetched) + viewModelScope.launch { deleteAbandonedDrafts() } } - private fun deleteAbandonedDrafts(fetched: List) { + private suspend fun fetchShares(filterState: ShareState): NetworkResult> = repository.fetchShares( + filterSourceTypeValue = sourceId, + filterSourceTypeClass = Source.NODE_SOURCE_CLASS, + filterState = filterState + ) + + private suspend fun deleteAbandonedDrafts() { if (isCreatingDraft.value) return + val drafts = (fetchShares(ShareState.DRAFT) as? NetworkResult.Success)?.data ?: return val openShareIds = setOfNotNull(_activeShare.value.shareOrNull?.id, savedState.activeShareId) - val abandoned = fetched.filter { it.shareState == ShareState.DRAFT && it.id !in openShareIds } - if (abandoned.isEmpty()) return - - viewModelScope.launch { - abandoned.forEach { repository.deleteShare(it.id) } - } + drafts.filterNot { it.id in openShareIds }.forEach { repository.deleteShare(it.id) } } // endregion diff --git a/ui/src/main/res/drawable/ic_person_add_filled.xml b/ui/src/main/res/drawable/ic_person_add_filled.xml new file mode 100644 index 00000000..6c5f6c80 --- /dev/null +++ b/ui/src/main/res/drawable/ic_person_add_filled.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 7af7bc20..9918d2b2 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -23,7 +23,7 @@ The link can be changed to be easy to remember, but do not set it to something that is easy to guess. Refresh link - No shares yet + Not shared yet No result Copied From bd13d90c2ac7a88a25db7f43c47526dd1ab56114 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 09:15:59 +0200 Subject: [PATCH 145/174] wip Signed-off-by: alperozturk96 --- .../share/model/api/capabilities/SharingCapabilities.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 27c52b3b..8f980241 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -19,4 +19,11 @@ data class SharingCapabilities( val sourceTypes: List = emptyList(), @SerialName("permission_presets") val permissionPresets: List = emptyList(), -) +) { + val isUnifiedShareEnabled: Boolean + get() = UNIFIED_SHARE_API_VERSION in apiVersions + + companion object { + const val UNIFIED_SHARE_API_VERSION = "v1" + } +} From cb9f6e698812d74b73429fe98400af6bea09c595 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 09:24:43 +0200 Subject: [PATCH 146/174] wip Signed-off-by: alperozturk96 --- ui/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 9918d2b2..6c7d257b 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -44,7 +44,7 @@ Failed to create share. Failed to update share. - Change permission + Participants Participants Can… From 74fced0b4c6c3e807c76191753d0cd75280a2d0e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 09:31:45 +0200 Subject: [PATCH 147/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 2 +- .../common/ui/share/model/ui/ShareItemType.kt | 41 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index e5a31cb3..c684686a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -111,7 +111,7 @@ private fun ShareScreen( val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current var overlay by rememberSaveable(stateSaver = ShareOverlay.Saver) { - mutableStateOf(ShareOverlay.None) + mutableStateOf(ShareOverlay.None) } LaunchedEffect(errorMessageId) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt index f3c08a5f..554ed7fb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt @@ -11,25 +11,40 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp +private val OUTER_CORNER_RADIUS = 12.dp +private val INNER_CORNER_RADIUS = 4.dp + enum class ShareItemType { - Top, Mid, Bottom; + Single, + 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) - } + fun getShape(): RoundedCornerShape = when (this) { + Single -> RoundedCornerShape(OUTER_CORNER_RADIUS) + Top -> RoundedCornerShape( + OUTER_CORNER_RADIUS, + OUTER_CORNER_RADIUS, + INNER_CORNER_RADIUS, + INNER_CORNER_RADIUS + ) + + Mid -> RoundedCornerShape(INNER_CORNER_RADIUS) + Bottom -> RoundedCornerShape( + INNER_CORNER_RADIUS, + INNER_CORNER_RADIUS, + OUTER_CORNER_RADIUS, + OUTER_CORNER_RADIUS + ) } companion object { - fun type(index: Int, lastIndex: Int): ShareItemType { - return when (index) { - 0 -> Top - lastIndex -> Bottom - else -> Mid - } + fun type(index: Int, lastIndex: Int): ShareItemType = when { + lastIndex == 0 -> Single + index == 0 -> Top + index == lastIndex -> Bottom + else -> Mid } } } From fec8654b17c33e7b469e7733d6e1640e2dfd454c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 10:23:18 +0200 Subject: [PATCH 148/174] wip Signed-off-by: alperozturk96 --- .../component/CollapsibleShareSection.kt | 4 +- .../common/ui/share/component/CustomLink.kt | 2 +- .../share/component/SelectRecipientField.kt | 8 +- .../common/ui/share/component/ShareSwitch.kt | 2 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 153 ++++++++++-------- 5 files changed, 95 insertions(+), 74 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt index 91e640c4..4214e7f6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +private val SECTION_VERTICAL_PADDING = 16.dp + @Composable fun CollapsibleShareSection( label: String, @@ -32,7 +34,7 @@ fun CollapsibleShareSection( onToggle: () -> Unit, content: @Composable () -> Unit ) { - Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Column(modifier = Modifier.fillMaxWidth().padding(vertical = SECTION_VERTICAL_PADDING)) { Row( modifier = Modifier .fillMaxWidth() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index e747b63e..2f28967b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -49,7 +49,7 @@ fun CustomLink( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding(vertical = 8.dp) ) { Text( text = stringResource(R.string.share_view_custom_link_title), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 05521c60..2f21eb41 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -47,7 +47,7 @@ import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share -private val FIELD_PADDING = 12.dp +private val FIELD_VERTICAL_PADDING = 12.dp private val CHIP_SPACING = 8.dp private val CHIP_AVATAR_SIZE = 20.dp private val CHIP_CLOSE_ICON_SIZE = 18.dp @@ -83,9 +83,7 @@ private fun RecipientChips(recipients: List, onRemove: (Recipient) -> if (recipients.isEmpty()) return FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = FIELD_PADDING), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(CHIP_SPACING) ) { recipients.forEach { recipient -> @@ -119,7 +117,7 @@ private fun RecipientSearch(share: Share, selected: List, viewModel: ExposedDropdownMenuBox( modifier = Modifier .fillMaxWidth() - .padding(FIELD_PADDING), + .padding(vertical = FIELD_VERTICAL_PADDING), expanded = expanded && query.isNotBlank(), onExpandedChange = { expanded = it } ) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index ebe0744d..8af52f43 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -31,7 +31,7 @@ fun ShareSwitch( modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) - .padding(vertical = 4.dp, horizontal = 16.dp), + .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 73a4ebb7..a87831cc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -87,6 +87,12 @@ import kotlinx.coroutines.launch private const val CUSTOM_SELECTION = "custom" +private val SHEET_CONTENT_PADDING = 16.dp +private val CLOSE_BUTTON_END_PADDING = 4.dp +private val CATEGORY_VERTICAL_PADDING = 8.dp +private val PRESET_VERTICAL_PADDING = 12.dp +private val ACTION_BUTTONS_VERTICAL_PADDING = 16.dp + @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddOrEditShareBottomSheet( @@ -129,81 +135,98 @@ fun AddOrEditShareBottomSheet( .fillMaxWidth() .verticalScroll(rememberScrollState()) ) { - Row( + ShareSheetHeader( + title = share.title(context), + onClose = { scope.launch { sheetState.hide() }.invokeOnCompletion { dismissSheet() } } + ) + + Column( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 4.dp, top = 12.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = SHEET_CONTENT_PADDING) ) { - Text( - text = share.title(context), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.MiddleEllipsis, - modifier = Modifier.weight(1f) + ShareCategorySelector( + share = share, + categories = categories, + selectedCategory = selectedCategory, + onCategorySelected = { category -> + selectedCategory = category + viewModel.selectCategory(category, share) + } ) - IconButton(onClick = { - scope.launch { sheetState.hide() }.invokeOnCompletion { dismissSheet() } - }) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "close bottom sheet", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (selectedCategory == ShareCategory.Invited) { + SelectRecipientField(share, viewModel) } - } - - ShareCategorySelector( - share = share, - categories = categories, - selectedCategory = selectedCategory, - onCategorySelected = { category -> - selectedCategory = category - viewModel.selectCategory(category, share) - } - ) - if (selectedCategory == ShareCategory.Invited) { - SelectRecipientField(share, viewModel) - } - - PermissionsView( - share = share, - entry = entry, - permissionPresets = permissionPresets, - viewModel = viewModel - ) - - BasicSettingsSection( - share = share, - propertyErrors = propertyErrors, - viewModel = viewModel - ) + PermissionsView( + share = share, + entry = entry, + permissionPresets = permissionPresets, + viewModel = viewModel + ) - AdvancedSettingsSection( - share = share, - isExpanded = showAdvancedSettings, - onToggle = { showAdvancedSettings = !showAdvancedSettings }, - propertyErrors = propertyErrors, - viewModel = viewModel - ) + BasicSettingsSection( + share = share, + propertyErrors = propertyErrors, + viewModel = viewModel + ) - if (share.canSend) { - ActionButtons( + AdvancedSettingsSection( share = share, - internalLink = internalLink, - category = selectedCategory, - sendEnabled = sendEnabled, + isExpanded = showAdvancedSettings, + onToggle = { showAdvancedSettings = !showAdvancedSettings }, + propertyErrors = propertyErrors, viewModel = viewModel ) + + if (share.canSend) { + ActionButtons( + share = share, + internalLink = internalLink, + category = selectedCategory, + sendEnabled = sendEnabled, + viewModel = viewModel + ) + } } } } } +@Composable +private fun ShareSheetHeader(title: String, onClose: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = SHEET_CONTENT_PADDING, + end = CLOSE_BUTTON_END_PADDING, + top = PRESET_VERTICAL_PADDING, + bottom = CATEGORY_VERTICAL_PADDING + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier.weight(1f) + ) + + IconButton(onClick = onClose) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "close bottom sheet", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShareCategorySelector( @@ -218,7 +241,7 @@ private fun ShareCategorySelector( SingleChoiceSegmentedButtonRow( modifier = Modifier .fillMaxWidth() - .padding(8.dp) + .padding(vertical = CATEGORY_VERTICAL_PADDING) ) { categories.forEachIndexed { index, category -> SegmentedButton( @@ -293,7 +316,9 @@ private fun PermissionPresetDropdown( var expanded by remember { mutableStateOf(false) } ExposedDropdownMenuBox( - modifier = Modifier.padding(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = PRESET_VERTICAL_PADDING), expanded = expanded, onExpandedChange = { expanded = it } ) { @@ -335,11 +360,7 @@ private fun BasicSettingsSection( return } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { + Column(modifier = Modifier.fillMaxWidth()) { share.basicProperties.forEach { property -> key(property.clazz) { SharePropertyView( @@ -413,7 +434,7 @@ private fun ActionButtons( Row( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(vertical = ACTION_BUTTONS_VERTICAL_PADDING), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { Button( From 78445f7557e5122a8ee392118c1df06615cd018e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 10:40:32 +0200 Subject: [PATCH 149/174] wip Signed-off-by: alperozturk96 --- .../common/ui/network/model/NetworkResult.kt | 5 +++ .../ui/share/viewmodel/ShareViewModel.kt | 32 +++++++++++++++++-- ui/src/main/res/values/strings.xml | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt index ef05856b..839ea5aa 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt @@ -7,6 +7,8 @@ package com.nextcloud.android.common.ui.network.model +private const val HTTP_TOO_MANY_REQUESTS = 429 + sealed class NetworkResult { data class Success(val data: T) : NetworkResult() @@ -21,6 +23,9 @@ sealed class NetworkResult { } } +val NetworkResult<*>.isRateLimited: Boolean + get() = this is NetworkResult.ServerError && response.ocs.meta.statusCode == HTTP_TOO_MANY_REQUESTS + inline fun NetworkResult.dataOrElse(onError: () -> Unit): T? = when (this) { is NetworkResult.Success -> data diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index f5c90430..363c45a4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.dataOrElse +import com.nextcloud.android.common.ui.network.model.isRateLimited import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest @@ -48,6 +49,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.UUID import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeMark +import kotlin.time.TimeSource class ShareViewModel( private val repository: ShareRepository, @@ -60,6 +64,8 @@ class ShareViewModel( private const val SEARCH_SUBSCRIPTION_TIMEOUT = 5_000L private const val RECIPIENT_SEARCH_LIMIT = 10 private const val RECIPIENT_SEARCH_OFFSET = 0 + + private val DRAFT_CREATION_INTERVAL = 5.seconds } private val savedState = ShareSavedState(savedStateHandle) @@ -110,6 +116,8 @@ class ShareViewModel( private var secretUpdateJob: Job? = null + private var lastDraftCreation: TimeMark? = null + init { loadInitialData() } @@ -207,15 +215,20 @@ class ShareViewModel( // region create fun createDraftShare() { + if (isWithinDraftCreationInterval()) { + _errorMessageId.update { R.string.share_view_rate_limited_message } + return + } + if (!isCreatingDraft.compareAndSet(expect = false, update = true)) return viewModelScope.launch { try { _errorMessageId.update { null } + lastDraftCreation = TimeSource.Monotonic.markNow() val result = repository.createDraftShare() - val draft = result.dataOrElse { _errorMessageId.update { R.string.share_view_create_error_message } } - ?: return@launch + val draft = result.dataOrElse { onDraftCreationFailed(result) } ?: return@launch updateEditorEntry(ShareEditorEntry.EDIT) updateActiveShare(draft.toActiveShare()) @@ -226,6 +239,21 @@ class ShareViewModel( } } } + + private fun onDraftCreationFailed(result: NetworkResult) { + val messageId = if (result.isRateLimited) { + R.string.share_view_rate_limited_message + } else { + R.string.share_view_create_error_message + } + + _errorMessageId.update { messageId } + } + + private fun isWithinDraftCreationInterval(): Boolean { + val elapsed = lastDraftCreation?.elapsedNow() ?: return false + return elapsed < DRAFT_CREATION_INTERVAL + } // endregion // region state diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 6c7d257b..cae207bb 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ Failed to delete share. Failed to create share. Failed to update share. + Please try again in a few seconds. Participants Participants From 6fe39c53c86d8b157a30bb76490c2025906604ff Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 14:15:23 +0200 Subject: [PATCH 150/174] wip Signed-off-by: alperozturk96 --- build.gradle | 5 +- .../common/core/utils/DateFormatter.kt | 4 +- .../core/utils/ecosystem/EcosystemApp.kt | 4 +- .../core/utils/ecosystem/EcosystemManager.kt | 14 +- .../common/core/utils/ecosystem/LinkHelper.kt | 6 +- .../android/common/sample/MainActivity.kt | 15 +- .../android/common/sample/MainViewModel.kt | 17 +- .../android/common/ui/color/ColorUtil.kt | 133 +- .../ui/component/ContentUnavailableView.kt | 2 +- .../common/ui/network/PredefinedStatus.kt | 7 +- .../common/ui/network/UserStatusRepository.kt | 16 +- .../common/ui/network/auth/AuthInterceptor.kt | 15 +- .../ui/network/auth/ServerCredentials.kt | 6 +- .../common/ui/network/http/HttpMethod.kt | 2 +- .../ui/network/http/NextcloudHttpClient.kt | 25 +- .../network/http/NextcloudHttpClientLogger.kt | 26 +- .../common/ui/network/http/RequestBuilder.kt | 7 +- .../common/ui/network/model/NetworkResult.kt | 18 +- .../common/ui/network/model/OcsResponse.kt | 13 +- .../android/common/ui/share/ShareScreen.kt | 235 +-- .../ui/share/avatar/ShareAvatarRepository.kt | 13 +- .../component/CollapsibleShareSection.kt | 7 +- .../common/ui/share/component/CustomLink.kt | 6 +- .../ui/share/component/RecipientAvatar.kt | 38 + .../share/component/RecipientAvatarStack.kt | 61 + .../share/component/SelectRecipientField.kt | 12 +- .../ui/share/component/ShareContextMenu.kt | 90 ++ .../ui/share/component/ShareListItem.kt | 212 +++ .../ui/share/component/SharePermissionChip.kt | 85 ++ .../ui/share/component/ShareRecipientRow.kt | 114 ++ .../common/ui/share/component/ShareSwitch.kt | 6 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 34 +- .../RecipientPermissionBottomSheet.kt | 159 +++ .../dialog/DeleteShareConfirmationDialog.kt | 7 +- .../property/SharePropertyEnumField.kt | 6 +- .../component/property/SharePropertyView.kt | 12 +- .../datepicker/util/NoPastSelectableDates.kt | 1 - .../datepicker/util/ShareDateFormatter.kt | 9 +- .../api/capabilities/SharingCapabilities.kt | 2 +- .../model/api/capabilities/SourceType.kt | 2 +- .../common/ui/share/model/api/icon/Icon.kt | 6 +- .../share/model/api/permission/Permission.kt | 6 - .../model/api/permission/PermissionPreset.kt | 2 - .../share/model/api/recipients/Recipient.kt | 17 +- .../share/model/api/request/ShareRequests.kt | 24 +- .../ui/share/model/api/secret/Secret.kt | 6 +- .../common/ui/share/model/api/share/Share.kt | 25 +- .../ui/share/model/api/source/Source.kt | 3 - .../common/ui/share/model/api/user/User.kt | 3 - .../ui/share/model/ui/ActiveShareState.kt | 13 +- .../share/model/ui/PermissionPresetOption.kt | 1 - .../ui/share/model/ui/RecipientPermissions.kt | 49 + .../ui/share/model/ui/RecipientSummary.kt | 45 + .../common/ui/share/model/ui/ShareItemType.kt | 40 +- .../ui/share/model/ui/ShareListItemActions.kt | 19 + .../ui/share/model/ui/ShareListItemState.kt | 12 + .../common/ui/share/model/ui/ShareOverlay.kt | 82 +- .../ui/share/model/ui/ShareScreenState.kt | 3 + .../share/repository/MockShareRepository.kt | 133 +- .../share/repository/ShareRemoteRepository.kt | 176 ++- .../ui/share/repository/ShareRepository.kt | 44 +- .../ui/share/viewmodel/ShareSavedState.kt | 4 +- .../ui/share/viewmodel/ShareViewModel.kt | 167 ++- .../share/viewmodel/ShareViewModelFactory.kt | 11 +- .../common/ui/theme/MaterialSchemes.kt | 8 +- .../common/ui/theme/ViewThemeUtilsBase.kt | 14 +- .../ui/theme/utils/AndroidViewThemeUtils.kt | 1012 ++++++------- .../ui/theme/utils/AndroidXViewThemeUtils.kt | 162 +-- .../common/ui/theme/utils/ColorRole.kt | 4 +- .../ui/theme/utils/DialogViewThemeUtils.kt | 96 +- .../ui/theme/utils/MaterialViewThemeUtils.kt | 1270 ++++++++--------- .../common/ui/theme/utils/SwitchColorUtils.kt | 26 +- .../extensions/AppCompatActivityExtensions.kt | 4 +- .../ui/util/extensions/SchemeExtensions.kt | 101 +- ui/src/main/res/values/strings.xml | 34 + .../datepicker/util/ShareDateFormatterTest.kt | 10 +- 76 files changed, 2865 insertions(+), 2213 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt diff --git a/build.gradle b/build.gradle index e91c2350..53eb9fdf 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,10 @@ subprojects { spotless { kotlin { target '**/*.kt' - ktlint() + ktlint().editorConfigOverride([ + 'ktlint_function_naming_ignore_when_annotated_with': 'Composable', + 'ktlint_standard_filename' : 'disabled' + ]) } } detekt { diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt index c5da7255..b6ce886e 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/DateFormatter.kt @@ -18,9 +18,7 @@ import kotlin.text.toInt /** * Helper implementation for date formatting. */ -class DateFormatter( - private val context: Context -) { +class DateFormatter(private val context: Context) { private val sdfDays: SimpleDateFormat private val sdfMonths: SimpleDateFormat private val sdfYears: SimpleDateFormat diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt index 3d89bcd4..87a0f130 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt @@ -15,9 +15,7 @@ package com.nextcloud.android.common.core.utils.ecosystem * allow compatibility with different flavours (Play Store, F-Droid, QA, beta/dev versions, etc.). * */ -enum class EcosystemApp( - val packageNames: List -) { +enum class EcosystemApp(val packageNames: List) { FILES( listOf( "com.nextcloud.client", // generic, gplay, huawei diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt index a134a03f..9daf67f1 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt @@ -28,9 +28,7 @@ import java.util.regex.Pattern * - Redirecting to Play Store if app not installed * - Receiving account info from intents with callback support */ -class EcosystemManager( - private val activity: Activity -) { +class EcosystemManager(private val activity: Activity) { companion object { private const val TAG = "EcosystemManager" @@ -63,10 +61,7 @@ class EcosystemManager( * e.g. "abc@example.cloud.com" */ @Suppress("TooGenericExceptionCaught", "ReturnCount") - fun openApp( - app: EcosystemApp, - accountName: String? - ) { + fun openApp(app: EcosystemApp, accountName: String?) { Log.d(TAG, "open app, package name: ${app.packageNames}, account name: $accountName") // check account name emptiness @@ -163,10 +158,7 @@ class EcosystemManager( * */ @Suppress("ReturnCount") - fun receiveAccount( - intent: Intent?, - callback: AccountReceiverCallback - ) { + fun receiveAccount(intent: Intent?, callback: AccountReceiverCallback) { Log.d(TAG, "receive account started") if (intent == null) { diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/LinkHelper.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/LinkHelper.kt index 23561573..91b3417b 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/LinkHelper.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/LinkHelper.kt @@ -25,11 +25,7 @@ object LinkHelper { * @param string packageName or url-encoded search string * @param search false -> show app corresponding to packageName; true -> open search for string */ - fun openAppStore( - string: String, - search: Boolean = false, - context: Context - ) { + fun openAppStore(string: String, search: Boolean = false, context: Context) { var suffix = (if (search) "search?q=" else "details?id=") + string val intent = Intent(Intent.ACTION_VIEW, "market://$suffix".toUri()) try { diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt index f46c2704..bbbbe8c8 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt @@ -86,9 +86,18 @@ class MainActivity : AppCompatActivity() { } binding.testApiBtn.setOnClickListener { - val baseUrl = binding.baseUrl.text?.toString().orEmpty() - val username = binding.username.text?.toString().orEmpty() - val token = binding.token.text?.toString().orEmpty() + val baseUrl = + binding.baseUrl.text + ?.toString() + .orEmpty() + val username = + binding.username.text + ?.toString() + .orEmpty() + val token = + binding.token.text + ?.toString() + .orEmpty() mainViewModel.testPredefinedStatuses(baseUrl, username, token) } diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 58e6bdf4..99e6f533 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -20,29 +20,28 @@ class MainViewModel : ViewModel() { val color = MutableLiveData() val apiTestResult = MutableLiveData() - fun testPredefinedStatuses( - baseUrl: String, - username: String, - token: String - ) { + fun testPredefinedStatuses(baseUrl: String, username: String, token: String) { viewModelScope.launch { val credentials = ServerCredentials(baseUrl, username, token) val client = NextcloudHttpClient.create(credentials) val service = UserStatusRepository(client) when (val result = service.fetchPredefinedStatuses()) { - is NetworkResult.Success -> + is NetworkResult.Success -> { apiTestResult.value = "✅ Success (${result.data.size} statuses):\n" + - result.data.joinToString("\n") { "${it.icon} ${it.message}" } + result.data.joinToString("\n") { "${it.icon} ${it.message}" } + } - is NetworkResult.ServerError -> + is NetworkResult.ServerError -> { apiTestResult.value = "❌ Error ${result.response.ocs.meta.statusCode}: ${result.response.ocs.meta.message}" + } - is NetworkResult.NetworkException -> + is NetworkResult.NetworkException -> { apiTestResult.value = "❌ Exception: ${result.throwable.message}" + } } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt b/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt index 48d19696..5c9c335e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt @@ -20,94 +20,71 @@ import javax.inject.Inject import kotlin.math.roundToInt class ColorUtil - @Inject - constructor( - private val context: Context - ) { - @ColorInt - fun getNullSafeColor( - color: String?, - @ColorInt fallbackColor: Int - ): Int = color.parseColorOrFallback { fallbackColor } +@Inject +constructor(private val context: Context) { + @ColorInt + fun getNullSafeColor(color: String?, @ColorInt fallbackColor: Int): Int = + color.parseColorOrFallback { fallbackColor } - @ColorInt - fun getNullSafeColorWithFallbackRes( - color: String?, - @ColorRes fallbackColorRes: Int - ): Int = color.parseColorOrFallback { ContextCompat.getColor(context, fallbackColorRes) } + @ColorInt + fun getNullSafeColorWithFallbackRes(color: String?, @ColorRes fallbackColorRes: Int): Int = + color.parseColorOrFallback { ContextCompat.getColor(context, fallbackColorRes) } - @ColorInt - fun getTextColor( - colorText: String?, - @ColorInt backgroundColor: Int - ): Int = colorText.parseColorOrFallback { getForegroundColorForBackgroundColor(backgroundColor) } + @ColorInt + fun getTextColor(colorText: String?, @ColorInt backgroundColor: Int): Int = + colorText.parseColorOrFallback { getForegroundColorForBackgroundColor(backgroundColor) } - @ColorInt - fun getForegroundColorForBackgroundColor( - @ColorInt color: Int - ): Int = - if (isDarkBackground(color)) { - Color.WHITE - } else { - ContextCompat.getColor(context, R.color.grey_900) - } + @ColorInt + fun getForegroundColorForBackgroundColor(@ColorInt color: Int): Int = if (isDarkBackground(color)) { + Color.WHITE + } else { + ContextCompat.getColor(context, R.color.grey_900) + } - fun isDarkBackground( - @ColorInt color: Int - ): Boolean { - val hsl = FloatArray(HSL_SIZE) - ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) + fun isDarkBackground(@ColorInt color: Int): Boolean { + val hsl = FloatArray(HSL_SIZE) + ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) - return hsl[INDEX_LIGHTNESS] < LIGHTNESS_DARK_THRESHOLD - } + return hsl[INDEX_LIGHTNESS] < LIGHTNESS_DARK_THRESHOLD + } - fun setLightness( - @ColorInt color: Int, - lightness: Float - ): Int { - require(lightness in 0.0..1.0) { "Lightness must be between 0 and 1" } - val hsl = FloatArray(HSL_SIZE) - ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) + fun setLightness(@ColorInt color: Int, lightness: Float): Int { + require(lightness in 0.0..1.0) { "Lightness must be between 0 and 1" } + val hsl = FloatArray(HSL_SIZE) + ColorUtils.RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl) - hsl[INDEX_LIGHTNESS] = lightness + hsl[INDEX_LIGHTNESS] = lightness - return ColorUtils.HSLToColor(hsl) - } + return ColorUtils.HSLToColor(hsl) + } - fun colorToHexString( - @ColorInt color: Int - ): String = String.format(null, "#%06X", HEX_WHITE and color) + fun colorToHexString(@ColorInt color: Int): String = String.format(null, "#%06X", HEX_WHITE and color) - @ColorInt - private fun String?.parseColorOrFallback(fallback: () -> Int): Int = - if (this?.isNotBlank() == true) { - try { - Color.parseColor(this) - } catch (e: IllegalArgumentException) { - Log.w(TAG, "Invalid color: $this", e) - fallback() - } - } else { - fallback() - } + @ColorInt + private fun String?.parseColorOrFallback(fallback: () -> Int): Int = if (this?.isNotBlank() == true) { + try { + Color.parseColor(this) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Invalid color: $this", e) + fallback() + } + } else { + fallback() + } - @ColorInt - fun adjustOpacity( - color: Int, - opacity: Float - ): Int = - Color.argb( - (Color.alpha(color) * opacity).roundToInt(), - Color.red(color), - Color.green(color), - Color.blue(color) - ) + @ColorInt + fun adjustOpacity(color: Int, opacity: Float): Int = Color.argb( + (Color.alpha(color) * opacity).roundToInt(), + Color.red(color), + Color.green(color), + Color.blue(color) + ) - companion object { - private const val HSL_SIZE: Int = 3 - private const val INDEX_LIGHTNESS: Int = 2 - private const val LIGHTNESS_DARK_THRESHOLD: Float = 0.6f - private const val HEX_WHITE = 0xFFFFFF - private val TAG = ColorUtil::class.simpleName - } + companion object { + private const val HSL_SIZE: Int = 3 + private const val INDEX_LIGHTNESS: Int = 2 + private const val LIGHTNESS_DARK_THRESHOLD: Float = 0.6f + private const val HEX_WHITE = 0xFFFFFF + private val TAG = ColorUtil::class.simpleName } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt index 7132c2b9..e5d6fd78 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt @@ -37,7 +37,7 @@ fun ContentUnavailableView(title: String, description: String? = null, iconId: I Image( painter = painterResource(iconId), modifier = Modifier.size(48.dp), - contentDescription = "", + contentDescription = "" ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt index bc1eb79b..817e8def 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt @@ -35,9 +35,4 @@ data class ClearAt( ) @Serializable -data class PredefinedStatus( - val id: String, - val icon: String, - val message: String, - val clearAt: ClearAt? = null -) +data class PredefinedStatus(val id: String, val icon: String, val message: String, val clearAt: ClearAt? = null) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt index 7c36bdf9..b7ee92fb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt @@ -14,17 +14,17 @@ import com.nextcloud.android.common.ui.network.model.OcsResponse import com.nextcloud.android.common.ui.network.serialization.OCSSerializer class UserStatusRepository(private val client: NextcloudHttpClient) { - private companion object { private const val PREDEFINED_STATUSES_ENDPOINT = "/ocs/v2.php/apps/user_status/api/v1/predefined_statuses" } - suspend fun fetchPredefinedStatuses(): NetworkResult> = - client.executeRequest( - endpoint = PREDEFINED_STATUSES_ENDPOINT, - method = HttpMethod.GET - ) { body -> - OCSSerializer.json.decodeFromString>>(body).ocs.data - } + suspend fun fetchPredefinedStatuses(): NetworkResult> = client.executeRequest( + endpoint = PREDEFINED_STATUSES_ENDPOINT, + method = HttpMethod.GET + ) { body -> + OCSSerializer.json + .decodeFromString>>(body) + .ocs.data + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt index 07c7b485..654d0313 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt @@ -12,7 +12,6 @@ import okhttp3.Interceptor import okhttp3.Response class AuthInterceptor(private val credentials: ServerCredentials) : Interceptor { - private companion object { private const val HTTP_PREFIX = "http://" private const val HTTPS_PREFIX = "https://" @@ -26,12 +25,14 @@ class AuthInterceptor(private val credentials: ServerCredentials) : Interceptor override fun intercept(chain: Interceptor.Chain): Response { val basicCredentials = Credentials.basic(credentials.username, credentials.token) - val request = chain.request() - .newBuilder() - .header(HEADER_AUTHORIZATION, basicCredentials) - .header(HEADER_OCS_REQUEST, HEADER_OCS_REQUEST_VALUE) - .url(resolveUrl(chain.request().url.toString())) - .build() + val request = + chain + .request() + .newBuilder() + .header(HEADER_AUTHORIZATION, basicCredentials) + .header(HEADER_OCS_REQUEST, HEADER_OCS_REQUEST_VALUE) + .url(resolveUrl(chain.request().url.toString())) + .build() return chain.proceed(request) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt index b299353c..ffb2a18c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt @@ -7,8 +7,4 @@ package com.nextcloud.android.common.ui.network.auth -data class ServerCredentials( - val baseURL: String, - val username: String, - val token: String -) \ No newline at end of file +data class ServerCredentials(val baseURL: String, val username: String, val token: String) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt index f5901b8d..b03aa4c0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt @@ -12,4 +12,4 @@ enum class HttpMethod(val type: String) { POST("POST"), PUT("PUT"), DELETE("DELETE") -} \ No newline at end of file +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index ff1e3b26..d0f9454f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -35,15 +35,15 @@ class NextcloudHttpClient private constructor( private const val OCS_OK = "ok" private const val OCS_FAILURE = "failure" - fun create( - credentials: ServerCredentials - ): NextcloudHttpClient { - val okHttpClient = OkHttpClient.Builder() - .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .addInterceptor(AuthInterceptor(credentials)) - .build() + fun create(credentials: ServerCredentials): NextcloudHttpClient { + val okHttpClient = + OkHttpClient + .Builder() + .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .addInterceptor(AuthInterceptor(credentials)) + .build() return NextcloudHttpClient(okHttpClient, credentials, NextcloudHttpClientLogger(BuildConfig.DEBUG)) } @@ -75,9 +75,10 @@ class NextcloudHttpClient private constructor( } } - val ocs = OCSSerializer.json - .decodeFromString>(responseBody) - .ocs + val ocs = + OCSSerializer.json + .decodeFromString>(responseBody) + .ocs val meta = ocs.meta val isError = !response.isSuccessful || meta.status != OCS_OK debugLogger.logResponse(endpoint, method, response.code, responseBody, isError = isError) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt index 98d66e0c..da36f41d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt @@ -13,15 +13,15 @@ import okhttp3.RequestBody import okio.Buffer internal class NextcloudHttpClientLogger(private val enabled: Boolean) { - companion object { private const val TAG = "NextcloudHttpClient" - private val prettyJson = Json { - prettyPrint = true - prettyPrintIndent = " " - ignoreUnknownKeys = true - } + private val prettyJson = + Json { + prettyPrint = true + prettyPrintIndent = " " + ignoreUnknownKeys = true + } } fun logRequest(endpoint: String, method: HttpMethod, body: RequestBody?) { @@ -37,13 +37,7 @@ internal class NextcloudHttpClientLogger(private val enabled: Boolean) { ) } - fun logResponse( - endpoint: String, - method: HttpMethod, - code: Int, - responseBody: String, - isError: Boolean - ) { + fun logResponse(endpoint: String, method: HttpMethod, code: Int, responseBody: String, isError: Boolean) { if (!enabled) return Log.d( @@ -84,7 +78,11 @@ internal class NextcloudHttpClientLogger(private val enabled: Boolean) { if (this.isBlank() || this == "" || this == "") return this return try { val element = Json.parseToJsonElement(this) - prettyJson.encodeToString(kotlinx.serialization.json.JsonElement.serializer(), element) + prettyJson.encodeToString( + kotlinx.serialization.json.JsonElement + .serializer(), + element + ) } catch (_: Exception) { this } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt index 7e7560cd..3bbaf7a2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt @@ -16,11 +16,8 @@ val JSON_CONTENT_TYPE = "application/json; charset=utf-8".toMediaType() private const val HEADER_ACCEPT = "Accept" private const val HEADER_ACCEPT_VALUE = "application/json" -fun buildOcsRequest( - url: String, - method: HttpMethod, - body: RequestBody? = null -): Request = Request.Builder() +fun buildOcsRequest(url: String, method: HttpMethod, body: RequestBody? = null): Request = Request + .Builder() .url(url) .header(HEADER_ACCEPT, HEADER_ACCEPT_VALUE) .method(method.type, body) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt index 839ea5aa..2fcb0fcd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt @@ -26,12 +26,14 @@ sealed class NetworkResult { val NetworkResult<*>.isRateLimited: Boolean get() = this is NetworkResult.ServerError && response.ocs.meta.statusCode == HTTP_TOO_MANY_REQUESTS -inline fun NetworkResult.dataOrElse(onError: () -> Unit): T? = - when (this) { - is NetworkResult.Success -> data - is NetworkResult.ServerError, - is NetworkResult.NetworkException -> { - onError() - null - } +inline fun NetworkResult.dataOrElse(onError: () -> Unit): T? = when (this) { + is NetworkResult.Success -> { + data } + + is NetworkResult.ServerError, + is NetworkResult.NetworkException -> { + onError() + null + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt index 0b5c39db..af1c40cc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt @@ -11,28 +11,19 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class OcsResponse( - val ocs: Ocs -) +data class OcsResponse(val ocs: Ocs) @Serializable -data class Ocs( - val meta: Meta, - val data: T -) +data class Ocs(val meta: Meta, val data: T) @Serializable data class Meta( val status: String, - @SerialName("statuscode") val statusCode: Int, - val message: String, - @SerialName("totalitems") val totalItems: String = "", - @SerialName("itemsperpage") val itemsPerPage: String = "" ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index c684686a..220b36c0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -79,19 +80,21 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient -import com.nextcloud.android.common.ui.share.component.PublicLinkIcon -import com.nextcloud.android.common.ui.share.component.RecipientIcon +import com.nextcloud.android.common.ui.share.component.ShareListItem import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePermissionBottomSheet +import com.nextcloud.android.common.ui.share.component.bottomsheet.RecipientPermissionBottomSheet import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareItemType +import com.nextcloud.android.common.ui.share.model.ui.ShareListItemActions +import com.nextcloud.android.common.ui.share.model.ui.ShareListItemState import com.nextcloud.android.common.ui.share.model.ui.ShareOverlay import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState -import com.nextcloud.android.common.ui.share.model.ui.label +import com.nextcloud.android.common.ui.share.model.ui.recipientSummary import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModelFactory @@ -99,11 +102,13 @@ import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModelFactory private val FIRST_ITEM_TOP_SPACING = 16.dp private val ITEM_SPACING = 2.dp +private val ExpandedShareIdsSaver = listSaver, String>( + save = { it.toList() }, + restore = { it.toSet() } +) + @Composable -private fun ShareScreen( - internalLink: String, - viewModel: ShareViewModel -) { +private fun ShareScreen(internalLink: String, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsStateWithLifecycle() val screenState by viewModel.state.collectAsStateWithLifecycle() val activeShare by viewModel.activeShare.collectAsStateWithLifecycle() @@ -124,7 +129,7 @@ private fun ShareScreen( Scaffold( floatingActionButton = { FloatingActionButton( - onClick = { viewModel.createDraftShare() }, + onClick = { viewModel.createDraftShare() } ) { Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") } @@ -196,6 +201,22 @@ private fun ShareItemOverlay( onDismiss = onDismiss ) + is ShareOverlay.RecipientPermission -> { + val recipient = share.recipients.firstOrNull { + it.clazz == overlay.recipientClass && + it.value == overlay.recipientValue && + it.instance == overlay.recipientInstance + } ?: return + + RecipientPermissionBottomSheet( + share = share, + recipient = recipient, + permissionPresets = permissionPresets, + viewModel = viewModel, + onDismiss = onDismiss + ) + } + is ShareOverlay.DeleteConfirmation -> DeleteShareConfirmationDialog( onConfirm = { onDismiss() @@ -220,6 +241,24 @@ private fun ShareList( val context = LocalContext.current val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() val shares = (state as? ShareScreenState.Loaded)?.shares.orEmpty() + var expandedShareIds by rememberSaveable(stateSaver = ExpandedShareIdsSaver) { mutableStateOf(emptySet()) } + val actions = remember(viewModel, onShowOverlay) { + ShareListItemActions( + onSelectShare = { share -> viewModel.setActiveShare(share, ShareEditorEntry.EDIT) }, + onSendEmail = { share -> viewModel.setActiveShare(share, ShareEditorEntry.SEND_EMAIL) }, + onToggleExpanded = { share -> + expandedShareIds = if (share.id in expandedShareIds) { + expandedShareIds - share.id + } else { + expandedShareIds + share.id + } + }, + onShowOverlay = onShowOverlay, + onRemoveRecipient = { share, recipient -> + viewModel.removeRecipient(share.id, recipient.clazz, recipient.value, recipient.instance) + } + ) + } PullToRefreshBox( isRefreshing = isRefreshing, @@ -243,19 +282,19 @@ private fun ShareList( itemsIndexed(shares, key = { _, share -> share.id }) { index, share -> Spacer(modifier = Modifier.height(if (index == 0) FIRST_ITEM_TOP_SPACING else ITEM_SPACING)) - ShareItem( - share = share, - title = share.getHeadline(context, shares), - type = ShareItemType.type(index, shares.lastIndex), - presetOptions = PermissionPresetOption.optionsFor(share, permissionPresets), - selectedPresetOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), - onSelectShare = { selected -> - viewModel.setActiveShare(selected, ShareEditorEntry.EDIT) - }, - onSendEmail = { selected -> - viewModel.setActiveShare(selected, ShareEditorEntry.SEND_EMAIL) - }, - onShowOverlay = onShowOverlay + ShareListItem( + state = ShareListItemState( + share = share, + title = if (share.hasMultipleRecipients) { + recipientSummary(share.recipients) + } else { + share.getHeadline(context, shares) + }, + type = ShareItemType.type(index, shares.lastIndex), + isExpanded = share.id in expandedShareIds + ), + permissionPresets = permissionPresets, + actions = actions ) } } @@ -278,160 +317,6 @@ private fun SharePlaceholder(state: ShareScreenState) { ) } -@Composable -private fun ShareItem( - share: Share, - title: String, - type: ShareItemType, - presetOptions: List, - selectedPresetOption: PermissionPresetOption, - onSelectShare: (Share) -> Unit, - onSendEmail: (Share) -> Unit, - onShowOverlay: (ShareOverlay) -> Unit -) { - var contextMenuExpanded by remember { mutableStateOf(false) } - val haptics = LocalHapticFeedback.current - - ListItem( - modifier = Modifier - .fillMaxWidth(0.9f) - .clip(type.getShape()) - .combinedClickable( - onClick = { onSelectShare(share) }, - onLongClick = { - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - contextMenuExpanded = true - } - ) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), - headlineContent = { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - leadingContent = { - Box( - modifier = Modifier.size(24.dp), - contentAlignment = Alignment.Center - ) { - val icon = share.recipients.firstOrNull()?.icon - if (icon != null) { - RecipientIcon(icon = icon, modifier = Modifier.size(24.dp)) - } else { - PublicLinkIcon(modifier = Modifier.size(24.dp)) - } - } - }, - supportingContent = { - val chipHorizontalPadding = 10.dp - val selectedLabel = selectedPresetOption.label() - - Row( - modifier = Modifier - .offset(x = -chipHorizontalPadding) - .clip(RoundedCornerShape(percent = 50)) - .clickable { onShowOverlay(ShareOverlay.QuickShare(share.id)) } - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.08f)) - .padding(horizontal = chipHorizontalPadding, vertical = 2.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box(contentAlignment = Alignment.CenterStart) { - presetOptions.forEach { option -> - Text( - text = option.label(), - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - modifier = Modifier - .alpha(0f) - .clearAndSetSemantics {} - ) - } - - Text( - text = selectedLabel, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, - maxLines = 1 - ) - } - - Spacer(modifier = Modifier.width(2.dp)) - - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(18.dp) - ) - } - }, - trailingContent = { - Box { - IconButton(onClick = { contextMenuExpanded = true }) { - Icon(Icons.Default.MoreVert, contentDescription = "More options") - } - - DropdownMenu( - expanded = contextMenuExpanded, - onDismissRequest = { contextMenuExpanded = false } - ) { - DropdownMenuItem( - leadingIcon = { - Icon(Icons.Default.Edit, - contentDescription = "Edit icon", - modifier = Modifier.size(18.dp) - ) - }, - text = { Text(stringResource(R.string.share_view_list_item_edit)) }, - onClick = { - contextMenuExpanded = false - onSelectShare(share) - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - Icons.AutoMirrored.Filled.Send, - contentDescription = "Send icon", - modifier = Modifier.size(18.dp) - ) - }, - text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, - onClick = { - contextMenuExpanded = false - onSendEmail(share) - } - ) - HorizontalDivider() - DropdownMenuItem( - leadingIcon = { - Icon( - Icons.Default.Delete, - contentDescription = "Delete icon", - modifier = Modifier.size(18.dp) - ) - }, - text = { - Text( - stringResource(R.string.share_view_list_item_delete), - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { - contextMenuExpanded = false - onShowOverlay(ShareOverlay.DeleteConfirmation(share.id)) - } - ) - } - } - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent) - ) -} - fun ComposeView.initShareScreen( viewModelStoreOwner: ViewModelStoreOwner, sourceId: String, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt index 6316f315..d05adc9f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt @@ -26,12 +26,13 @@ class ShareAvatarRepository(private val credentials: ServerCredentials) { suspend fun fetchShareAvatars(sourceId: String): List? = withContext(Dispatchers.IO) { val client = NextcloudHttpClient.create(credentials) val shareRepository = ShareRemoteRepository(client) - val result = shareRepository.fetchShares( - filterSourceTypeValue = sourceId, - limit = SHARES_PAGE_SIZE, - filterSourceTypeClass = Source.NODE_SOURCE_CLASS, - lastShareID = null - ) + val result = + shareRepository.fetchShares( + filterSourceTypeValue = sourceId, + limit = SHARES_PAGE_SIZE, + filterSourceTypeClass = Source.NODE_SOURCE_CLASS, + lastShareID = null + ) return@withContext result.dataOrElse { Log.e(TAG, "failed to fetch avatars") } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt index 4214e7f6..a31eecf0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -28,12 +28,7 @@ import androidx.compose.ui.unit.dp private val SECTION_VERTICAL_PADDING = 16.dp @Composable -fun CollapsibleShareSection( - label: String, - isExpanded: Boolean, - onToggle: () -> Unit, - content: @Composable () -> Unit -) { +fun CollapsibleShareSection(label: String, isExpanded: Boolean, onToggle: () -> Unit, content: @Composable () -> Unit) { Column(modifier = Modifier.fillMaxWidth().padding(vertical = SECTION_VERTICAL_PADDING)) { Row( modifier = Modifier diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index 2f28967b..fc59caed 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -35,11 +35,7 @@ import kotlinx.coroutines.launch private const val MAX_TOKEN_LENGTH = 32 @Composable -fun CustomLink( - recipient: Recipient, - onGenerateSecret: suspend () -> String?, - onTokenChange: (String) -> Unit -) { +fun CustomLink(recipient: Recipient, onGenerateSecret: suspend () -> String?, onTokenChange: (String) -> Unit) { val prefix = remember(recipient.secret.url, recipient.secret.value) { val url = recipient.secret.url.orEmpty() val token = recipient.secret.value.orEmpty() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt new file mode 100644 index 00000000..e97028e6 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient + +private val PUBLIC_LINK_ICON_PADDING = 3.dp + +@Composable +fun RecipientAvatar(recipient: Recipient, size: Dp, modifier: Modifier = Modifier) { + val avatarModifier = modifier + .size(size) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHighest) + + val icon = recipient.icon + if (icon == null) { + PublicLinkIcon(modifier = avatarModifier.padding(PUBLIC_LINK_ICON_PADDING)) + return + } + + RecipientIcon(icon = icon, modifier = avatarModifier) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt new file mode 100644 index 00000000..5d8a9dd2 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient + +private const val MAX_AVATARS = 3 + +private val AVATAR_SIZE = 26.dp +private val AVATAR_OVERLAP = 9.dp +private val AVATAR_RING_WIDTH = 1.5.dp +private val OVERFLOW_START_PADDING = 4.dp + +@Composable +fun RecipientAvatarStack(recipients: List, modifier: Modifier = Modifier) { + val displayed = recipients.take(MAX_AVATARS) + val overflow = recipients.size - displayed.size + + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + displayed.forEachIndexed { index, recipient -> + RecipientAvatar( + recipient = recipient, + size = AVATAR_SIZE, + modifier = Modifier + .offset(x = -AVATAR_OVERLAP * index) + .zIndex((displayed.size - index).toFloat()) + .border(AVATAR_RING_WIDTH, MaterialTheme.colorScheme.surface, CircleShape) + ) + } + + if (overflow > 0) { + Text( + text = stringResource(R.string.share_view_recipients_overflow, overflow), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .offset(x = -AVATAR_OVERLAP * displayed.size) + .padding(start = OVERFLOW_START_PADDING) + ) + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 2f21eb41..41f2b8e2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -54,10 +54,7 @@ private val CHIP_CLOSE_ICON_SIZE = 18.dp private val RESULT_ICON_SIZE = 20.dp @Composable -fun SelectRecipientField( - share: Share, - viewModel: ShareViewModel -) { +fun SelectRecipientField(share: Share, viewModel: ShareViewModel) { val selected = share.invitedRecipients Column(modifier = Modifier.fillMaxWidth()) { @@ -153,10 +150,7 @@ private fun RecipientSearch(share: Share, selected: List, viewModel: } @Composable -private fun RecipientDropdownContent( - results: List, - onSelect: (Recipient) -> Unit -) { +private fun RecipientDropdownContent(results: List, onSelect: (Recipient) -> Unit) { if (results.isEmpty()) { DropdownMenuItem( text = { @@ -210,7 +204,7 @@ fun RecipientIcon(icon: Icon, modifier: Modifier = Modifier) { .decoderFactory(SvgDecoder.Factory()) .build(), contentDescription = null, - modifier = modifier, + modifier = modifier ) } else { // reserve the same space even with no image diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt new file mode 100644 index 00000000..b06a3eba --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt @@ -0,0 +1,90 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R + +private val MENU_ICON_SIZE = 18.dp + +@Composable +fun ShareContextMenu( + expanded: Boolean, + onDismiss: () -> Unit, + onEdit: () -> Unit, + onSendEmail: () -> Unit, + onDelete: () -> Unit +) { + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(MENU_ICON_SIZE) + ) + }, + text = { Text(stringResource(R.string.share_view_list_item_edit)) }, + onClick = { + onDismiss() + onEdit() + } + ) + + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = null, + modifier = Modifier.size(MENU_ICON_SIZE) + ) + }, + text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, + onClick = { + onDismiss() + onSendEmail() + } + ) + + HorizontalDivider() + + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(MENU_ICON_SIZE) + ) + }, + text = { + Text( + text = stringResource(R.string.share_view_list_item_delete), + color = MaterialTheme.colorScheme.error + ) + }, + onClick = { + onDismiss() + onDelete() + } + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt new file mode 100644 index 00000000..8f283ab5 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -0,0 +1,212 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.MoreVert +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +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.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption +import com.nextcloud.android.common.ui.share.model.ui.ShareListItemActions +import com.nextcloud.android.common.ui.share.model.ui.ShareListItemState +import com.nextcloud.android.common.ui.share.model.ui.ShareOverlay + +private const val ITEM_WIDTH_FRACTION = 0.9f +private const val CONTAINER_ALPHA = 0.5f +private const val COLLAPSED_CHEVRON_ROTATION = -90f +private const val EXPANDED_CHEVRON_ROTATION = 0f + +private val LEADING_ICON_SIZE = 24.dp + +@Composable +fun ShareListItem(state: ShareListItemState, permissionPresets: List, actions: ShareListItemActions) { + val share = state.share + var contextMenuExpanded by remember { mutableStateOf(false) } + val haptics = LocalHapticFeedback.current + + Column( + modifier = Modifier + .fillMaxWidth(ITEM_WIDTH_FRACTION) + .clip(state.type.getShape()) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = CONTAINER_ALPHA)) + ) { + ListItem( + modifier = Modifier.combinedClickable( + onClick = { actions.onSelectShare(share) }, + onLongClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + contextMenuExpanded = true + } + ), + headlineContent = { + Text( + text = state.title, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingContent = { ShareItemLeadingContent(share) }, + supportingContent = { + SharePermissionChip( + options = PermissionPresetOption.optionsFor(share, permissionPresets), + selectedOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), + onClick = { actions.onShowOverlay(ShareOverlay.QuickShare(share.id)) } + ) + }, + trailingContent = { + ShareItemTrailingContent( + state = state, + isContextMenuExpanded = contextMenuExpanded, + onContextMenuExpandedChange = { contextMenuExpanded = it }, + actions = actions + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) + + AnimatedVisibility(visible = state.isExpanded && share.hasMultipleRecipients) { + ShareRecipientList( + share = share, + permissionPresets = permissionPresets, + actions = actions + ) + } + } +} + +@Composable +private fun ShareItemTrailingContent( + state: ShareListItemState, + isContextMenuExpanded: Boolean, + onContextMenuExpandedChange: (Boolean) -> Unit, + actions: ShareListItemActions +) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (state.share.hasMultipleRecipients) { + ExpandRecipientsButton( + isExpanded = state.isExpanded, + onClick = { actions.onToggleExpanded(state.share) } + ) + } + + Box { + IconButton(onClick = { onContextMenuExpandedChange(true) }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.share_view_list_item_more_options) + ) + } + + ShareContextMenu( + expanded = isContextMenuExpanded, + onDismiss = { onContextMenuExpandedChange(false) }, + onEdit = { actions.onSelectShare(state.share) }, + onSendEmail = { actions.onSendEmail(state.share) }, + onDelete = { actions.onShowOverlay(ShareOverlay.DeleteConfirmation(state.share.id)) } + ) + } + } +} + +@Composable +private fun ShareRecipientList(share: Share, permissionPresets: List, actions: ShareListItemActions) { + Column { + share.recipients.forEach { recipient -> + key(recipient.clazz, recipient.value, recipient.instance) { + ShareRecipientRow( + share = share, + recipient = recipient, + permissionPresets = permissionPresets, + onEditPermissions = { + actions.onShowOverlay( + ShareOverlay.RecipientPermission( + shareId = share.id, + recipientClass = recipient.clazz, + recipientValue = recipient.value, + recipientInstance = recipient.instance + ) + ) + }, + onRemove = { actions.onRemoveRecipient(share, recipient) } + ) + } + } + } +} + +@Composable +private fun ShareItemLeadingContent(share: Share) { + if (share.hasMultipleRecipients) { + RecipientAvatarStack(recipients = share.recipients) + return + } + + Box( + modifier = Modifier.size(LEADING_ICON_SIZE), + contentAlignment = Alignment.Center + ) { + val recipient = share.recipients.firstOrNull() + if (recipient != null) { + RecipientAvatar(recipient = recipient, size = LEADING_ICON_SIZE) + } else { + PublicLinkIcon(modifier = Modifier.size(LEADING_ICON_SIZE)) + } + } +} + +@Composable +private fun ExpandRecipientsButton(isExpanded: Boolean, onClick: () -> Unit) { + val descriptionId = if (isExpanded) { + R.string.share_view_recipients_collapse + } else { + R.string.share_view_recipients_expand + } + + IconButton(onClick = onClick) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = stringResource(descriptionId), + modifier = Modifier.rotate( + if (isExpanded) EXPANDED_CHEVRON_ROTATION else COLLAPSED_CHEVRON_ROTATION + ) + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt new file mode 100644 index 00000000..08f1035f --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt @@ -0,0 +1,85 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption +import com.nextcloud.android.common.ui.share.model.ui.label + +private val CHIP_HORIZONTAL_PADDING = 10.dp +private val CHIP_VERTICAL_PADDING = 2.dp +private val CHIP_ICON_SIZE = 18.dp +private val CHIP_ICON_SPACING = 2.dp +private const val CHIP_BACKGROUND_ALPHA = 0.08f + +@Composable +fun SharePermissionChip( + options: List, + selectedOption: PermissionPresetOption, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .offset(x = -CHIP_HORIZONTAL_PADDING) + .clip(RoundedCornerShape(percent = 50)) + .clickable { onClick() } + .background(MaterialTheme.colorScheme.primary.copy(alpha = CHIP_BACKGROUND_ALPHA)) + .padding(horizontal = CHIP_HORIZONTAL_PADDING, vertical = CHIP_VERTICAL_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Box(contentAlignment = Alignment.CenterStart) { + options.forEach { option -> + Text( + text = option.label(), + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + modifier = Modifier + .alpha(0f) + .clearAndSetSemantics {} + ) + } + + Text( + text = selectedOption.label(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + maxLines = 1 + ) + } + + Spacer(modifier = Modifier.width(CHIP_ICON_SPACING)) + + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(CHIP_ICON_SIZE) + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt new file mode 100644 index 00000000..5e068e60 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt @@ -0,0 +1,114 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +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.Text +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.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.effectivePermissionsFor +import com.nextcloud.android.common.ui.share.model.ui.presetOption +import com.nextcloud.android.common.ui.share.model.ui.recipientPresetOptions + +private val ROW_START_PADDING = 24.dp +private val AVATAR_SIZE = 24.dp +private val MENU_ICON_SIZE = 18.dp + +@Composable +fun ShareRecipientRow( + share: Share, + recipient: Recipient, + permissionPresets: List, + onEditPermissions: () -> Unit, + onRemove: () -> Unit +) { + ListItem( + modifier = Modifier + .clickable { onEditPermissions() } + .padding(start = ROW_START_PADDING), + headlineContent = { + Text( + text = recipient.displayName, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + leadingContent = { RecipientAvatar(recipient = recipient, size = AVATAR_SIZE) }, + supportingContent = { + SharePermissionChip( + options = share.recipientPresetOptions(permissionPresets), + selectedOption = share.effectivePermissionsFor(recipient).presetOption(permissionPresets), + onClick = onEditPermissions + ) + }, + trailingContent = { RecipientRowMenu(onRemove = onRemove) }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) +} + +@Composable +private fun RecipientRowMenu(onRemove: () -> Unit) { + var expanded by remember { mutableStateOf(false) } + + Box { + IconButton(onClick = { expanded = true }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(R.string.share_view_list_item_more_options) + ) + } + + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(MENU_ICON_SIZE) + ) + }, + text = { + Text( + text = stringResource(R.string.share_view_recipient_remove), + color = MaterialTheme.colorScheme.error + ) + }, + onClick = { + expanded = false + onRemove() + } + ) + } + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index 8af52f43..d50f99f9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -22,11 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun ShareSwitch( - label: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { +fun ShareSwitch(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { Row( modifier = Modifier .fillMaxWidth() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index a87831cc..81fa788e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -5,7 +5,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.layout.Arrangement @@ -128,7 +127,7 @@ fun AddOrEditShareBottomSheet( sheetGesturesEnabled = false, dragHandle = null, properties = ModalBottomSheetProperties(shouldDismissOnClickOutside = false), - containerColor = MaterialTheme.colorScheme.surface, + containerColor = MaterialTheme.colorScheme.surface ) { Column( modifier = Modifier @@ -351,11 +350,7 @@ private fun PermissionPresetDropdown( } @Composable -private fun BasicSettingsSection( - share: Share, - propertyErrors: Map, - viewModel: ShareViewModel -) { +private fun BasicSettingsSection(share: Share, propertyErrors: Map, viewModel: ShareViewModel) { if (!share.isBasicSectionAvailable) { return } @@ -435,7 +430,7 @@ private fun ActionButtons( modifier = Modifier .fillMaxWidth() .padding(vertical = ACTION_BUTTONS_VERTICAL_PADDING), - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Button( onClick = { @@ -452,8 +447,8 @@ private fun ActionButtons( shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onSurface, - ), + contentColor = MaterialTheme.colorScheme.onSurface + ) ) { if (isPreparingLink) { CircularProgressIndicator( @@ -465,12 +460,12 @@ private fun ActionButtons( Icon( painter = painterResource(R.drawable.ic_link), contentDescription = null, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(20.dp) ) } Text( text = stringResource(category.copyLinkTitleId), - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = 8.dp) ) } @@ -484,17 +479,17 @@ private fun ActionButtons( shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), + contentColor = MaterialTheme.colorScheme.onPrimary + ) ) { Icon( imageVector = category.sendActionIcon, contentDescription = null, - modifier = Modifier.size(20.dp), + modifier = Modifier.size(20.dp) ) Text( text = stringResource(category.sendActionTitleId), - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = 8.dp) ) } } else { @@ -507,12 +502,12 @@ private fun ActionButtons( shape = RoundedCornerShape(10.dp), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onSurface, - ), + contentColor = MaterialTheme.colorScheme.onSurface + ) ) { Text( text = stringResource(category.sendActionTitleId), - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = 8.dp) ) } } @@ -585,4 +580,3 @@ private val previewShare = Share( ), permissionPreset = null ) - diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt new file mode 100644 index 00000000..f9dfa566 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -0,0 +1,159 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.android.common.ui.share.component.bottomsheet + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.component.ShareSwitch +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption +import com.nextcloud.android.common.ui.share.model.ui.effectivePermissionsFor +import com.nextcloud.android.common.ui.share.model.ui.grantedPermissions +import com.nextcloud.android.common.ui.share.model.ui.label +import com.nextcloud.android.common.ui.share.model.ui.presetOption +import com.nextcloud.android.common.ui.share.model.ui.recipientPresetOptions +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel + +private val SHEET_BOTTOM_PADDING = 32.dp +private val CONTENT_PADDING = 16.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RecipientPermissionBottomSheet( + share: Share, + recipient: Recipient, + permissionPresets: List, + viewModel: ShareViewModel, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val selectedOption = share.effectivePermissionsFor(recipient).presetOption(permissionPresets) + var showCustomPermissions by remember(recipient.value) { + mutableStateOf(selectedOption == PermissionPresetOption.Custom) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = SHEET_BOTTOM_PADDING) + ) { + Text( + text = stringResource(R.string.share_view_recipient_permission_title, recipient.displayName), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(CONTENT_PADDING) + ) + + share.recipientPresetOptions(permissionPresets).forEach { option -> + PresetOptionItem( + option = option, + isSelected = option == if (showCustomPermissions) PermissionPresetOption.Custom else selectedOption, + onClick = { + val presetClass = option.presetClass + if (presetClass == null) { + showCustomPermissions = true + return@PresetOptionItem + } + + onDismiss() + viewModel.updateRecipientPermissionPreset(share.id, recipient, presetClass) + } + ) + } + + if (!showCustomPermissions) return@Column + + HorizontalDivider() + + CustomPermissions( + share = share, + recipient = recipient, + viewModel = viewModel + ) + } + } +} + +@Composable +private fun CustomPermissions(share: Share, recipient: Recipient, viewModel: ShareViewModel) { + val effectivePermissions = share.effectivePermissionsFor(recipient) + + Column(modifier = Modifier.padding(horizontal = CONTENT_PADDING)) { + Text( + text = stringResource(R.string.share_view_recipient_permission_custom_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = CONTENT_PADDING) + ) + + share.grantedPermissions().forEach { permission -> + key(permission.clazz) { + val isEnabled = effectivePermissions.first { it.clazz == permission.clazz }.enabled + var checked by remember(permission.clazz, isEnabled) { mutableStateOf(isEnabled) } + + ShareSwitch( + label = permission.displayName, + checked = checked, + onCheckedChange = { value -> + checked = value + viewModel.updateRecipientPermission(share.id, recipient, permission.clazz, value) + } + ) + } + } + } +} + +@Composable +private fun PresetOptionItem(option: PermissionPresetOption, isSelected: Boolean, onClick: () -> Unit) { + ListItem( + modifier = Modifier.clickable { onClick() }, + headlineContent = { Text(option.label()) }, + trailingContent = { + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt index cf16a865..b55eb7af 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt @@ -18,10 +18,7 @@ import androidx.compose.ui.res.stringResource import com.nextcloud.android.common.ui.R @Composable -fun DeleteShareConfirmationDialog( - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { +fun DeleteShareConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(R.string.share_view_delete_confirm_title)) }, @@ -43,4 +40,4 @@ fun DeleteShareConfirmationDialog( } } ) -} \ No newline at end of file +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt index 22fb837d..9331e88e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt @@ -27,11 +27,7 @@ import com.nextcloud.android.common.ui.share.model.api.property.PropertyEnum @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SharePropertyEnumField( - property: PropertyEnum, - isError: Boolean, - onValueSelected: (String) -> Unit -) { +fun SharePropertyEnumField(property: PropertyEnum, isError: Boolean, onValueSelected: (String) -> Unit) { var expanded by remember { mutableStateOf(false) } var selectedValue by remember(property.clazz) { mutableStateOf(property.value ?: "") } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 689eb4f6..aa38992a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -45,11 +45,7 @@ fun propertyErrorMessage(propertyErrors: Map, clazz: String): S } @Composable -fun SharePropertyView( - property: Property, - errorMessage: String?, - onValueChange: (String?) -> Unit -) { +fun SharePropertyView(property: Property, errorMessage: String?, onValueChange: (String?) -> Unit) { Column { SharePropertyField( property = property, @@ -62,11 +58,7 @@ fun SharePropertyView( } @Composable -private fun SharePropertyField( - property: Property, - isError: Boolean, - onValueChange: (String?) -> Unit -) { +private fun SharePropertyField(property: Property, isError: Boolean, onValueChange: (String?) -> Unit) { when (property) { is PropertyBoolean -> { var checkedValue by remember(property.clazz) { mutableStateOf(property.isTrue()) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt index 0fdc2c3c..5888b2bb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt @@ -12,7 +12,6 @@ import java.time.LocalDate import java.time.ZoneOffset class NoPastSelectableDates : SelectableDates { - private val today: LocalDate = LocalDate.now(ZoneOffset.UTC) private val todayUtcMillis: Long = today.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt index 8e766bfb..84d963a3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt @@ -19,7 +19,6 @@ import java.time.format.FormatStyle import java.util.Locale class ShareDateFormatter { - private val displayDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.getDefault()) @@ -30,10 +29,10 @@ class ShareDateFormatter { datePickerState.getSelectedDate()?.format(displayDateFormatter) @OptIn(ExperimentalMaterial3Api::class) - fun formatIso8601Date(datePickerState: DatePickerState): String? = - datePickerState.getSelectedDate() - ?.atStartOfDay(ZoneId.systemDefault()) - ?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + fun formatIso8601Date(datePickerState: DatePickerState): String? = datePickerState + .getSelectedDate() + ?.atStartOfDay(ZoneId.systemDefault()) + ?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) fun formatIsoForDisplay(isoDate: String?): String { if (isoDate.isNullOrBlank()) return "" diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 8f980241..33884193 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -18,7 +18,7 @@ data class SharingCapabilities( @SerialName("source_types") val sourceTypes: List = emptyList(), @SerialName("permission_presets") - val permissionPresets: List = emptyList(), + val permissionPresets: List = emptyList() ) { val isUnifiedShareEnabled: Boolean get() = UNIFIED_SHARE_API_VERSION in apiVersions diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt index dbdfe0a6..a1d4d945 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt @@ -13,5 +13,5 @@ import kotlinx.serialization.Serializable @Serializable data class SourceType( @SerialName("class") - val classField: String, + val classField: String ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt index 4343042a..8b122817 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt @@ -10,8 +10,4 @@ package com.nextcloud.android.common.ui.share.model.api.icon import kotlinx.serialization.Serializable @Serializable -data class Icon( - val svg: String? = null, - val light: String? = null, - val dark: String? = null -) +data class Icon(val svg: String? = null, val light: String? = null, val dark: String? = null) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt index ccd405a5..c0e9f07c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -17,18 +17,12 @@ import kotlinx.serialization.Serializable data class Permission( @SerialName("class") val clazz: String, - @SerialName("source_class") val sourceClass: String? = null, - @SerialName("display_name") val displayName: String, - val hint: String? = null, - val priority: Int, - val presets: List, - val enabled: Boolean ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt index eb78df95..f884c132 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt @@ -14,9 +14,7 @@ import kotlinx.serialization.Serializable data class PermissionPreset( @SerialName("class") val clazz: String, - @SerialName("display_name") val displayName: String, - val hint: String? = null ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index 601dabcc..94b38c55 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -8,6 +8,7 @@ package com.nextcloud.android.common.ui.share.model.api.recipients import com.nextcloud.android.common.ui.share.model.api.icon.Icon +import com.nextcloud.android.common.ui.share.model.api.permission.Permission import com.nextcloud.android.common.ui.share.model.api.secret.Secret import com.nextcloud.android.common.ui.share.model.api.user.User import kotlinx.serialization.SerialName @@ -17,24 +18,22 @@ import kotlinx.serialization.Serializable data class Recipient( @SerialName("class") val clazz: String, - val value: String, - val instance: String? = null, - @SerialName("display_name") val displayName: String, - val icon: Icon? = null, - val secret: Secret, - - val initiator: User? = null + val initiator: User? = null, + val permissions: List = emptyList() ) { companion object { + const val USER_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\UserShareRecipientType""" + const val GROUP_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\GroupShareRecipientType""" + const val TEAM_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\TeamShareRecipientType""" + const val EMAIL_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\EmailShareRecipientType""" const val TOKEN_RECIPIENT_CLASS = """OC\Core\Sharing\Recipient\TokenShareRecipientType""" } - fun isSameAs(other: Recipient): Boolean = - clazz == other.clazz && value == other.value && instance == other.instance + fun isSameAs(other: Recipient): Boolean = clazz == other.clazz && value == other.value && instance == other.instance } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt index 3ca61b45..c6a24f60 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt @@ -13,10 +13,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement @Serializable -data class GetShareRequest( - val secret: String? = null, - val arguments: Map = emptyMap() -) +data class GetShareRequest(val secret: String? = null, val arguments: Map = emptyMap()) @Serializable data class UpdateShareStateRequest( @@ -28,7 +25,6 @@ data class UpdateShareStateRequest( data class AddSourceRequest( @SerialName("class") val clazz: String, - val value: String ) @@ -36,9 +32,7 @@ data class AddSourceRequest( data class AddRecipientRequest( @SerialName("class") val clazz: String, - val value: String, - val instance: String? = null ) @@ -46,7 +40,6 @@ data class AddRecipientRequest( data class UpdateSharePropertyRequest( @SerialName("class") val clazz: String, - val value: String? = null ) @@ -54,23 +47,26 @@ data class UpdateSharePropertyRequest( data class UpdateSharePermissionRequest( @SerialName("class") val clazz: String, - val enabled: Boolean ) @Serializable -data class UpdateSharePermissionPresetRequest( - val permissionPresetClass: String +data class UpdateShareRecipientPermissionRequest( + val recipientClass: String, + val recipientValue: String, + val recipientInstance: String? = null, + val permissionClass: String, + val enabled: Boolean ) +@Serializable +data class UpdateSharePermissionPresetRequest(val permissionPresetClass: String) + @Serializable data class UpdateShareRecipientSecretRequest( @SerialName("class") val clazz: String, - val value: String, - val instance: String? = null, - val secret: String ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt index b951e6d0..2a9a29e6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt @@ -10,8 +10,4 @@ package com.nextcloud.android.common.ui.share.model.api.secret import kotlinx.serialization.Serializable @Serializable -data class Secret( - val updatable: Boolean, - val value: String? = null, - val url: String? = null -) +data class Secret(val updatable: Boolean, val value: String? = null, val url: String? = null) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index ae89d385..67171514 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -27,26 +27,17 @@ import kotlinx.serialization.Serializable @Serializable data class Share( val id: String, - val owner: User, - @SerialName("last_updated") val lastUpdated: Long, - @SerialName("state") val shareState: ShareState, - @SerialName("user_status") val userStatus: ShareUserStatus? = null, - val sources: List, - val recipients: List, - val properties: List, - val permissions: List, - @SerialName("permission_preset") val permissionPreset: String? = null ) { @@ -72,9 +63,7 @@ data class Share( return advancedProperties.isNotEmpty() || customLinkRecipient != null } - fun toActiveShare(): ActiveShareState { - return ActiveShareState.Editing(this) - } + fun toActiveShare(): ActiveShareState = ActiveShareState.Editing(this) fun getClipEntry(internalLink: String, category: ShareCategory): ClipEntry? { if (category == ShareCategory.Anyone) { @@ -104,10 +93,14 @@ data class Share( } val label: String? - get() = properties - .firstOrNull { it.clazz.contains(LABEL_PROPERTY_CLASS, ignoreCase = true) } - ?.value - ?.takeIf { it.isNotBlank() } + get() = + properties + .firstOrNull { it.clazz.contains(LABEL_PROPERTY_CLASS, ignoreCase = true) } + ?.value + ?.takeIf { it.isNotBlank() } + + val hasMultipleRecipients: Boolean + get() = recipients.size > 1 val belongsAnyoneTab: Boolean get() { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt index f4c66311..e9eaea43 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt @@ -15,12 +15,9 @@ import kotlinx.serialization.Serializable data class Source( @SerialName("class") val clazz: String, - val value: String, - @SerialName("display_name") val displayName: String, - val icon: Icon? = null ) { companion object { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt index d0870bf3..73745398 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt @@ -15,11 +15,8 @@ import kotlinx.serialization.Serializable data class User( @SerialName("user_id") val userId: String, - val instance: String? = null, - @SerialName("display_name") val displayName: String, - val icon: Icon ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt index e7b56192..2cd8b708 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt @@ -11,13 +11,16 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share sealed class ActiveShareState { data object None : ActiveShareState() + data class Editing(val share: Share) : ActiveShareState() + data class Activating(val share: Share) : ActiveShareState() val shareOrNull: Share? - get() = when (this) { - is Editing -> share - is Activating -> share - None -> null - } + get() = + when (this) { + is Editing -> share + is Activating -> share + None -> null + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt index 73afcc48..819c4f4a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt @@ -11,7 +11,6 @@ import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPres import com.nextcloud.android.common.ui.share.model.api.share.Share sealed interface PermissionPresetOption { - val presetClass: String? data object Custom : PermissionPresetOption { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt new file mode 100644 index 00000000..fe0a70c2 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import com.nextcloud.android.common.ui.share.model.api.permission.Permission +import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.share.Share + +fun Share.effectivePermissionsFor(recipient: Recipient): List { + val overrides = recipient.permissions.associateBy { it.clazz } + return permissions.map { permission -> + val override = overrides[permission.clazz] ?: return@map permission + permission.copy(enabled = permission.enabled && override.enabled) + } +} + +fun Share.grantedPermissions(): List = permissions.filter { it.enabled } + +fun List.presetOption(presets: List): PermissionPresetOption { + val enabled = filter { it.enabled }.map { it.clazz }.toSet() + val preset = + presets.firstOrNull { preset -> + val members = filter { preset.clazz in it.presets }.map { it.clazz } + members.isNotEmpty() && members.size == enabled.size && enabled.containsAll(members) + } + return preset?.let { PermissionPresetOption.Preset(it) } ?: PermissionPresetOption.Custom +} + +fun Share.recipientPresetOptions(presets: List): List { + val selectable = + presets.filter { preset -> + val members = permissions.filter { preset.clazz in it.presets } + members.isNotEmpty() && members.all { it.enabled } + } + return selectable.map { PermissionPresetOption.Preset(it) } + PermissionPresetOption.Custom +} + +fun Share.recipientPermissionChanges(recipient: Recipient, presetClass: String): Map { + val effective = effectivePermissionsFor(recipient).associate { it.clazz to it.enabled } + return grantedPermissions() + .associate { it.clazz to (presetClass in it.presets) } + .filterNot { (clazz, enabled) -> effective[clazz] == enabled } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt new file mode 100644 index 00000000..a7a75064 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import androidx.annotation.PluralsRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient + +private val SUMMARY_PLURALS = + mapOf( + Recipient.USER_RECIPIENT_CLASS to R.plurals.share_view_recipient_summary_people, + Recipient.GROUP_RECIPIENT_CLASS to R.plurals.share_view_recipient_summary_groups, + Recipient.TEAM_RECIPIENT_CLASS to R.plurals.share_view_recipient_summary_teams, + Recipient.EMAIL_RECIPIENT_CLASS to R.plurals.share_view_recipient_summary_emails, + Recipient.TOKEN_RECIPIENT_CLASS to R.plurals.share_view_recipient_summary_links + ) + +@Composable +fun recipientSummary(recipients: List): String { + val counts = recipients.groupingBy { it.clazz }.eachCount() + val parts = + SUMMARY_PLURALS.mapNotNull { (clazz, pluralId) -> + counts[clazz]?.let { count -> quantityLabel(pluralId, count) } + } + val unknownCount = recipients.count { it.clazz !in SUMMARY_PLURALS } + val unknownPart = quantityLabel(R.plurals.share_view_recipient_summary_others, unknownCount) + + return (parts + listOfNotNull(unknownPart)).joinToString( + separator = stringResource(R.string.share_view_recipient_summary_separator) + ) +} + +@Composable +private fun quantityLabel(@PluralsRes pluralId: Int, count: Int): String? { + if (count == 0) return null + return pluralStringResource(pluralId, count, count) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt index 554ed7fb..92e6a21e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt @@ -22,21 +22,31 @@ enum class ShareItemType { @Composable fun getShape(): RoundedCornerShape = when (this) { - Single -> RoundedCornerShape(OUTER_CORNER_RADIUS) - Top -> RoundedCornerShape( - OUTER_CORNER_RADIUS, - OUTER_CORNER_RADIUS, - INNER_CORNER_RADIUS, - INNER_CORNER_RADIUS - ) - - Mid -> RoundedCornerShape(INNER_CORNER_RADIUS) - Bottom -> RoundedCornerShape( - INNER_CORNER_RADIUS, - INNER_CORNER_RADIUS, - OUTER_CORNER_RADIUS, - OUTER_CORNER_RADIUS - ) + Single -> { + RoundedCornerShape(OUTER_CORNER_RADIUS) + } + + Top -> { + RoundedCornerShape( + OUTER_CORNER_RADIUS, + OUTER_CORNER_RADIUS, + INNER_CORNER_RADIUS, + INNER_CORNER_RADIUS + ) + } + + Mid -> { + RoundedCornerShape(INNER_CORNER_RADIUS) + } + + Bottom -> { + RoundedCornerShape( + INNER_CORNER_RADIUS, + INNER_CORNER_RADIUS, + OUTER_CORNER_RADIUS, + OUTER_CORNER_RADIUS + ) + } } companion object { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt new file mode 100644 index 00000000..cef5c5a1 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient +import com.nextcloud.android.common.ui.share.model.api.share.Share + +data class ShareListItemActions( + val onSelectShare: (Share) -> Unit, + val onSendEmail: (Share) -> Unit, + val onToggleExpanded: (Share) -> Unit, + val onShowOverlay: (ShareOverlay) -> Unit, + val onRemoveRecipient: (Share, Recipient) -> Unit +) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt new file mode 100644 index 00000000..28fd4af0 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.ui + +import com.nextcloud.android.common.ui.share.model.api.share.Share + +data class ShareListItemState(val share: Share, val title: String, val type: ShareItemType, val isExpanded: Boolean) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt index 92242a11..16aa8866 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt @@ -18,28 +18,78 @@ sealed class ShareOverlay { } data class QuickShare(override val shareId: String) : ShareOverlay() + data class DeleteConfirmation(override val shareId: String) : ShareOverlay() + data class RecipientPermission( + override val shareId: String, + val recipientClass: String, + val recipientValue: String, + val recipientInstance: String? + ) : ShareOverlay() + companion object { private const val QUICK_SHARE = "quick_share" private const val DELETE_CONFIRMATION = "delete_confirmation" + private const val RECIPIENT_PERMISSION = "recipient_permission" + private const val RECIPIENT_CLASS_INDEX = 2 + private const val RECIPIENT_VALUE_INDEX = 3 + private const val RECIPIENT_INSTANCE_INDEX = 4 + + val Saver: Saver = + listSaver( + save = { overlay -> + when (overlay) { + None -> { + emptyList() + } + + is QuickShare -> { + listOf(QUICK_SHARE, overlay.shareId) + } + + is DeleteConfirmation -> { + listOf(DELETE_CONFIRMATION, overlay.shareId) + } + + is RecipientPermission -> { + listOf( + RECIPIENT_PERMISSION, + overlay.shareId, + overlay.recipientClass, + overlay.recipientValue, + overlay.recipientInstance.orEmpty() + ) + } + } + }, + restore = { saved -> + val shareId = saved.getOrNull(1) + when (saved.firstOrNull()) { + QUICK_SHARE -> { + shareId?.let(::QuickShare) + } + + DELETE_CONFIRMATION -> { + shareId?.let(::DeleteConfirmation) + } + + RECIPIENT_PERMISSION -> { + shareId?.let { + RecipientPermission( + shareId = it, + recipientClass = saved[RECIPIENT_CLASS_INDEX], + recipientValue = saved[RECIPIENT_VALUE_INDEX], + recipientInstance = saved[RECIPIENT_INSTANCE_INDEX].takeIf(String::isNotEmpty) + ) + } + } - val Saver: Saver = listSaver( - save = { overlay -> - when (overlay) { - None -> emptyList() - is QuickShare -> listOf(QUICK_SHARE, overlay.shareId) - is DeleteConfirmation -> listOf(DELETE_CONFIRMATION, overlay.shareId) + else -> { + None + } + } ?: None } - }, - restore = { saved -> - val shareId = saved.getOrNull(1) - when (saved.firstOrNull()) { - QUICK_SHARE -> shareId?.let(::QuickShare) - DELETE_CONFIRMATION -> shareId?.let(::DeleteConfirmation) - else -> None - } ?: None - } - ) + ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index 1a1fe517..ef96dc23 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -11,7 +11,10 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share sealed class ShareScreenState { data object Loading : ShareScreenState() + data object Empty : ShareScreenState() + data object Error : ShareScreenState() + data class Loaded(val shares: List) : ShareScreenState() } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 1fb3fc39..41ad2f52 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -22,6 +22,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest @@ -307,10 +308,7 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(share) } - override suspend fun fetchShare( - id: String, - request: GetShareRequest - ): NetworkResult { + override suspend fun fetchShare(id: String, request: GetShareRequest): NetworkResult { val share = mockShares.find { it.id == id } ?: return NetworkResult.Success( buildShare(id = id, sources = emptyList(), recipients = emptyList()) @@ -341,23 +339,31 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(result) } - override suspend fun updateShareState( - id: String, - request: UpdateShareStateRequest - ): NetworkResult { + override suspend fun updateShareState(id: String, request: UpdateShareStateRequest): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val updated = (if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList())) + val updated = ( + if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } + ) .copy(shareState = request.shareState, lastUpdated = System.currentTimeMillis()) if (index >= 0) mockShares[index] = updated return NetworkResult.Success(updated) } - override suspend fun addShareSource( - id: String, - request: AddSourceRequest - ): NetworkResult { + override suspend fun addShareSource(id: String, request: AddSourceRequest): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val current = if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } val newSource = Source(clazz = request.clazz, value = request.value, displayName = request.value) val updated = current.copy( sources = current.sources + newSource, @@ -367,13 +373,15 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(updated) } - override suspend fun removeShareSource( - id: String, - clazz: String, - value: String - ): NetworkResult { + override suspend fun removeShareSource(id: String, clazz: String, value: String): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val current = if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } val updated = current.copy( sources = current.sources.filterNot { it.clazz == clazz && it.value == value }, lastUpdated = System.currentTimeMillis() @@ -382,12 +390,15 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(updated) } - override suspend fun addShareRecipient( - id: String, - request: AddRecipientRequest - ): NetworkResult { + override suspend fun addShareRecipient(id: String, request: AddRecipientRequest): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val current = if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } val newRecipient = Recipient( clazz = request.clazz, value = request.value, @@ -410,7 +421,13 @@ class MockShareRepository : ShareRepository { instance: String? ): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val current = if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } val updated = current.copy( recipients = current.recipients.filterNot { it.clazz == clazz && it.value == value && it.instance == instance @@ -421,12 +438,15 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(updated) } - override suspend fun updateShareProperty( - id: String, - request: UpdateSharePropertyRequest - ): NetworkResult { + override suspend fun updateShareProperty(id: String, request: UpdateSharePropertyRequest): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val current = if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } val updated = current.copy(lastUpdated = System.currentTimeMillis()) if (index >= 0) mockShares[index] = updated return NetworkResult.Success(updated) @@ -437,7 +457,13 @@ class MockShareRepository : ShareRepository { request: UpdateShareRecipientSecretRequest ): NetworkResult { val index = mockShares.indexOfFirst { it.id == id } - val current = if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val current = if (index >= + 0 + ) { + mockShares[index] + } else { + buildShare(id = id, sources = emptyList(), recipients = emptyList()) + } return NetworkResult.Success(current) } @@ -461,6 +487,36 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(updated) } + override suspend fun updateShareRecipientPermission( + id: String, + request: UpdateShareRecipientPermissionRequest + ): NetworkResult { + val index = mockShares.indexOfFirst { it.id == id } + val current = + if (index >= 0) mockShares[index] else buildShare(id = id, sources = emptyList(), recipients = emptyList()) + val updatedRecipients = current.recipients.map { recipient -> + val isTarget = recipient.clazz == request.recipientClass && + recipient.value == request.recipientValue && + recipient.instance == request.recipientInstance + if (!isTarget) return@map recipient + + val override = current.permissions + .firstOrNull { it.clazz == request.permissionClass } + ?.copy(enabled = request.enabled) + ?: return@map recipient + + recipient.copy( + permissions = recipient.permissions.filterNot { it.clazz == override.clazz } + override + ) + } + val updated = current.copy( + recipients = updatedRecipients, + lastUpdated = System.currentTimeMillis() + ) + if (index >= 0) mockShares[index] = updated + return NetworkResult.Success(updated) + } + override suspend fun updateSharePermissionPreset( id: String, request: UpdateSharePermissionPresetRequest @@ -476,13 +532,12 @@ class MockShareRepository : ShareRepository { return NetworkResult.Success(updated) } - override suspend fun fetchSharingCapabilities(): NetworkResult = - NetworkResult.Success( - SharingCapabilities( - permissionPresets = listOf( - PermissionPreset(clazz = "view", displayName = "Can view"), - PermissionPreset(clazz = "edit", displayName = "Can edit") - ) + override suspend fun fetchSharingCapabilities(): NetworkResult = NetworkResult.Success( + SharingCapabilities( + permissionPresets = listOf( + PermissionPreset(clazz = "view", displayName = "Can view"), + PermissionPreset(clazz = "edit", displayName = "Can edit") ) ) + ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index a525ddd6..4eb4ef62 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -20,6 +20,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest @@ -38,7 +39,6 @@ class ShareRemoteRepository( private val client: NextcloudHttpClient, private val json: kotlinx.serialization.json.Json = OCSSerializer.json ) : ShareRepository { - private companion object { private const val SHARE_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/share" private const val SHARES_ENDPOINT = "/ocs/v2.php/apps/sharing/api/v1/shares" @@ -49,8 +49,7 @@ class ShareRemoteRepository( private const val NODE_SHARING = "sharing" } - private fun decodeShare(body: String): Share = - json.decodeFromString>(body).ocs.data + private fun decodeShare(body: String): Share = json.decodeFromString>(body).ocs.data override suspend fun fetchRecipients( recipientTypeClasses: List?, @@ -58,10 +57,11 @@ class ShareRemoteRepository( limit: Int, offset: Int ): NetworkResult> { - val queryParams = buildString { - append("?query=${query.urlEncoded()}&limit=$limit&offset=$offset") - recipientTypeClasses?.forEach { append("&recipientTypeClasses[]=${it.urlEncoded()}") } - } + val queryParams = + buildString { + append("?query=${query.urlEncoded()}&limit=$limit&offset=$offset") + recipientTypeClasses?.forEach { append("&recipientTypeClasses[]=${it.urlEncoded()}") } + } return client.executeRequest( endpoint = "$RECIPIENTS_ENDPOINT$queryParams", method = HttpMethod.GET @@ -70,28 +70,22 @@ class ShareRemoteRepository( } } - override suspend fun createDraftShare(): NetworkResult = - client.executeRequest( - endpoint = SHARE_ENDPOINT, - method = HttpMethod.POST, - body = ByteArray(0).toRequestBody() - ) { decodeShare(it) } + override suspend fun createDraftShare(): NetworkResult = client.executeRequest( + endpoint = SHARE_ENDPOINT, + method = HttpMethod.POST, + body = ByteArray(0).toRequestBody() + ) { decodeShare(it) } - override suspend fun fetchShare( - id: String, - request: GetShareRequest - ): NetworkResult = - client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id", - method = HttpMethod.POST, - body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { decodeShare(it) } + override suspend fun fetchShare(id: String, request: GetShareRequest): NetworkResult = client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id", + method = HttpMethod.POST, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { decodeShare(it) } - override suspend fun deleteShare(id: String): NetworkResult = - client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id", - method = HttpMethod.DELETE - ) { } + override suspend fun deleteShare(id: String): NetworkResult = client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id", + method = HttpMethod.DELETE + ) { } override suspend fun fetchShares( filterSourceTypeValue: String, @@ -100,13 +94,14 @@ class ShareRemoteRepository( filterState: ShareState?, lastShareID: String? ): NetworkResult> { - val queryParams = buildString { - append("?filterSourceTypeValue=${filterSourceTypeValue.urlEncoded()}") - limit?.let { append("&limit=$it") } - filterSourceTypeClass?.let { append("&filterSourceTypeClass=${it.urlEncoded()}") } - filterState?.let { append("&filterState=${it.apiValue().urlEncoded()}") } - lastShareID?.let { append("&lastShareID=${it.urlEncoded()}") } - } + val queryParams = + buildString { + append("?filterSourceTypeValue=${filterSourceTypeValue.urlEncoded()}") + limit?.let { append("&limit=$it") } + filterSourceTypeClass?.let { append("&filterSourceTypeClass=${it.urlEncoded()}") } + filterState?.let { append("&filterState=${it.apiValue().urlEncoded()}") } + lastShareID?.let { append("&lastShareID=${it.urlEncoded()}") } + } return client.executeRequest( endpoint = "$SHARES_ENDPOINT$queryParams", method = HttpMethod.GET @@ -115,40 +110,27 @@ class ShareRemoteRepository( } } - override suspend fun updateShareState( - id: String, - request: UpdateShareStateRequest - ): NetworkResult = + override suspend fun updateShareState(id: String, request: UpdateShareStateRequest): NetworkResult = client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/state", method = HttpMethod.PUT, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) ) { decodeShare(it) } - override suspend fun addShareSource( - id: String, - request: AddSourceRequest - ): NetworkResult = + override suspend fun addShareSource(id: String, request: AddSourceRequest): NetworkResult = client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/source", method = HttpMethod.POST, body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) ) { decodeShare(it) } - override suspend fun removeShareSource( - id: String, - clazz: String, - value: String - ): NetworkResult = + override suspend fun removeShareSource(id: String, clazz: String, value: String): NetworkResult = client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/source?class=${clazz.urlEncoded()}&value=${value.urlEncoded()}", method = HttpMethod.DELETE ) { decodeShare(it) } - override suspend fun addShareRecipient( - id: String, - request: AddRecipientRequest - ): NetworkResult = + override suspend fun addShareRecipient(id: String, request: AddRecipientRequest): NetworkResult = client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/recipient", method = HttpMethod.POST, @@ -161,20 +143,18 @@ class ShareRemoteRepository( value: String, instance: String? ): NetworkResult { - val queryParams = buildString { - append("?class=${clazz.urlEncoded()}&value=${value.urlEncoded()}") - instance?.let { append("&instance=${it.urlEncoded()}") } - } + val queryParams = + buildString { + append("?class=${clazz.urlEncoded()}&value=${value.urlEncoded()}") + instance?.let { append("&instance=${it.urlEncoded()}") } + } return client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/recipient$queryParams", method = HttpMethod.DELETE ) { decodeShare(it) } } - override suspend fun updateShareProperty( - id: String, - request: UpdateSharePropertyRequest - ): NetworkResult = + override suspend fun updateShareProperty(id: String, request: UpdateSharePropertyRequest): NetworkResult = client.executeRequest( endpoint = "$SHARE_ENDPOINT/$id/property", method = HttpMethod.PUT, @@ -184,48 +164,56 @@ class ShareRemoteRepository( override suspend fun updateSharePermission( id: String, request: UpdateSharePermissionRequest - ): NetworkResult = - client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id/permission", - method = HttpMethod.PUT, - body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { decodeShare(it) } + ): NetworkResult = client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/permission", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { decodeShare(it) } + + override suspend fun updateShareRecipientPermission( + id: String, + request: UpdateShareRecipientPermissionRequest + ): NetworkResult = client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/recipient/permission", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { decodeShare(it) } override suspend fun updateSharePermissionPreset( id: String, request: UpdateSharePermissionPresetRequest - ): NetworkResult = - client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id/permission/preset", - method = HttpMethod.PUT, - body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { decodeShare(it) } + ): NetworkResult = client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/permission/preset", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { decodeShare(it) } override suspend fun updateShareRecipientSecret( id: String, request: UpdateShareRecipientSecretRequest - ): NetworkResult = - client.executeRequest( - endpoint = "$SHARE_ENDPOINT/$id/recipient/secret", - method = HttpMethod.PUT, - body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) - ) { decodeShare(it) } - - override suspend fun generateSecret(): NetworkResult = - client.executeRequest( - endpoint = SECRET_ENDPOINT, - method = HttpMethod.GET - ) { body -> - json.decodeFromString>(body).ocs.data - } + ): NetworkResult = client.executeRequest( + endpoint = "$SHARE_ENDPOINT/$id/recipient/secret", + method = HttpMethod.PUT, + body = json.encodeToString(request).toRequestBody(JSON_CONTENT_TYPE) + ) { decodeShare(it) } + + override suspend fun generateSecret(): NetworkResult = client.executeRequest( + endpoint = SECRET_ENDPOINT, + method = HttpMethod.GET + ) { body -> + json.decodeFromString>(body).ocs.data + } - override suspend fun fetchSharingCapabilities(): NetworkResult = - client.executeRequest( - endpoint = CAPABILITIES_ENDPOINT, - method = HttpMethod.GET - ) { body -> - val sharing = json.decodeFromString>(body) - .ocs.data[NODE_CAPABILITIES]?.jsonObject?.get(NODE_SHARING) - sharing?.let { json.decodeFromJsonElement(it) } ?: SharingCapabilities() - } + override suspend fun fetchSharingCapabilities(): NetworkResult = client.executeRequest( + endpoint = CAPABILITIES_ENDPOINT, + method = HttpMethod.GET + ) { body -> + val sharing = + json + .decodeFromString>(body) + .ocs.data[NODE_CAPABILITIES] + ?.jsonObject + ?.get(NODE_SHARING) + sharing?.let { json.decodeFromJsonElement(it) } ?: SharingCapabilities() + } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index f378f9d1..c7918d1e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -14,6 +14,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest @@ -21,7 +22,6 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState interface ShareRepository { - suspend fun fetchRecipients( recipientTypeClasses: List?, query: String, @@ -31,10 +31,7 @@ interface ShareRepository { suspend fun createDraftShare(): NetworkResult - suspend fun fetchShare( - id: String, - request: GetShareRequest = GetShareRequest() - ): NetworkResult + suspend fun fetchShare(id: String, request: GetShareRequest = GetShareRequest()): NetworkResult suspend fun deleteShare(id: String): NetworkResult @@ -46,26 +43,13 @@ interface ShareRepository { lastShareID: String? = null ): NetworkResult> - suspend fun updateShareState( - id: String, - request: UpdateShareStateRequest - ): NetworkResult + suspend fun updateShareState(id: String, request: UpdateShareStateRequest): NetworkResult - suspend fun addShareSource( - id: String, - request: AddSourceRequest - ): NetworkResult + suspend fun addShareSource(id: String, request: AddSourceRequest): NetworkResult - suspend fun removeShareSource( - id: String, - clazz: String, - value: String - ): NetworkResult + suspend fun removeShareSource(id: String, clazz: String, value: String): NetworkResult - suspend fun addShareRecipient( - id: String, - request: AddRecipientRequest - ): NetworkResult + suspend fun addShareRecipient(id: String, request: AddRecipientRequest): NetworkResult suspend fun removeShareRecipient( id: String, @@ -74,14 +58,13 @@ interface ShareRepository { instance: String? = null ): NetworkResult - suspend fun updateShareProperty( - id: String, - request: UpdateSharePropertyRequest - ): NetworkResult + suspend fun updateShareProperty(id: String, request: UpdateSharePropertyRequest): NetworkResult + + suspend fun updateSharePermission(id: String, request: UpdateSharePermissionRequest): NetworkResult - suspend fun updateSharePermission( + suspend fun updateShareRecipientPermission( id: String, - request: UpdateSharePermissionRequest + request: UpdateShareRecipientPermissionRequest ): NetworkResult suspend fun updateSharePermissionPreset( @@ -89,10 +72,7 @@ interface ShareRepository { request: UpdateSharePermissionPresetRequest ): NetworkResult - suspend fun updateShareRecipientSecret( - id: String, - request: UpdateShareRecipientSecretRequest - ): NetworkResult + suspend fun updateShareRecipientSecret(id: String, request: UpdateShareRecipientSecretRequest): NetworkResult suspend fun generateSecret(): NetworkResult diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt index 3b746273..3436fd5b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt @@ -11,9 +11,7 @@ import androidx.lifecycle.SavedStateHandle import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import kotlinx.coroutines.flow.StateFlow -class ShareSavedState( - private val handle: SavedStateHandle -) { +class ShareSavedState(private val handle: SavedStateHandle) { companion object { private const val KEY_ACTIVE_SHARE_ID = "active_share_id" private const val KEY_EDITOR_ENTRY = "editor_entry" diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 363c45a4..114cd9b5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -20,6 +20,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientReque import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest @@ -30,6 +31,7 @@ import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.ShareScreenState +import com.nextcloud.android.common.ui.share.model.ui.recipientPermissionChanges import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -58,7 +60,6 @@ class ShareViewModel( private val sourceId: String, savedStateHandle: SavedStateHandle ) : ViewModel() { - companion object { private const val SEARCH_DEBOUNCE_DELAY = 300L private const val SEARCH_SUBSCRIPTION_TIMEOUT = 5_000L @@ -107,12 +108,13 @@ class ShareViewModel( val searchQuery: StateFlow = savedState.searchQuery @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) - val recipientSearchResults: StateFlow> = searchQuery - .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) - .distinctUntilChanged() - .filter { it.isNotBlank() } - .mapLatest { query -> searchRecipients(query) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SEARCH_SUBSCRIPTION_TIMEOUT), emptyList()) + val recipientSearchResults: StateFlow> = + searchQuery + .debounce(SEARCH_DEBOUNCE_DELAY.milliseconds) + .distinctUntilChanged() + .filter { it.isNotBlank() } + .mapLatest { query -> searchRecipients(query) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(SEARCH_SUBSCRIPTION_TIMEOUT), emptyList()) private var secretUpdateJob: Job? = null @@ -241,11 +243,12 @@ class ShareViewModel( } private fun onDraftCreationFailed(result: NetworkResult) { - val messageId = if (result.isRateLimited) { - R.string.share_view_rate_limited_message - } else { - R.string.share_view_create_error_message - } + val messageId = + if (result.isRateLimited) { + R.string.share_view_rate_limited_message + } else { + R.string.share_view_create_error_message + } _errorMessageId.update { messageId } } @@ -300,8 +303,9 @@ class ShareViewModel( // region sources private suspend fun applySource(id: String, value: String) { val result = repository.addShareSource(id, AddSourceRequest(Source.NODE_SOURCE_CLASS, value)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } @@ -311,8 +315,9 @@ class ShareViewModel( fun addRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch { val result = repository.addShareRecipient(id, AddRecipientRequest(clazz, value, instance)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } @@ -347,8 +352,9 @@ class ShareViewModel( fun removeRecipient(id: String, clazz: String, value: String, instance: String? = null) { viewModelScope.launch { val result = repository.removeShareRecipient(id, clazz, value, instance) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } @@ -356,19 +362,22 @@ class ShareViewModel( fun updateRecipientSecret(shareId: String, recipient: Recipient, secret: String) { secretUpdateJob?.cancel() - secretUpdateJob = viewModelScope.launch { - val request = UpdateShareRecipientSecretRequest( - clazz = recipient.clazz, - value = recipient.value, - instance = recipient.instance, - secret = secret - ) - val result = repository.updateShareRecipientSecret(shareId, request) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch - refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - } + secretUpdateJob = + viewModelScope.launch { + val request = + UpdateShareRecipientSecretRequest( + clazz = recipient.clazz, + value = recipient.value, + instance = recipient.instance, + secret = secret + ) + val result = repository.updateShareRecipientSecret(shareId, request) + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch + refreshActiveShare(updated.toActiveShare()) + replaceInList(updated) + } } suspend fun generateSecret(): String? { @@ -385,37 +394,79 @@ class ShareViewModel( if (value.isNullOrEmpty()) { _propertyErrors.update { it - clazz } } - propertyUpdateJobs[clazz] = viewModelScope.launch { - when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { - is NetworkResult.Success -> { - _propertyErrors.update { it - clazz } - refreshActiveShare(result.data.toActiveShare()) - replaceInList(result.data) - } + propertyUpdateJobs[clazz] = + viewModelScope.launch { + when (val result = repository.updateShareProperty(shareId, UpdateSharePropertyRequest(clazz, value))) { + is NetworkResult.Success -> { + _propertyErrors.update { it - clazz } + refreshActiveShare(result.data.toActiveShare()) + replaceInList(result.data) + } - is NetworkResult.ServerError -> { - if (!value.isNullOrEmpty()) { - _propertyErrors.update { it + (clazz to result.response.ocs.data) } + is NetworkResult.ServerError -> { + if (!value.isNullOrEmpty()) { + _propertyErrors.update { it + (clazz to result.response.ocs.data) } + } } - } - is NetworkResult.NetworkException -> { - if (!value.isNullOrEmpty()) { - _propertyErrors.update { it + (clazz to null) } + is NetworkResult.NetworkException -> { + if (!value.isNullOrEmpty()) { + _propertyErrors.update { it + (clazz to null) } + } } } + _pendingProperties.update { it - clazz } } - _pendingProperties.update { it - clazz } - } } // endregion // region permissions + fun updateRecipientPermission(shareId: String, recipient: Recipient, clazz: String, enabled: Boolean) { + viewModelScope.launch { applyRecipientPermission(shareId, recipient, clazz, enabled) } + } + + fun updateRecipientPermissionPreset(shareId: String, recipient: Recipient, presetClass: String) { + val share = currentShares.firstOrNull { it.id == shareId } ?: return + val changes = share.recipientPermissionChanges(recipient, presetClass) + if (changes.isEmpty()) return + + viewModelScope.launch { + changes.forEach { (clazz, enabled) -> + applyRecipientPermission(shareId, recipient, clazz, enabled) ?: return@launch + } + } + } + + private suspend fun applyRecipientPermission( + shareId: String, + recipient: Recipient, + clazz: String, + enabled: Boolean + ): Share? { + val request = + UpdateShareRecipientPermissionRequest( + recipientClass = recipient.clazz, + recipientValue = recipient.value, + recipientInstance = recipient.instance, + permissionClass = clazz, + enabled = enabled + ) + val result = repository.updateShareRecipientPermission(shareId, request) + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return null + + refreshActiveShare(updated.toActiveShare()) + replaceInList(updated) + return updated + } + fun updatePermission(id: String, clazz: String, enabled: Boolean) { viewModelScope.launch { val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch refreshActiveShare(updated.toActiveShare()) replaceInList(updated) } @@ -424,8 +475,9 @@ class ShareViewModel( fun updatePermissionPreset(id: String, presetClass: String, updateActiveShare: Boolean) { viewModelScope.launch { val result = repository.updateSharePermissionPreset(id, UpdateSharePermissionPresetRequest(presetClass)) - val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return@launch + val updated = + result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + ?: return@launch if (updateActiveShare) { refreshActiveShare(updated.toActiveShare()) } @@ -497,12 +549,13 @@ class ShareViewModel( val index = current.indexOfFirst { it.id == updated.id } val isActive = updated.shareState == ShareState.ACTIVE - val shares = when { - index >= 0 && isActive -> current.toMutableList().apply { this[index] = updated } - index >= 0 -> current.filterNot { it.id == updated.id } - isActive -> listOf(updated) + current - else -> return - } + val shares = + when { + index >= 0 && isActive -> current.toMutableList().apply { this[index] = updated } + index >= 0 -> current.filterNot { it.id == updated.id } + isActive -> listOf(updated) + current + else -> return + } publishShares(shares) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt index cea0cdda..728e9049 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt @@ -14,15 +14,10 @@ import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.CreationExtras import com.nextcloud.android.common.ui.share.repository.ShareRepository -class ShareViewModelFactory( - private val sourceId: String, - private val repositoryProvider: () -> ShareRepository -) : ViewModelProvider.Factory { +class ShareViewModelFactory(private val sourceId: String, private val repositoryProvider: () -> ShareRepository) : + ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: Class, - extras: CreationExtras - ): T { + override fun create(modelClass: Class, extras: CreationExtras): T { require(modelClass.isAssignableFrom(ShareViewModel::class.java)) { "ShareViewModelFactory cannot create ${modelClass.name}" } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt index b9a3af7a..32c626c0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/MaterialSchemes.kt @@ -25,17 +25,13 @@ interface MaterialSchemes { companion object { fun fromServerTheme(theme: ServerTheme): MaterialSchemes = MaterialSchemesImpl(theme) - fun fromColor( - @ColorInt color: Int - ): MaterialSchemes = MaterialSchemesImpl(color) + fun fromColor(@ColorInt color: Int): MaterialSchemes = MaterialSchemesImpl(color) /** * Creates a [MaterialSchemes] where the primary color is preserved as-is. This is useful for views where * we want it for branding purposes, especially as a background color. The resulting dark and light * schemes will be the same, as they are derived from the primary color and not the device theme. */ - fun withPrimaryAsBackground( - @ColorInt primary: Int - ): MaterialSchemes = MaterialSchemesImpl(primary, true) + fun withPrimaryAsBackground(@ColorInt primary: Int): MaterialSchemes = MaterialSchemesImpl(primary, true) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt index 7a0c9de5..82c1d6ef 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt @@ -15,9 +15,7 @@ import com.nextcloud.android.common.ui.util.PlatformThemeUtil import com.nextcloud.android.common.ui.util.extensions.toColorScheme import dynamiccolor.DynamicScheme -open class ViewThemeUtilsBase( - private val schemes: MaterialSchemes -) { +open class ViewThemeUtilsBase(private val schemes: MaterialSchemes) { /** * Scheme for painting elements */ @@ -34,15 +32,9 @@ open class ViewThemeUtilsBase( else -> schemes.lightScheme } - protected fun withScheme( - view: View, - block: (DynamicScheme) -> R - ): R = block(getSchemeInternal(view.context)) + protected fun withScheme(view: View, block: (DynamicScheme) -> R): R = block(getSchemeInternal(view.context)) - protected fun withScheme( - context: Context, - block: (DynamicScheme) -> R - ): R = block(getSchemeInternal(context)) + protected fun withScheme(context: Context, block: (DynamicScheme) -> R): R = block(getSchemeInternal(context)) protected fun withSchemeDark(block: (DynamicScheme) -> R): R = block(schemes.darkScheme) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt index 5a611067..5731b58e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt @@ -62,649 +62,571 @@ import javax.inject.Inject */ @Suppress("TooManyFunctions") class AndroidViewThemeUtils - @Inject - constructor( - schemes: MaterialSchemes, - private val colorUtil: ColorUtil - ) : ViewThemeUtilsBase(schemes) { - private val dynamicColor = MaterialDynamicColors() - - fun colorBottomNavigationView(bottomNavigationView: BottomNavigationView) { - withScheme(bottomNavigationView) { scheme -> - bottomNavigationView.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) - - bottomNavigationView.itemIconTintList = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme) - ) +@Inject +constructor(schemes: MaterialSchemes, private val colorUtil: ColorUtil) : + ViewThemeUtilsBase(schemes) { + private val dynamicColor = MaterialDynamicColors() + + fun colorBottomNavigationView(bottomNavigationView: BottomNavigationView) { + withScheme(bottomNavigationView) { scheme -> + bottomNavigationView.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) + + bottomNavigationView.itemIconTintList = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme) + ) - bottomNavigationView.itemTextColor = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.onSurface().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme) - ) + bottomNavigationView.itemTextColor = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSurface().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme) + ) - bottomNavigationView.itemActiveIndicatorColor = - ColorStateList - .valueOf(dynamicColor.secondaryContainer().getArgb(scheme)) - } + bottomNavigationView.itemActiveIndicatorColor = + ColorStateList + .valueOf(dynamicColor.secondaryContainer().getArgb(scheme)) } + } - @JvmOverloads - fun colorViewBackground( - view: View, - colorRole: ColorRole = ColorRole.SURFACE - ) { - withScheme(view) { scheme -> - view.setBackgroundColor(colorRole.select(scheme)) - } + @JvmOverloads + fun colorViewBackground(view: View, colorRole: ColorRole = ColorRole.SURFACE) { + withScheme(view) { scheme -> + view.setBackgroundColor(colorRole.select(scheme)) } + } - @JvmOverloads - fun colorNavigationView( - navigationView: NavigationView, - colorIcons: Boolean = true - ) { - withScheme(navigationView) { scheme -> - if (navigationView.itemBackground != null) { - navigationView.itemBackground!!.setTintList( - buildColorStateList( - android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to Color.TRANSPARENT - ) - ) - } - navigationView.background.setTintList(ColorStateList.valueOf(dynamicColor.surface().getArgb(scheme))) - - val colorStateList = + @JvmOverloads + fun colorNavigationView(navigationView: NavigationView, colorIcons: Boolean = true) { + withScheme(navigationView) { scheme -> + if (navigationView.itemBackground != null) { + navigationView.itemBackground!!.setTintList( buildColorStateList( - android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme) + android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to Color.TRANSPARENT ) - - navigationView.itemTextColor = colorStateList - if (colorIcons) { - navigationView.itemIconTintList = colorStateList - } + ) } - } + navigationView.background.setTintList(ColorStateList.valueOf(dynamicColor.surface().getArgb(scheme))) - fun getPrimaryColorDrawable(context: Context): Drawable = - withScheme(context) { scheme -> - ColorDrawable(dynamicColor.primary().getArgb(scheme)) - } + val colorStateList = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme) + ) - fun colorToolbarMenuIcon( - context: Context, - item: MenuItem - ) { - withScheme(context) { scheme -> - colorMenuItemIcon(dynamicColor.onSurfaceVariant().getArgb(scheme), item) + navigationView.itemTextColor = colorStateList + if (colorIcons) { + navigationView.itemIconTintList = colorStateList } } + } - fun colorMenuItemText( - context: Context, - item: MenuItem - ) { - withScheme(context) { scheme: DynamicScheme -> - colorMenuItemText(dynamicColor.onSurface().getArgb(scheme), item) - } - } + fun getPrimaryColorDrawable(context: Context): Drawable = withScheme(context) { scheme -> + ColorDrawable(dynamicColor.primary().getArgb(scheme)) + } - private fun colorMenuItemIcon( - @ColorInt color: Int, - item: MenuItem - ) { - item.icon?.setTint(color) + fun colorToolbarMenuIcon(context: Context, item: MenuItem) { + withScheme(context) { scheme -> + colorMenuItemIcon(dynamicColor.onSurfaceVariant().getArgb(scheme), item) } + } - private fun colorMenuItemText( - @ColorInt color: Int, - item: MenuItem - ) { - val newItemTitle = SpannableString(item.title) - newItemTitle.setSpan( - ForegroundColorSpan(color), - 0, - newItemTitle.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - item.title = newItemTitle - } - - @JvmOverloads - fun tintDrawable( - context: Context, - @DrawableRes id: Int, - colorRole: ColorRole = ColorRole.PRIMARY - ): Drawable? { - val drawable = ResourcesCompat.getDrawable(context.resources, id, null) - return drawable?.let { - tintDrawable(context, it, colorRole) - } + fun colorMenuItemText(context: Context, item: MenuItem) { + withScheme(context) { scheme: DynamicScheme -> + colorMenuItemText(dynamicColor.onSurface().getArgb(scheme), item) } + } - @Deprecated( - replaceWith = - ReplaceWith( - "tintDrawable(context, id, ColorRole.PRIMARY)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use tintDrawable(context, id, ColorRole.PRIMARY) instead" - ) - fun tintPrimaryDrawable( - context: Context, - @DrawableRes id: Int - ): Drawable? = tintDrawable(context, id, ColorRole.PRIMARY) - - @JvmOverloads - fun tintDrawable( - context: Context, - drawable: Drawable, - colorRole: ColorRole = ColorRole.PRIMARY - ): Drawable = - withScheme(context) { scheme: DynamicScheme -> - colorDrawable(drawable, colorRole.select(scheme)) - } + private fun colorMenuItemIcon(@ColorInt color: Int, item: MenuItem) { + item.icon?.setTint(color) + } - @Deprecated( - replaceWith = - ReplaceWith( - "tintDrawable(context, drawable, ColorRole.PRIMARY)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use tintDrawable(context, drawable, ColorRole.PRIMARY) instead" + private fun colorMenuItemText(@ColorInt color: Int, item: MenuItem) { + val newItemTitle = SpannableString(item.title) + newItemTitle.setSpan( + ForegroundColorSpan(color), + 0, + newItemTitle.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) - fun tintPrimaryDrawable( - context: Context, - drawable: Drawable? - ): Drawable? = drawable?.let { tintDrawable(context, it, ColorRole.PRIMARY) } - - @Deprecated( - replaceWith = - ReplaceWith( - "tintDrawable(context, drawable, ColorRole.ON_SURFACE)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use tintDrawable(context, drawable, ColorRole.ON_SURFACE) instead" - ) - fun tintTextDrawable( - context: Context, - drawable: Drawable? - ): Drawable? = drawable?.let { tintDrawable(context, it, ColorRole.ON_SURFACE) } - - /** - * Public for edge cases. For most cases use [tintDrawable] instead - */ - fun colorDrawable( - drawable: Drawable, - @ColorInt color: Int - ): Drawable { + item.title = newItemTitle + } + + @JvmOverloads + fun tintDrawable(context: Context, @DrawableRes id: Int, colorRole: ColorRole = ColorRole.PRIMARY): Drawable? { + val drawable = ResourcesCompat.getDrawable(context.resources, id, null) + return drawable?.let { + tintDrawable(context, it, colorRole) + } + } + + @Deprecated( + replaceWith = + ReplaceWith( + "tintDrawable(context, id, ColorRole.PRIMARY)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use tintDrawable(context, id, ColorRole.PRIMARY) instead" + ) + fun tintPrimaryDrawable(context: Context, @DrawableRes id: Int): Drawable? = + tintDrawable(context, id, ColorRole.PRIMARY) + + @JvmOverloads + fun tintDrawable(context: Context, drawable: Drawable, colorRole: ColorRole = ColorRole.PRIMARY): Drawable = + withScheme(context) { scheme: DynamicScheme -> + colorDrawable(drawable, colorRole.select(scheme)) + } + + @Deprecated( + replaceWith = + ReplaceWith( + "tintDrawable(context, drawable, ColorRole.PRIMARY)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use tintDrawable(context, drawable, ColorRole.PRIMARY) instead" + ) + fun tintPrimaryDrawable(context: Context, drawable: Drawable?): Drawable? = + drawable?.let { tintDrawable(context, it, ColorRole.PRIMARY) } + + @Deprecated( + replaceWith = + ReplaceWith( + "tintDrawable(context, drawable, ColorRole.ON_SURFACE)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use tintDrawable(context, drawable, ColorRole.ON_SURFACE) instead" + ) + fun tintTextDrawable(context: Context, drawable: Drawable?): Drawable? = + drawable?.let { tintDrawable(context, it, ColorRole.ON_SURFACE) } + + /** + * Public for edge cases. For most cases use [tintDrawable] instead + */ + fun colorDrawable(drawable: Drawable, @ColorInt color: Int): Drawable { + val wrap = DrawableCompat.wrap(drawable) + wrap.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + color, + BlendModeCompat.SRC_ATOP + ) + return wrap + } + + fun tintToolbarArrowDrawable(context: Context, drawerToggle: ActionBarDrawerToggle, drawable: Drawable) { + withScheme(context) { scheme: DynamicScheme -> val wrap = DrawableCompat.wrap(drawable) wrap.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - color, + dynamicColor.onSurface().getArgb(scheme), BlendModeCompat.SRC_ATOP ) - return wrap - } - - fun tintToolbarArrowDrawable( - context: Context, - drawerToggle: ActionBarDrawerToggle, - drawable: Drawable - ) { - withScheme(context) { scheme: DynamicScheme -> - val wrap = DrawableCompat.wrap(drawable) - wrap.colorFilter = - BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - dynamicColor.onSurface().getArgb(scheme), - BlendModeCompat.SRC_ATOP - ) - drawerToggle.setHomeAsUpIndicator(wrap) - drawerToggle.drawerArrowDrawable.color = dynamicColor.onSurface().getArgb(scheme) - } - } - - fun themeStatusBar(activity: Activity) { - themeStatusBar(activity, ColorRole.SURFACE) + drawerToggle.setHomeAsUpIndicator(wrap) + drawerToggle.drawerArrowDrawable.color = dynamicColor.onSurface().getArgb(scheme) } + } - fun themeStatusBar( - activity: Activity, - colorRole: ColorRole - ) { - withScheme(activity) { scheme -> - colorStatusBar(activity, colorRole.select(scheme)) - } - } + fun themeStatusBar(activity: Activity) { + themeStatusBar(activity, ColorRole.SURFACE) + } - /** - * Public for special cases, e.g. action mode. You probably want [themeStatusBar] for most cases instead. - */ - fun colorStatusBar( - activity: Activity, - @ColorInt color: Int - ) { - val window = activity.window ?: return - val isLightBackground = !colorUtil.isDarkBackground(color) - val decor = window.decorView - window.statusBarColor = color - window.navigationBarColor = color - WindowInsetsControllerCompat(window, decor).isAppearanceLightStatusBars = isLightBackground - WindowInsetsControllerCompat(window, decor).isAppearanceLightNavigationBars = isLightBackground - } - - fun resetStatusBar(activity: Activity) { - colorStatusBar( - activity, - ResourcesCompat.getColor( - activity.resources, - R.color.bg_default, - activity.theme - ) - ) + fun themeStatusBar(activity: Activity, colorRole: ColorRole) { + withScheme(activity) { scheme -> + colorStatusBar(activity, colorRole.select(scheme)) } + } - fun themeDialog(view: View) { - withScheme(view) { scheme -> - view.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) - } - } + /** + * Public for special cases, e.g. action mode. You probably want [themeStatusBar] for most cases instead. + */ + fun colorStatusBar(activity: Activity, @ColorInt color: Int) { + val window = activity.window ?: return + val isLightBackground = !colorUtil.isDarkBackground(color) + val decor = window.decorView + window.statusBarColor = color + window.navigationBarColor = color + WindowInsetsControllerCompat(window, decor).isAppearanceLightStatusBars = isLightBackground + WindowInsetsControllerCompat(window, decor).isAppearanceLightNavigationBars = isLightBackground + } - fun themeDialogDark(view: View) { - withSchemeDark { scheme -> - view.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) - } - } + fun resetStatusBar(activity: Activity) { + colorStatusBar( + activity, + ResourcesCompat.getColor( + activity.resources, + R.color.bg_default, + activity.theme + ) + ) + } - fun themeDialogDivider(view: View) { - withScheme(view) { scheme -> - view.setBackgroundColor(dynamicColor.surfaceVariant().getArgb(scheme)) - } + fun themeDialog(view: View) { + withScheme(view) { scheme -> + view.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) } + } - fun themeHorizontalSeekBar(seekBar: SeekBar) { - withScheme(seekBar) { scheme -> - themeHorizontalProgressBar(seekBar, dynamicColor.primary().getArgb(scheme)) - seekBar.thumb.colorFilter = - BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - dynamicColor.primary().getArgb(scheme), - BlendModeCompat.SRC_IN - ) - } + fun themeDialogDark(view: View) { + withSchemeDark { scheme -> + view.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) } + } - fun themeHorizontalProgressBar(progressBar: ProgressBar) { - withScheme(progressBar) { scheme -> - themeHorizontalProgressBar(progressBar, dynamicColor.primary().getArgb(scheme)) - } + fun themeDialogDivider(view: View) { + withScheme(view) { scheme -> + view.setBackgroundColor(dynamicColor.surfaceVariant().getArgb(scheme)) } + } - fun themeHorizontalProgressBar( - progressBar: ProgressBar?, - @ColorInt color: Int - ) { - progressBar?.indeterminateDrawable?.colorFilter = - BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - color, - BlendModeCompat.SRC_IN - ) - progressBar?.progressDrawable?.colorFilter = + fun themeHorizontalSeekBar(seekBar: SeekBar) { + withScheme(seekBar) { scheme -> + themeHorizontalProgressBar(seekBar, dynamicColor.primary().getArgb(scheme)) + seekBar.thumb.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - color, + dynamicColor.primary().getArgb(scheme), BlendModeCompat.SRC_IN ) } + } - @JvmOverloads - fun colorTextView( - textView: TextView, - colorRole: ColorRole = ColorRole.PRIMARY - ) { - withScheme(textView) { scheme -> - textView.setTextColor(colorRole.select(scheme)) - } + fun themeHorizontalProgressBar(progressBar: ProgressBar) { + withScheme(progressBar) { scheme -> + themeHorizontalProgressBar(progressBar, dynamicColor.primary().getArgb(scheme)) } + } - @JvmOverloads - fun colorTextViewLinks( - textView: TextView, - colorRole: ColorRole = ColorRole.PRIMARY - ) { - withScheme(textView.context) { scheme -> - textView.setLinkTextColor(colorRole.select(scheme)) - } + fun themeHorizontalProgressBar(progressBar: ProgressBar?, @ColorInt color: Int) { + progressBar?.indeterminateDrawable?.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + color, + BlendModeCompat.SRC_IN + ) + progressBar?.progressDrawable?.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + color, + BlendModeCompat.SRC_IN + ) + } + + @JvmOverloads + fun colorTextView(textView: TextView, colorRole: ColorRole = ColorRole.PRIMARY) { + withScheme(textView) { scheme -> + textView.setTextColor(colorRole.select(scheme)) } + } - @Deprecated( - replaceWith = ReplaceWith("colorTextView(textView)"), - message = "Use colorTextView(textView) instead" - ) - fun colorPrimaryTextViewElement(textView: TextView) { - colorTextView(textView, ColorRole.PRIMARY) + @JvmOverloads + fun colorTextViewLinks(textView: TextView, colorRole: ColorRole = ColorRole.PRIMARY) { + withScheme(textView.context) { scheme -> + textView.setLinkTextColor(colorRole.select(scheme)) } + } - @Deprecated( - replaceWith = - ReplaceWith( - "colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER) instead" - ) - fun colorOnSecondaryContainerTextViewElement(textView: TextView) { - colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER) + @Deprecated( + replaceWith = ReplaceWith("colorTextView(textView)"), + message = "Use colorTextView(textView) instead" + ) + fun colorPrimaryTextViewElement(textView: TextView) { + colorTextView(textView, ColorRole.PRIMARY) + } + + @Deprecated( + replaceWith = + ReplaceWith( + "colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER) instead" + ) + fun colorOnSecondaryContainerTextViewElement(textView: TextView) { + colorTextView(textView, ColorRole.ON_SECONDARY_CONTAINER) + } + + fun colorPrimaryTextViewElementDarkMode(textView: TextView) { + withSchemeDark { scheme -> + textView.setTextColor(dynamicColor.primary().getArgb(scheme)) + } + } + + @Deprecated( + replaceWith = ReplaceWith("colorViewBackground(view)"), + message = "Use colorViewBackground(view) instead" + ) + fun colorPrimaryView(view: View) { + withScheme(view) { scheme -> + view.setBackgroundColor(dynamicColor.primary().getArgb(scheme)) } + } - fun colorPrimaryTextViewElementDarkMode(textView: TextView) { - withSchemeDark { scheme -> - textView.setTextColor(dynamicColor.primary().getArgb(scheme)) - } + /** + * Colors the background as element color and the foreground as text color. + * + */ + @Deprecated( + replaceWith = ReplaceWith("colorImageViewBackgroundAndIcon"), + message = "Use colorImageViewBackgroundAndIcon, which has a better name, instead" + ) + fun colorImageViewButton(imageView: ImageView) { + colorImageViewBackgroundAndIcon(imageView) + } + + /** + * Colors the background as element color and the foreground as text color. + */ + fun colorImageViewBackgroundAndIcon(imageView: ImageView) { + withScheme(imageView) { scheme -> + imageView.imageTintList = ColorStateList.valueOf(dynamicColor.onPrimaryContainer().getArgb(scheme)) + imageView.backgroundTintList = ColorStateList.valueOf(dynamicColor.primaryContainer().getArgb(scheme)) } + } - @Deprecated( - replaceWith = ReplaceWith("colorViewBackground(view)"), - message = "Use colorViewBackground(view) instead" - ) - fun colorPrimaryView(view: View) { - withScheme(view) { scheme -> - view.setBackgroundColor(dynamicColor.primary().getArgb(scheme)) - } + fun themeImageButton(imageButton: ImageButton) { + withScheme(imageButton) { scheme -> + imageButton.imageTintList = + buildColorStateList( + android.R.attr.state_selected to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_selected to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_enabled to dynamicColor.onSurfaceVariant().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ) + ) } + } - /** - * Colors the background as element color and the foreground as text color. - * - */ - @Deprecated( - replaceWith = ReplaceWith("colorImageViewBackgroundAndIcon"), - message = "Use colorImageViewBackgroundAndIcon, which has a better name, instead" - ) - fun colorImageViewButton(imageView: ImageView) { - colorImageViewBackgroundAndIcon(imageView) + /** + * In most cases you'll want to use [themeImageButton] instead. + */ + fun colorImageButton(imageButton: ImageButton?, @ColorInt color: Int) { + imageButton?.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + + /** + * Tints the image with element color + */ + @Deprecated( + replaceWith = ReplaceWith("colorImageView(imageView, ColorRole.PRIMARY)"), + message = "Use colorImageView(imageView, ColorRole.PRIMARY) instead" + ) + fun colorImageView(imageView: ImageView) { + colorImageView(imageView, ColorRole.PRIMARY) + } + + fun colorImageView(imageView: ImageView, colorRole: ColorRole) { + withScheme(imageView) { scheme -> + imageView.imageTintList = ColorStateList.valueOf(colorRole.select(scheme)) } + } - /** - * Colors the background as element color and the foreground as text color. - */ - fun colorImageViewBackgroundAndIcon(imageView: ImageView) { - withScheme(imageView) { scheme -> - imageView.imageTintList = ColorStateList.valueOf(dynamicColor.onPrimaryContainer().getArgb(scheme)) - imageView.backgroundTintList = ColorStateList.valueOf(dynamicColor.primaryContainer().getArgb(scheme)) - } + fun colorTextButtons(vararg buttons: Button) { + withScheme(buttons[0]) { scheme -> + colorTextButtons(dynamicColor.primary().getArgb(scheme), *buttons) } + } - fun themeImageButton(imageButton: ImageButton) { - withScheme(imageButton) { scheme -> - imageButton.imageTintList = + /** + * In most cases you'll want to use [colorTextButtons] instead. + */ + fun colorTextButtons(@ColorInt color: Int, vararg buttons: Button) { + withScheme(buttons[0]) { scheme -> + for (button in buttons) { + button.setTextColor( buildColorStateList( - android.R.attr.state_selected to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_selected to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_enabled to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_enabled to color, -android.R.attr.state_enabled to colorUtil.adjustOpacity( dynamicColor.onSurface().getArgb(scheme), ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) + ) } } + } - /** - * In most cases you'll want to use [themeImageButton] instead. - */ - fun colorImageButton( - imageButton: ImageButton?, - @ColorInt color: Int - ) { - imageButton?.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) - } - - /** - * Tints the image with element color - */ - @Deprecated( - replaceWith = ReplaceWith("colorImageView(imageView, ColorRole.PRIMARY)"), - message = "Use colorImageView(imageView, ColorRole.PRIMARY) instead" - ) - fun colorImageView(imageView: ImageView) { - colorImageView(imageView, ColorRole.PRIMARY) - } - - fun colorImageView( - imageView: ImageView, - colorRole: ColorRole - ) { - withScheme(imageView) { scheme -> - imageView.imageTintList = ColorStateList.valueOf(colorRole.select(scheme)) - } - } - - fun colorTextButtons(vararg buttons: Button) { - withScheme(buttons[0]) { scheme -> - colorTextButtons(dynamicColor.primary().getArgb(scheme), *buttons) - } - } - - /** - * In most cases you'll want to use [colorTextButtons] instead. - */ - fun colorTextButtons( - @ColorInt color: Int, - vararg buttons: Button - ) { - withScheme(buttons[0]) { scheme -> - for (button in buttons) { - button.setTextColor( - buildColorStateList( - android.R.attr.state_enabled to color, - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ) - ) - ) - } - } - } + @Deprecated( + replaceWith = + ReplaceWith( + "colorCircularProgressBar(progressBar, ColorRole.ON_PRIMARY_CONTAINER)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use colorCircularProgressBar(progressBar, ColorRole.ON_PRIMARY_CONTAINER) instead" + ) + fun colorCircularProgressBarOnPrimaryContainer(progressBar: ProgressBar) { + colorCircularProgressBar(progressBar, ColorRole.ON_PRIMARY_CONTAINER) + } - @Deprecated( - replaceWith = - ReplaceWith( - "colorCircularProgressBar(progressBar, ColorRole.ON_PRIMARY_CONTAINER)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use colorCircularProgressBar(progressBar, ColorRole.ON_PRIMARY_CONTAINER) instead" - ) - fun colorCircularProgressBarOnPrimaryContainer(progressBar: ProgressBar) { - colorCircularProgressBar(progressBar, ColorRole.ON_PRIMARY_CONTAINER) - } + @Deprecated( + replaceWith = + ReplaceWith( + "colorCircularProgressBar(progressBar, ColorRole.PRIMARY)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use colorCircularProgressBar(progressBar, ColorRole.PRIMARY) instead" + ) + fun colorCircularProgressBar(progressBar: ProgressBar) { + colorCircularProgressBar(progressBar, ColorRole.PRIMARY) + } - @Deprecated( - replaceWith = - ReplaceWith( - "colorCircularProgressBar(progressBar, ColorRole.PRIMARY)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use colorCircularProgressBar(progressBar, ColorRole.PRIMARY) instead" - ) - fun colorCircularProgressBar(progressBar: ProgressBar) { - colorCircularProgressBar(progressBar, ColorRole.PRIMARY) - } + @Deprecated( + replaceWith = + ReplaceWith( + "colorCircularProgressBar(progressBar, ColorRole.ON_SURFACE_VARIANT)", + imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] + ), + message = "Use colorCircularProgressBar(progressBar, ColorRole.ON_SURFACE_VARIANT) instead" + ) + fun colorCircularProgressBarOnSurfaceVariant(progressBar: ProgressBar) { + colorCircularProgressBar(progressBar, ColorRole.ON_SURFACE_VARIANT) + } - @Deprecated( - replaceWith = - ReplaceWith( - "colorCircularProgressBar(progressBar, ColorRole.ON_SURFACE_VARIANT)", - imports = ["com.nextcloud.android.common.ui.theme.utils.ColorRole"] - ), - message = "Use colorCircularProgressBar(progressBar, ColorRole.ON_SURFACE_VARIANT) instead" - ) - fun colorCircularProgressBarOnSurfaceVariant(progressBar: ProgressBar) { - colorCircularProgressBar(progressBar, ColorRole.ON_SURFACE_VARIANT) - } - - fun colorCircularProgressBar( - progressBar: ProgressBar, - colorRole: ColorRole - ) { - withScheme(progressBar) { scheme -> - progressBar.indeterminateDrawable.colorFilter = - BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - colorRole.select(scheme), - BlendModeCompat.SRC_ATOP - ) - } + fun colorCircularProgressBar(progressBar: ProgressBar, colorRole: ColorRole) { + withScheme(progressBar) { scheme -> + progressBar.indeterminateDrawable.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + colorRole.select(scheme), + BlendModeCompat.SRC_ATOP + ) } + } - fun themeCheckedTextView(vararg checkedTextViews: CheckedTextView) { - withScheme(checkedTextViews[0]) { scheme -> - val colorStateList = - buildColorStateList( - -android.R.attr.state_checked to Color.GRAY, - -android.R.attr.state_enabled to Color.GRAY, - android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme) - ) + fun themeCheckedTextView(vararg checkedTextViews: CheckedTextView) { + withScheme(checkedTextViews[0]) { scheme -> + val colorStateList = + buildColorStateList( + -android.R.attr.state_checked to Color.GRAY, + -android.R.attr.state_enabled to Color.GRAY, + android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme) + ) - checkedTextViews.forEach { - it.checkMarkTintList = colorStateList - } + checkedTextViews.forEach { + it.checkMarkTintList = colorStateList } } + } - fun themeCheckbox(vararg checkboxes: CheckBox) { - withScheme(checkboxes[0]) { scheme -> - val colorStateList = - buildColorStateList( - -android.R.attr.state_checked to Color.GRAY, - -android.R.attr.state_enabled to Color.GRAY, - android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme) - ) - checkboxes.forEach { - it.buttonTintList = colorStateList - } + fun themeCheckbox(vararg checkboxes: CheckBox) { + withScheme(checkboxes[0]) { scheme -> + val colorStateList = + buildColorStateList( + -android.R.attr.state_checked to Color.GRAY, + -android.R.attr.state_enabled to Color.GRAY, + android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme) + ) + checkboxes.forEach { + it.buttonTintList = colorStateList } } + } - fun themeRadioButton(radioButton: RadioButton) { - withScheme(radioButton) { scheme -> - radioButton.buttonTintList = - buildColorStateList( - -android.R.attr.state_checked to Color.GRAY, - android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme) - ) - } + fun themeRadioButton(radioButton: RadioButton) { + withScheme(radioButton) { scheme -> + radioButton.buttonTintList = + buildColorStateList( + -android.R.attr.state_checked to Color.GRAY, + android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme) + ) } + } - fun colorEditText(editText: EditText) { - withScheme(editText) { scheme -> - // TODO check API-level compatibility - // editText.background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) - editText.backgroundTintList = - buildColorStateList( - -android.R.attr.state_focused to dynamicColor.outline().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme) - ) + fun colorEditText(editText: EditText) { + withScheme(editText) { scheme -> + // TODO check API-level compatibility + // editText.background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + editText.backgroundTintList = + buildColorStateList( + -android.R.attr.state_focused to dynamicColor.outline().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme) + ) - editText.setHintTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) - editText.setTextColor(dynamicColor.onSurface().getArgb(scheme)) - } + editText.setHintTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) + editText.setTextColor(dynamicColor.onSurface().getArgb(scheme)) } + } - fun colorEditTextOnPrimary(editText: EditText) { - withScheme(editText) { scheme -> - // TODO check API-level compatibility - editText.setHintTextColor(dynamicColor.onPrimary().getArgb(scheme)) - editText.setTextColor(dynamicColor.onPrimary().getArgb(scheme)) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - editText.textCursorDrawable?.let { - editText.textCursorDrawable = colorDrawable(it, dynamicColor.onPrimary().getArgb(scheme)) - } + fun colorEditTextOnPrimary(editText: EditText) { + withScheme(editText) { scheme -> + // TODO check API-level compatibility + editText.setHintTextColor(dynamicColor.onPrimary().getArgb(scheme)) + editText.setTextColor(dynamicColor.onPrimary().getArgb(scheme)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + editText.textCursorDrawable?.let { + editText.textCursorDrawable = colorDrawable(it, dynamicColor.onPrimary().getArgb(scheme)) } } } + } - fun themeViewBackground( - view: View, - colorRole: ColorRole = ColorRole.SURFACE - ) { - withScheme(view) { scheme -> - view.setBackgroundColor(colorRole.select(scheme)) - } + fun themeViewBackground(view: View, colorRole: ColorRole = ColorRole.SURFACE) { + withScheme(view) { scheme -> + view.setBackgroundColor(colorRole.select(scheme)) } + } - @JvmOverloads - fun highlightText( - textView: TextView, - originalText: String, - constraint: String, - colorRole: ColorRole = ColorRole.PRIMARY - ) { - withScheme(textView) { scheme -> - highlightText(textView, originalText, constraint, colorRole.select(scheme)) - } + @JvmOverloads + fun highlightText( + textView: TextView, + originalText: String, + constraint: String, + colorRole: ColorRole = ColorRole.PRIMARY + ) { + withScheme(textView) { scheme -> + highlightText(textView, originalText, constraint, colorRole.select(scheme)) } + } - private fun highlightText( - textView: TextView, - originalText: String, - constraint: String, - @ColorInt color: Int - ) { - val constraintLower = constraint.lowercase() - val start: Int = originalText.lowercase().indexOf(constraintLower) - if (start != -1) { - val spanText = Spannable.Factory.getInstance().newSpannable(originalText) - spanText(originalText, constraintLower, color, start, spanText) - textView.setText(spanText, TextView.BufferType.SPANNABLE) - } else { - textView.setText(originalText, TextView.BufferType.NORMAL) - } + private fun highlightText(textView: TextView, originalText: String, constraint: String, @ColorInt color: Int) { + val constraintLower = constraint.lowercase() + val start: Int = originalText.lowercase().indexOf(constraintLower) + if (start != -1) { + val spanText = Spannable.Factory.getInstance().newSpannable(originalText) + spanText(originalText, constraintLower, color, start, spanText) + textView.setText(spanText, TextView.BufferType.SPANNABLE) + } else { + textView.setText(originalText, TextView.BufferType.NORMAL) } + } - private fun spanText( - originalText: String, - constraint: String, - @ColorInt color: Int, - start: Int, - spanText: Spannable - ) { - val matcher = - Pattern - .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) - .matcher(originalText) - - matcher.find(start) - - do { - val mStart = matcher.start() - val mEnd = matcher.end() - spanText.setSpan(ForegroundColorSpan(color), mStart, mEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - spanText.setSpan(StyleSpan(Typeface.BOLD), mStart, mEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } while (matcher.find()) - } - - // here for backwards compatibility - @SuppressLint("UseSwitchCompatOrMaterialCode") - fun colorSwitch(switch: Switch) { - withScheme(switch) { scheme -> - val colors = SwitchColorUtils.calculateSwitchColors(switch.context, scheme) - DrawableCompat.setTintList(switch.thumbDrawable, colors.thumbColor) - DrawableCompat.setTintList(switch.trackDrawable, colors.trackColor) - } + private fun spanText( + originalText: String, + constraint: String, + @ColorInt color: Int, + start: Int, + spanText: Spannable + ) { + val matcher = + Pattern + .compile(constraint, Pattern.CASE_INSENSITIVE or Pattern.LITERAL) + .matcher(originalText) + + matcher.find(start) + + do { + val mStart = matcher.start() + val mEnd = matcher.end() + spanText.setSpan(ForegroundColorSpan(color), mStart, mEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + spanText.setSpan(StyleSpan(Typeface.BOLD), mStart, mEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } while (matcher.find()) + } + + // here for backwards compatibility + @SuppressLint("UseSwitchCompatOrMaterialCode") + fun colorSwitch(switch: Switch) { + withScheme(switch) { scheme -> + val colors = SwitchColorUtils.calculateSwitchColors(switch.context, scheme) + DrawableCompat.setTintList(switch.thumbDrawable, colors.thumbColor) + DrawableCompat.setTintList(switch.trackDrawable, colors.trackColor) } + } - @Deprecated("Don't do this, implement custom viewThemeUtils instead") - fun primaryColor(activity: Activity): Int = - withScheme(activity) { scheme -> - dynamicColor.primary().getArgb(scheme) - } + @Deprecated("Don't do this, implement custom viewThemeUtils instead") + fun primaryColor(activity: Activity): Int = withScheme(activity) { scheme -> + dynamicColor.primary().getArgb(scheme) + } - companion object { - private const val ON_SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.38f - } + companion object { + private const val ON_SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.38f } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt index 48d517c7..9eeaaa1e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt @@ -36,111 +36,93 @@ import javax.inject.Inject * View theme utils for Android extension views (androidx.*) */ class AndroidXViewThemeUtils - @Inject - constructor( - schemes: MaterialSchemes, - private val androidViewThemeUtils: AndroidViewThemeUtils - ) : ViewThemeUtilsBase(schemes) { - private val dynamicColor = MaterialDynamicColors() +@Inject +constructor( + schemes: MaterialSchemes, + private val androidViewThemeUtils: AndroidViewThemeUtils +) : ViewThemeUtilsBase(schemes) { + private val dynamicColor = MaterialDynamicColors() - fun colorSwitchCompat(switchCompat: SwitchCompat) { - withScheme(switchCompat) { scheme -> - val colors = SwitchColorUtils.calculateSwitchColors(switchCompat.context, scheme) - switchCompat.thumbTintList = colors.thumbColor - switchCompat.trackTintList = colors.trackColor - } + fun colorSwitchCompat(switchCompat: SwitchCompat) { + withScheme(switchCompat) { scheme -> + val colors = SwitchColorUtils.calculateSwitchColors(switchCompat.context, scheme) + switchCompat.thumbTintList = colors.thumbColor + switchCompat.trackTintList = colors.trackColor } + } - fun themeSwipeRefreshLayout(swipeRefreshLayout: SwipeRefreshLayout) { - withScheme(swipeRefreshLayout) { scheme -> - swipeRefreshLayout.setColorSchemeColors(dynamicColor.primary().getArgb(scheme)) - swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) - } + fun themeSwipeRefreshLayout(swipeRefreshLayout: SwipeRefreshLayout) { + withScheme(swipeRefreshLayout) { scheme -> + swipeRefreshLayout.setColorSchemeColors(dynamicColor.primary().getArgb(scheme)) + swipeRefreshLayout.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background) } + } - fun colorPrimaryTextViewElement(textView: AppCompatTextView) { - withScheme(textView) { scheme -> - textView.setTextColor(dynamicColor.primary().getArgb(scheme)) - TextViewCompat.setCompoundDrawableTintList( - textView, - ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) - ) - } + fun colorPrimaryTextViewElement(textView: AppCompatTextView) { + withScheme(textView) { scheme -> + textView.setTextColor(dynamicColor.primary().getArgb(scheme)) + TextViewCompat.setCompoundDrawableTintList( + textView, + ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) + ) } + } - // TODO host the back arrow in this lib instead of passing it everywhere - fun themeActionBar( - context: Context, - actionBar: ActionBar, - title: String, - backArrow: Drawable - ) { - withScheme(context) { scheme -> - val text: Spannable = getColoredSpan(title, dynamicColor.onSurface().getArgb(scheme)) - actionBar.title = text - themeActionBar(context, actionBar, backArrow) - } + // TODO host the back arrow in this lib instead of passing it everywhere + fun themeActionBar(context: Context, actionBar: ActionBar, title: String, backArrow: Drawable) { + withScheme(context) { scheme -> + val text: Spannable = getColoredSpan(title, dynamicColor.onSurface().getArgb(scheme)) + actionBar.title = text + themeActionBar(context, actionBar, backArrow) } + } - fun themeActionBar( - context: Context, - actionBar: ActionBar, - backArrow: Drawable - ) { - withScheme(context) { scheme -> - actionBar.setBackgroundDrawable(ColorDrawable(dynamicColor.surface().getArgb(scheme))) - val indicator = androidViewThemeUtils.colorDrawable(backArrow, dynamicColor.onSurface().getArgb(scheme)) - actionBar.setHomeAsUpIndicator(indicator) - } + fun themeActionBar(context: Context, actionBar: ActionBar, backArrow: Drawable) { + withScheme(context) { scheme -> + actionBar.setBackgroundDrawable(ColorDrawable(dynamicColor.surface().getArgb(scheme))) + val indicator = androidViewThemeUtils.colorDrawable(backArrow, dynamicColor.onSurface().getArgb(scheme)) + actionBar.setHomeAsUpIndicator(indicator) } + } - fun themeActionBarSubtitle( - context: Context, - actionBar: ActionBar - ) { - withScheme(context) { scheme -> - actionBar.subtitle = - getColoredSpan(actionBar.subtitle.toString(), dynamicColor.onSurfaceVariant().getArgb(scheme)) - } + fun themeActionBarSubtitle(context: Context, actionBar: ActionBar) { + withScheme(context) { scheme -> + actionBar.subtitle = + getColoredSpan(actionBar.subtitle.toString(), dynamicColor.onSurfaceVariant().getArgb(scheme)) } + } - fun themeToolbarSearchView(searchView: SearchView) { - withScheme(searchView) { scheme -> - // hacky as no default way is provided - val editText = - searchView.findViewById(androidx.appcompat.R.id.search_src_text) - val searchPlate = searchView.findViewById(androidx.appcompat.R.id.search_plate) - val closeButton = searchView.findViewById(androidx.appcompat.R.id.search_close_btn) - val searchButton = searchView.findViewById(androidx.appcompat.R.id.search_button) - editText.setHintTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) - editText.highlightColor = dynamicColor.inverseOnSurface().getArgb(scheme) - editText.setTextColor(dynamicColor.onSurface().getArgb(scheme)) - closeButton.setColorFilter(dynamicColor.onSurface().getArgb(scheme)) - searchButton.setColorFilter(dynamicColor.onSurface().getArgb(scheme)) - searchPlate.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) - } + fun themeToolbarSearchView(searchView: SearchView) { + withScheme(searchView) { scheme -> + // hacky as no default way is provided + val editText = + searchView.findViewById(androidx.appcompat.R.id.search_src_text) + val searchPlate = searchView.findViewById(androidx.appcompat.R.id.search_plate) + val closeButton = searchView.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchButton = searchView.findViewById(androidx.appcompat.R.id.search_button) + editText.setHintTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) + editText.highlightColor = dynamicColor.inverseOnSurface().getArgb(scheme) + editText.setTextColor(dynamicColor.onSurface().getArgb(scheme)) + closeButton.setColorFilter(dynamicColor.onSurface().getArgb(scheme)) + searchButton.setColorFilter(dynamicColor.onSurface().getArgb(scheme)) + searchPlate.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) } + } - fun themeNotificationCompatBuilder( - context: Context, - builder: NotificationCompat.Builder - ) { - withScheme(context) { scheme -> - builder.setColor(dynamicColor.primary().getArgb(scheme)) - } + fun themeNotificationCompatBuilder(context: Context, builder: NotificationCompat.Builder) { + withScheme(context) { scheme -> + builder.setColor(dynamicColor.primary().getArgb(scheme)) } + } - private fun getColoredSpan( - title: String, - color: Int - ): Spannable { - val text: Spannable = SpannableString(title) - text.setSpan( - ForegroundColorSpan(color), - 0, - text.length, - Spannable.SPAN_INCLUSIVE_INCLUSIVE - ) - return text - } + private fun getColoredSpan(title: String, color: Int): Spannable { + val text: Spannable = SpannableString(title) + text.setSpan( + ForegroundColorSpan(color), + 0, + text.length, + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ) + return text } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt index 86a68625..4e67244f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/ColorRole.kt @@ -13,9 +13,7 @@ import dynamiccolor.DynamicScheme /** * Parameter for library methods so that clients can choose color roles without accessing the Scheme directly */ -enum class ColorRole( - internal val select: (DynamicScheme) -> Int -) { +enum class ColorRole(internal val select: (DynamicScheme) -> Int) { PRIMARY({ dynamiccolor.MaterialDynamicColors().primary().getArgb(it) }), ON_PRIMARY({ dynamiccolor.MaterialDynamicColors().onPrimary().getArgb(it) }), PRIMARY_CONTAINER({ dynamiccolor.MaterialDynamicColors().primaryContainer().getArgb(it) }), diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt index 477292f3..c12db763 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt @@ -29,70 +29,62 @@ import javax.inject.Inject * View theme utils for dialogs */ class DialogViewThemeUtils - @Inject - constructor( - schemes: MaterialSchemes - ) : ViewThemeUtilsBase(schemes) { - private val dynamicColor = MaterialDynamicColors() +@Inject +constructor(schemes: MaterialSchemes) : ViewThemeUtilsBase(schemes) { + private val dynamicColor = MaterialDynamicColors() - fun colorMaterialAlertDialogBackground( - context: Context, - dialogBuilder: MaterialAlertDialogBuilder - ) { - withScheme(dialogBuilder.context) { scheme -> - val materialShapeDrawable = - MaterialShapeDrawable( - context, - null, - androidx.appcompat.R.attr.alertDialogStyle, - com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents - ) - materialShapeDrawable.initializeElevationOverlay(context) - materialShapeDrawable.fillColor = ColorStateList.valueOf(dynamicColor.surface().getArgb(scheme)) + fun colorMaterialAlertDialogBackground(context: Context, dialogBuilder: MaterialAlertDialogBuilder) { + withScheme(dialogBuilder.context) { scheme -> + val materialShapeDrawable = + MaterialShapeDrawable( + context, + null, + androidx.appcompat.R.attr.alertDialogStyle, + com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents + ) + materialShapeDrawable.initializeElevationOverlay(context) + materialShapeDrawable.fillColor = ColorStateList.valueOf(dynamicColor.surface().getArgb(scheme)) - // dialogCornerRadius first appeared in Android Pie - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val radius = context.resources.getDimension(R.dimen.dialogBorderRadius) - materialShapeDrawable.setCornerSize(radius) - } - - dialogBuilder.background = materialShapeDrawable + // dialogCornerRadius first appeared in Android Pie + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val radius = context.resources.getDimension(R.dimen.dialogBorderRadius) + materialShapeDrawable.setCornerSize(radius) } + + dialogBuilder.background = materialShapeDrawable } + } - fun colorDialogMenuText(button: MaterialButton) { - withScheme(button) { scheme -> - button.setTextColor(dynamicColor.onSurface().getArgb(scheme)) - button.iconTint = ColorStateList.valueOf(dynamicColor.onSurface().getArgb(scheme)) - } + fun colorDialogMenuText(button: MaterialButton) { + withScheme(button) { scheme -> + button.setTextColor(dynamicColor.onSurface().getArgb(scheme)) + button.iconTint = ColorStateList.valueOf(dynamicColor.onSurface().getArgb(scheme)) } + } - fun colorDialogHeadline(textView: TextView) { - withScheme(textView) { scheme -> - textView.setTextColor(dynamicColor.onSurface().getArgb(scheme)) - } + fun colorDialogHeadline(textView: TextView) { + withScheme(textView) { scheme -> + textView.setTextColor(dynamicColor.onSurface().getArgb(scheme)) } + } - fun colorDialogSupportingText(textView: TextView) { - withScheme(textView) { scheme -> - textView.setTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) - } + fun colorDialogSupportingText(textView: TextView) { + withScheme(textView) { scheme -> + textView.setTextColor(dynamicColor.onSurfaceVariant().getArgb(scheme)) } + } - fun colorDialogIcon(icon: ImageView) { - withScheme(icon) { scheme -> - icon.setColorFilter(dynamicColor.secondary().getArgb(scheme)) - } + fun colorDialogIcon(icon: ImageView) { + withScheme(icon) { scheme -> + icon.setColorFilter(dynamicColor.secondary().getArgb(scheme)) } + } - fun colorMaterialAlertDialogIcon( - context: Context, - drawableId: Int - ): Drawable { - val drawable = AppCompatResources.getDrawable(context, drawableId)!! - withScheme(context) { scheme -> - DrawableCompat.setTint(drawable, dynamicColor.secondary().getArgb(scheme)) - } - return drawable + fun colorMaterialAlertDialogIcon(context: Context, drawableId: Int): Drawable { + val drawable = AppCompatResources.getDrawable(context, drawableId)!! + withScheme(context) { scheme -> + DrawableCompat.setTint(drawable, dynamicColor.secondary().getArgb(scheme)) } + return drawable } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt index b3c2c006..ca3185a4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt @@ -49,720 +49,684 @@ import javax.inject.Inject */ @Suppress("TooManyFunctions", "LargeClass") class MaterialViewThemeUtils - @Inject - constructor( - schemes: MaterialSchemes, - private val colorUtil: ColorUtil - ) : ViewThemeUtilsBase(schemes) { - private val dynamicColor = MaterialDynamicColors() - - fun colorToolbarOverflowIcon(toolbar: MaterialToolbar) { - withScheme(toolbar) { scheme -> - colorTrailingIcon(scheme, toolbar.overflowIcon) - } - } - - private fun colorTrailingIcon( - scheme: DynamicScheme, - icon: Drawable? - ) { - icon?.colorFilter = - BlendModeColorFilterCompat.createBlendModeColorFilterCompat( - dynamicColor.onSurfaceVariant().getArgb(scheme), - BlendModeCompat.SRC_ATOP - ) - } - - fun themeSearchBarText(searchText: MaterialTextView) { - withScheme(searchText) { scheme -> - searchText.setHintTextColor(dynamicColor.surfaceVariant().getArgb(scheme)) - } - } - - fun themeFAB(fab: FloatingActionButton) { - withScheme(fab) { scheme -> - fab.backgroundTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to Color.GRAY - ) +@Inject +constructor(schemes: MaterialSchemes, private val colorUtil: ColorUtil) : + ViewThemeUtilsBase(schemes) { + private val dynamicColor = MaterialDynamicColors() - fab.imageTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onPrimaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to Color.WHITE - ) - } + fun colorToolbarOverflowIcon(toolbar: MaterialToolbar) { + withScheme(toolbar) { scheme -> + colorTrailingIcon(scheme, toolbar.overflowIcon) } + } - fun themeSecondaryFAB(fab: FloatingActionButton) { - withScheme(fab) { scheme -> - fab.backgroundTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.secondaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to Color.GRAY - ) + private fun colorTrailingIcon(scheme: DynamicScheme, icon: Drawable?) { + icon?.colorFilter = + BlendModeColorFilterCompat.createBlendModeColorFilterCompat( + dynamicColor.onSurfaceVariant().getArgb(scheme), + BlendModeCompat.SRC_ATOP + ) + } - fab.imageTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to Color.WHITE - ) - } + fun themeSearchBarText(searchText: MaterialTextView) { + withScheme(searchText) { scheme -> + searchText.setHintTextColor(dynamicColor.surfaceVariant().getArgb(scheme)) } + } - fun themeExtendedFAB(fab: ExtendedFloatingActionButton) { - withScheme(fab) { scheme -> - fab.backgroundTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to Color.GRAY - ) + fun themeFAB(fab: FloatingActionButton) { + withScheme(fab) { scheme -> + fab.backgroundTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to Color.GRAY + ) - val colorStateList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onPrimaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to Color.WHITE - ) - fab.setTextColor(colorStateList) - fab.iconTint = colorStateList - } - } - - fun themeCardView(cardView: MaterialCardView) { - withScheme(cardView) { scheme -> - cardView.backgroundTintList = ColorStateList.valueOf(dynamicColor.surface().getArgb(scheme)) - cardView.setStrokeColor( - buildColorStateList( - android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.outlineVariant().getArgb(scheme) - ) + fab.imageTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onPrimaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to Color.WHITE ) - } } + } - fun themeSearchCardView(cardView: MaterialCardView) { - withScheme(cardView) { scheme -> - cardView.backgroundTintList = - ColorStateList.valueOf( - dynamicColor.surfaceContainerHigh().getArgb(scheme) - ) - cardView.setStrokeColor( - buildColorStateList( - android.R.attr.state_checked to dynamicColor.surfaceContainerHigh().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.surfaceContainerHigh().getArgb(scheme) - ) + fun themeSecondaryFAB(fab: FloatingActionButton) { + withScheme(fab) { scheme -> + fab.backgroundTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to Color.GRAY ) - } - } - fun themeDragHandleView(dragHandleView: BottomSheetDragHandleView) { - withScheme(dragHandleView) { scheme -> - dragHandleView.imageTintList = ColorStateList.valueOf(dynamicColor.onSurfaceVariant().getArgb(scheme)) - } + fab.imageTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to Color.WHITE + ) } + } + + fun themeExtendedFAB(fab: ExtendedFloatingActionButton) { + withScheme(fab) { scheme -> + fab.backgroundTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to Color.GRAY + ) - fun colorMaterialTextButton(button: MaterialButton) { - withScheme(button) { scheme -> - button.rippleColor = rippleColor(scheme) - } + val colorStateList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onPrimaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to Color.WHITE + ) + fab.setTextColor(colorStateList) + fab.iconTint = colorStateList } + } - fun colorMaterialButtonText(button: MaterialButton) { - withScheme(button) { scheme -> - val disabledColor = ContextCompat.getColor(button.context, R.color.disabled_text) - val colorStateList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_enabled to disabledColor - ) - button.setTextColor(colorStateList) - button.iconTint = colorStateList - } - } - - fun colorMaterialButtonPrimaryFilled(button: MaterialButton) { - withScheme(button) { scheme -> - button.backgroundTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - SURFACE_OPACITY_BUTTON_DISABLED - ) - ) + fun themeCardView(cardView: MaterialCardView) { + withScheme(cardView) { scheme -> + cardView.backgroundTintList = ColorStateList.valueOf(dynamicColor.surface().getArgb(scheme)) + cardView.setStrokeColor( + buildColorStateList( + android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.outlineVariant().getArgb(scheme) + ) + ) + } + } - val contentColorList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - SURFACE_OPACITY_BUTTON_DISABLED - ) - ) + fun themeSearchCardView(cardView: MaterialCardView) { + withScheme(cardView) { scheme -> + cardView.backgroundTintList = + ColorStateList.valueOf( + dynamicColor.surfaceContainerHigh().getArgb(scheme) + ) + cardView.setStrokeColor( + buildColorStateList( + android.R.attr.state_checked to dynamicColor.surfaceContainerHigh().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.surfaceContainerHigh().getArgb(scheme) + ) + ) + } + } - button.setTextColor(contentColorList) - button.iconTint = contentColorList - } - } - - fun colorMaterialButtonPrimaryTonal(button: MaterialButton) { - withScheme(button) { scheme -> - button.backgroundTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.secondaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - SURFACE_OPACITY_BUTTON_DISABLED - ), - -android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) - ) + fun themeDragHandleView(dragHandleView: BottomSheetDragHandleView) { + withScheme(dragHandleView) { scheme -> + dragHandleView.imageTintList = ColorStateList.valueOf(dynamicColor.onSurfaceVariant().getArgb(scheme)) + } + } - val contentColorList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ), - -android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) - ) - button.setTextColor(contentColorList) - button.iconTint = contentColorList - } - } - - fun colorMaterialButtonPrimaryOutlined(button: MaterialButton) { - withScheme(button) { scheme -> - val contentColorList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ) - ) + fun colorMaterialTextButton(button: MaterialButton) { + withScheme(button) { scheme -> + button.rippleColor = rippleColor(scheme) + } + } - button.setTextColor(contentColorList) - button.iconTint = contentColorList - button.strokeColor = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.outline().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED - ), - -android.R.attr.state_hovered to dynamicColor.outline().getArgb(scheme), - -android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_pressed to dynamicColor.outline().getArgb(scheme) - ) + fun colorMaterialButtonText(button: MaterialButton) { + withScheme(button) { scheme -> + val disabledColor = ContextCompat.getColor(button.context, R.color.disabled_text) + val colorStateList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_enabled to disabledColor + ) + button.setTextColor(colorStateList) + button.iconTint = colorStateList + } + } - button.strokeWidth = - button.resources.getDimension(R.dimen.outlinedButtonStrokeWidth).toInt() - button.rippleColor = rippleColor(scheme) - } - } - - fun colorMaterialButtonPrimaryBorderless(button: MaterialButton) { - withScheme(button) { scheme -> - val contentColorList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ) - ) + fun colorMaterialButtonPrimaryFilled(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + SURFACE_OPACITY_BUTTON_DISABLED + ) + ) - button.setTextColor(contentColorList) - button.iconTint = contentColorList - } - } - - /** - * text is primary, background is on_primary - */ - fun colorMaterialButtonFilledOnPrimary(button: MaterialButton) { - withScheme(button) { scheme -> - button.backgroundTintList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.surface().getArgb(scheme), - SURFACE_OPACITY_BUTTON_DISABLED - ), - -android.R.attr.state_hovered to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_focused to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_pressed to dynamicColor.onPrimary().getArgb(scheme) - ) + val contentColorList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + SURFACE_OPACITY_BUTTON_DISABLED + ) + ) - colorMaterialButtonContent(button) - } - } - - fun colorMaterialButtonContent( - button: MaterialButton, - colorRole: ColorRole = ColorRole.PRIMARY - ) { - withScheme(button) { scheme -> - val contentColorList = - buildColorStateList( - android.R.attr.state_enabled to colorRole.select(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - colorRole.select(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ), - -android.R.attr.state_hovered to colorRole.select(scheme), - -android.R.attr.state_focused to colorRole.select(scheme), - -android.R.attr.state_pressed to colorRole.select(scheme) - ) + button.setTextColor(contentColorList) + button.iconTint = contentColorList + } + } - button.setTextColor( - contentColorList + fun colorMaterialButtonPrimaryTonal(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + SURFACE_OPACITY_BUTTON_DISABLED + ), + -android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) ) - button.iconTint = contentColorList - } + val contentColorList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + -android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) + ) + button.setTextColor(contentColorList) + button.iconTint = contentColorList } + } - fun colorMaterialButtonOutlinedOnPrimary(button: MaterialButton) { - withScheme(button) { scheme -> - button.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) - val contentColorList = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onPrimary().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ) - ) + fun colorMaterialButtonPrimaryOutlined(button: MaterialButton) { + withScheme(button) { scheme -> + val contentColorList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ) + ) - button.setTextColor(contentColorList) - button.iconTint = contentColorList - button.strokeColor = contentColorList - button.strokeWidth = - button.resources.getDimension(R.dimen.outlinedButtonStrokeWidth).toInt() - button.rippleColor = rippleColor(scheme) - } - } - - fun themeToolbar(toolbar: MaterialToolbar) { - withScheme(toolbar) { scheme -> - toolbar.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) - toolbar.setNavigationIconTint(dynamicColor.onSurface().getArgb(scheme)) - toolbar.setTitleTextColor(dynamicColor.onSurface().getArgb(scheme)) - colorTrailingIcon(scheme, toolbar.overflowIcon) - } - } - - @Deprecated( - "Duplicated, use themeCardView instead", - replaceWith = ReplaceWith("themeCardView(card)") - ) - fun colorCardViewBackground(card: MaterialCardView) = themeCardView(card) - - /** - * Themes a [LinearProgressIndicator] using the provided [ColorRole]. - * - * @param linearProgressIndicator The progress indicator to theme. - * @param colorRole The color role to be used for the active indicator part. Defaults to [ColorRole.PRIMARY]. - */ - fun colorProgressBar( - linearProgressIndicator: LinearProgressIndicator, - colorRole: ColorRole = ColorRole.PRIMARY - ) { - withScheme(linearProgressIndicator) { scheme -> - colorProgressBar(linearProgressIndicator, colorRole.select(scheme)) - } - } - - /** - * Themes a [LinearProgressIndicator] using the provided color [Int]. - * - * @param linearProgressIndicator The progress indicator to theme. - * @param color The color to be used for the active indicator part. - */ - fun colorProgressBar( - linearProgressIndicator: LinearProgressIndicator, - @ColorInt color: Int - ) { - withScheme(linearProgressIndicator) { scheme -> - linearProgressIndicator.setIndicatorColor(color) - linearProgressIndicator.trackColor = dynamicColor.secondaryContainer().getArgb(scheme) - } - } - - /** - * Themes a [CircularProgressIndicator] using the provided [ColorRole]. - * - * @param circularProgressIndicator The progress indicator to theme. - * @param colorRole The color role to be used for the active indicator part. Defaults to [ColorRole.PRIMARY]. - */ - fun colorProgressBar( - circularProgressIndicator: CircularProgressIndicator, - colorRole: ColorRole = ColorRole.PRIMARY - ) { - withScheme(circularProgressIndicator) { scheme -> - colorProgressBar(circularProgressIndicator, colorRole.select(scheme)) - } - } - - /** - * Themes a [CircularProgressIndicator] using the provided color [Int]. - * - * @param circularProgressIndicator The progress indicator to theme. - * @param color The color to be used for the active indicator part. - */ - fun colorProgressBar( - circularProgressIndicator: CircularProgressIndicator, - @ColorInt color: Int - ) { - withScheme(circularProgressIndicator) { scheme -> - circularProgressIndicator.setIndicatorColor(color) - circularProgressIndicator.trackColor = dynamicColor.secondaryContainer().getArgb(scheme) - } - } - - fun colorTextInputLayout(textInputLayout: TextInputLayout) { - withScheme(textInputLayout) { scheme -> - val errorColor = dynamicColor.error().getArgb(scheme) - - val errorColorStateList = - buildColorStateList( - -android.R.attr.state_focused to errorColor, - android.R.attr.state_focused to errorColor - ) + button.setTextColor(contentColorList) + button.iconTint = contentColorList + button.strokeColor = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.outline().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED + ), + -android.R.attr.state_hovered to dynamicColor.outline().getArgb(scheme), + -android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_pressed to dynamicColor.outline().getArgb(scheme) + ) - val coloredColorStateList = - buildColorStateList( - -android.R.attr.state_focused to dynamicColor.outline().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme) - ) + button.strokeWidth = + button.resources.getDimension(R.dimen.outlinedButtonStrokeWidth).toInt() + button.rippleColor = rippleColor(scheme) + } + } - textInputLayout.setBoxStrokeColorStateList(coloredColorStateList) - textInputLayout.setErrorIconTintList(errorColorStateList) - textInputLayout.setErrorTextColor(errorColorStateList) - textInputLayout.boxStrokeErrorColor = errorColorStateList - textInputLayout.defaultHintTextColor = coloredColorStateList + fun colorMaterialButtonPrimaryBorderless(button: MaterialButton) { + withScheme(button) { scheme -> + val contentColorList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ) + ) - textInputLayout.editText?.highlightColor = dynamicColor.primary().getArgb(scheme) - } + button.setTextColor(contentColorList) + button.iconTint = contentColorList } + } - fun colorTextInputLayout( - textInputLayout: TextInputLayout, - colorRole: ColorRole - ) { - withScheme(textInputLayout) { scheme -> - val errorColor = dynamicColor.error().getArgb(scheme) + /** + * text is primary, background is on_primary + */ + fun colorMaterialButtonFilledOnPrimary(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.surface().getArgb(scheme), + SURFACE_OPACITY_BUTTON_DISABLED + ), + -android.R.attr.state_hovered to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_focused to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_pressed to dynamicColor.onPrimary().getArgb(scheme) + ) - val errorColorStateList = - buildColorStateList( - -android.R.attr.state_focused to errorColor, - android.R.attr.state_focused to errorColor - ) + colorMaterialButtonContent(button) + } + } - val coloredColorStateList = - buildColorStateList( - -android.R.attr.state_focused to dynamicColor.outline().getArgb(scheme), - android.R.attr.state_focused to colorRole.select(scheme) - ) + fun colorMaterialButtonContent(button: MaterialButton, colorRole: ColorRole = ColorRole.PRIMARY) { + withScheme(button) { scheme -> + val contentColorList = + buildColorStateList( + android.R.attr.state_enabled to colorRole.select(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + colorRole.select(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + -android.R.attr.state_hovered to colorRole.select(scheme), + -android.R.attr.state_focused to colorRole.select(scheme), + -android.R.attr.state_pressed to colorRole.select(scheme) + ) + + button.setTextColor( + contentColorList + ) - textInputLayout.setBoxStrokeColorStateList(coloredColorStateList) - textInputLayout.setErrorIconTintList(errorColorStateList) - textInputLayout.setErrorTextColor(errorColorStateList) - textInputLayout.boxStrokeErrorColor = errorColorStateList - textInputLayout.defaultHintTextColor = coloredColorStateList - - textInputLayout.editText?.highlightColor = colorRole.select(scheme) - textInputLayout.setEndIconTintList(coloredColorStateList) - textInputLayout.setStartIconTintList(coloredColorStateList) - } - } - - fun themeTabLayout(tabLayout: TabLayout) { - withScheme(tabLayout) { scheme -> - colorTabLayout(tabLayout, scheme) - } - } - - fun themeTabLayoutOnSurface(tabLayout: TabLayout) { - withScheme(tabLayout) { scheme -> - tabLayout.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) - colorTabLayout(tabLayout, scheme) - } - } - - fun colorBottomSheetBackground( - bottomSheet: View, - colorRole: ColorRole = ColorRole.SURFACE_CONTAINER_LOW - ) { - withScheme(bottomSheet) { scheme -> - bottomSheet.setBackgroundColor(colorRole.select(scheme)) - } - } - - fun colorBottomSheetDragHandle( - bottomSheetDragHandleView: BottomSheetDragHandleView, - colorRole: ColorRole = ColorRole.ON_SURFACE_VARIANT - ) { - withScheme(bottomSheetDragHandleView) { scheme -> - bottomSheetDragHandleView.setColorFilter(colorRole.select(scheme)) - } - } - - fun colorTabLayout( - tabLayout: TabLayout, - scheme: DynamicScheme - ) { - tabLayout.setSelectedTabIndicatorColor(dynamicColor.primary().getArgb(scheme)) - val tabContentColors = - buildColorStateList( - android.R.attr.state_selected to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_selected to - ContextCompat.getColor( - tabLayout.context, - R.color.high_emphasis_text + button.iconTint = contentColorList + } + } + + fun colorMaterialButtonOutlinedOnPrimary(button: MaterialButton) { + withScheme(button) { scheme -> + button.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT) + val contentColorList = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onPrimary().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED ) ) - tabLayout.tabTextColors = tabContentColors - tabLayout.tabIconTint = tabContentColors - tabLayout.tabRippleColor = rippleColor(scheme) + button.setTextColor(contentColorList) + button.iconTint = contentColorList + button.strokeColor = contentColorList + button.strokeWidth = + button.resources.getDimension(R.dimen.outlinedButtonStrokeWidth).toInt() + button.rippleColor = rippleColor(scheme) } + } - fun colorMaterialCheckBox(materialCheckBox: MaterialCheckBox) { - withScheme(materialCheckBox.context) { scheme -> - materialCheckBox.buttonTintList = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) - ) + fun themeToolbar(toolbar: MaterialToolbar) { + withScheme(toolbar) { scheme -> + toolbar.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) + toolbar.setNavigationIconTint(dynamicColor.onSurface().getArgb(scheme)) + toolbar.setTitleTextColor(dynamicColor.onSurface().getArgb(scheme)) + colorTrailingIcon(scheme, toolbar.overflowIcon) + } + } - materialCheckBox.buttonIconTintList = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_checked to android.R.color.transparent - ) - } + @Deprecated( + "Duplicated, use themeCardView instead", + replaceWith = ReplaceWith("themeCardView(card)") + ) + fun colorCardViewBackground(card: MaterialCardView) = themeCardView(card) + + /** + * Themes a [LinearProgressIndicator] using the provided [ColorRole]. + * + * @param linearProgressIndicator The progress indicator to theme. + * @param colorRole The color role to be used for the active indicator part. Defaults to [ColorRole.PRIMARY]. + */ + fun colorProgressBar(linearProgressIndicator: LinearProgressIndicator, colorRole: ColorRole = ColorRole.PRIMARY) { + withScheme(linearProgressIndicator) { scheme -> + colorProgressBar(linearProgressIndicator, colorRole.select(scheme)) } + } - fun colorMaterialSwitch(materialSwitch: MaterialSwitch) { - withScheme(materialSwitch.context) { scheme -> - materialSwitch.thumbTintList = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.onPrimary().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) - ) + /** + * Themes a [LinearProgressIndicator] using the provided color [Int]. + * + * @param linearProgressIndicator The progress indicator to theme. + * @param color The color to be used for the active indicator part. + */ + fun colorProgressBar(linearProgressIndicator: LinearProgressIndicator, @ColorInt color: Int) { + withScheme(linearProgressIndicator) { scheme -> + linearProgressIndicator.setIndicatorColor(color) + linearProgressIndicator.trackColor = dynamicColor.secondaryContainer().getArgb(scheme) + } + } - materialSwitch.trackTintList = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme), - // XXX: specs use surfaceContainerHighest - -android.R.attr.state_checked to dynamicColor.surface().getArgb(scheme) - ) + /** + * Themes a [CircularProgressIndicator] using the provided [ColorRole]. + * + * @param circularProgressIndicator The progress indicator to theme. + * @param colorRole The color role to be used for the active indicator part. Defaults to [ColorRole.PRIMARY]. + */ + fun colorProgressBar( + circularProgressIndicator: CircularProgressIndicator, + colorRole: ColorRole = ColorRole.PRIMARY + ) { + withScheme(circularProgressIndicator) { scheme -> + colorProgressBar(circularProgressIndicator, colorRole.select(scheme)) + } + } - materialSwitch.trackDecorationTintList = - buildColorStateList( - android.R.attr.state_checked to android.R.color.transparent, - -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) - ) - } - } - - fun colorChipBackground(chip: Chip) { - withScheme(chip) { scheme -> - chip.chipBackgroundColor = ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) - chip.setTextColor(dynamicColor.onPrimary().getArgb(scheme)) - } - } - - fun colorChipDrawable( - context: Context, - chipDrawable: ChipDrawable - ) { - withScheme(context) { scheme -> - chipDrawable.chipBackgroundColor = ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) - chipDrawable.setTextColor(dynamicColor.onPrimary().getArgb(scheme)) - } - } - - fun colorChipOutlined( - chip: Chip, - strokeWidth: Float - ) { - withScheme(chip) { scheme -> - chip.chipBackgroundColor = ColorStateList.valueOf(Color.TRANSPARENT) - chip.chipStrokeWidth = strokeWidth - chip.chipStrokeColor = ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) - chip.setTextColor(dynamicColor.primary().getArgb(scheme)) - } - } - - fun themeSnackbar(snackbar: Snackbar) { - withScheme(snackbar.context) { scheme -> - snackbar.setBackgroundTint(dynamicColor.inverseSurface().getArgb(scheme)) - snackbar.setActionTextColor(dynamicColor.inversePrimary().getArgb(scheme)) - snackbar.setTextColor(dynamicColor.inverseOnSurface().getArgb(scheme)) - } - } - - fun themeChipSuggestion(chip: Chip) { - withScheme(chip.context) { scheme -> - chip.chipStrokeColor = chipOutlineColorList(scheme) - chip.setTextColor(chipSuggestionInputTextColorList(scheme)) - } - } - - fun themeChipFilter(chip: Chip) { - withScheme(chip.context) { scheme -> - val backgroundColors = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.surface().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.secondaryContainer().getArgb(scheme), - android.R.attr.state_hovered to dynamicColor.secondaryContainer().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.secondaryContainer().getArgb(scheme) - ) + /** + * Themes a [CircularProgressIndicator] using the provided color [Int]. + * + * @param circularProgressIndicator The progress indicator to theme. + * @param color The color to be used for the active indicator part. + */ + fun colorProgressBar(circularProgressIndicator: CircularProgressIndicator, @ColorInt color: Int) { + withScheme(circularProgressIndicator) { scheme -> + circularProgressIndicator.setIndicatorColor(color) + circularProgressIndicator.trackColor = dynamicColor.secondaryContainer().getArgb(scheme) + } + } - val iconColors = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), - android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) - ) + fun colorTextInputLayout(textInputLayout: TextInputLayout) { + withScheme(textInputLayout) { scheme -> + val errorColor = dynamicColor.error().getArgb(scheme) - val textColors = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) - ) + val errorColorStateList = + buildColorStateList( + -android.R.attr.state_focused to errorColor, + android.R.attr.state_focused to errorColor + ) - chip.chipBackgroundColor = backgroundColors - chip.chipStrokeColor = chipOutlineFilterColorList(scheme) - chip.setTextColor(textColors) - chip.checkedIconTint = iconColors - } - } - - fun themeChipAssist(chip: Chip) { - withScheme(chip.context) { scheme -> - val iconColors = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ), - android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme), - android.R.attr.state_hovered to dynamicColor.primary().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.primary().getArgb(scheme) - ) + val coloredColorStateList = + buildColorStateList( + -android.R.attr.state_focused to dynamicColor.outline().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme) + ) - val textColors = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onSurface().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ), - android.R.attr.state_hovered to dynamicColor.onSurface().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.onSurface().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.onSurface().getArgb(scheme) - ) + textInputLayout.setBoxStrokeColorStateList(coloredColorStateList) + textInputLayout.setErrorIconTintList(errorColorStateList) + textInputLayout.setErrorTextColor(errorColorStateList) + textInputLayout.boxStrokeErrorColor = errorColorStateList + textInputLayout.defaultHintTextColor = coloredColorStateList - chip.chipStrokeColor = chipOutlineColorList(scheme) - chip.setTextColor(textColors) - chip.chipIconTint = iconColors - } - } - - fun themeChipInput(chip: Chip) { - withScheme(chip.context) { scheme -> - val iconColors = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onSurfaceVariant().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ), - android.R.attr.state_focused to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_hovered to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.onSurfaceVariant().getArgb(scheme) - ) + textInputLayout.editText?.highlightColor = dynamicColor.primary().getArgb(scheme) + } + } - chip.chipStrokeColor = chipOutlineFilterColorList(scheme) - chip.setTextColor(chipSuggestionInputTextColorList(scheme)) - chip.chipIconTint = iconColors - } + fun colorTextInputLayout(textInputLayout: TextInputLayout, colorRole: ColorRole) { + withScheme(textInputLayout) { scheme -> + val errorColor = dynamicColor.error().getArgb(scheme) + + val errorColorStateList = + buildColorStateList( + -android.R.attr.state_focused to errorColor, + android.R.attr.state_focused to errorColor + ) + + val coloredColorStateList = + buildColorStateList( + -android.R.attr.state_focused to dynamicColor.outline().getArgb(scheme), + android.R.attr.state_focused to colorRole.select(scheme) + ) + + textInputLayout.setBoxStrokeColorStateList(coloredColorStateList) + textInputLayout.setErrorIconTintList(errorColorStateList) + textInputLayout.setErrorTextColor(errorColorStateList) + textInputLayout.boxStrokeErrorColor = errorColorStateList + textInputLayout.defaultHintTextColor = coloredColorStateList + + textInputLayout.editText?.highlightColor = colorRole.select(scheme) + textInputLayout.setEndIconTintList(coloredColorStateList) + textInputLayout.setStartIconTintList(coloredColorStateList) } + } - private fun chipOutlineColorList(scheme: DynamicScheme) = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.outlineVariant().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED - ), - android.R.attr.state_hovered to dynamicColor.outlineVariant().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.outlineVariant().getArgb(scheme) - ) + fun themeTabLayout(tabLayout: TabLayout) { + withScheme(tabLayout) { scheme -> + colorTabLayout(tabLayout, scheme) + } + } - private fun chipOutlineFilterColorList(scheme: DynamicScheme) = - buildColorStateList( - android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), - -android.R.attr.state_checked to dynamicColor.outlineVariant().getArgb(scheme) - ) + fun themeTabLayoutOnSurface(tabLayout: TabLayout) { + withScheme(tabLayout) { scheme -> + tabLayout.setBackgroundColor(dynamicColor.surface().getArgb(scheme)) + colorTabLayout(tabLayout, scheme) + } + } - private fun chipSuggestionInputTextColorList(scheme: DynamicScheme) = - buildColorStateList( - android.R.attr.state_enabled to dynamicColor.onSurfaceVariant().getArgb(scheme), - -android.R.attr.state_enabled to - colorUtil.adjustOpacity( - dynamicColor.onSurface().getArgb(scheme), - ON_SURFACE_OPACITY_BUTTON_DISABLED - ), - android.R.attr.state_hovered to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_focused to dynamicColor.onSurfaceVariant().getArgb(scheme), - android.R.attr.state_pressed to dynamicColor.onSurfaceVariant().getArgb(scheme) - ) + fun colorBottomSheetBackground(bottomSheet: View, colorRole: ColorRole = ColorRole.SURFACE_CONTAINER_LOW) { + withScheme(bottomSheet) { scheme -> + bottomSheet.setBackgroundColor(colorRole.select(scheme)) + } + } + + fun colorBottomSheetDragHandle( + bottomSheetDragHandleView: BottomSheetDragHandleView, + colorRole: ColorRole = ColorRole.ON_SURFACE_VARIANT + ) { + withScheme(bottomSheetDragHandleView) { scheme -> + bottomSheetDragHandleView.setColorFilter(colorRole.select(scheme)) + } + } - private fun rippleColor(scheme: DynamicScheme) = + fun colorTabLayout(tabLayout: TabLayout, scheme: DynamicScheme) { + tabLayout.setSelectedTabIndicatorColor(dynamicColor.primary().getArgb(scheme)) + val tabContentColors = buildColorStateList( - android.R.attr.state_pressed to - colorUtil.adjustOpacity( - dynamicColor.primary().getArgb(scheme), - SURFACE_OPACITY_BUTTON_DISABLED + android.R.attr.state_selected to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_selected to + ContextCompat.getColor( + tabLayout.context, + R.color.high_emphasis_text ) ) - companion object { - private const val SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.12f - private const val ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED: Float = 0.12f - private const val ON_SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.38f + tabLayout.tabTextColors = tabContentColors + tabLayout.tabIconTint = tabContentColors + tabLayout.tabRippleColor = rippleColor(scheme) + } + + fun colorMaterialCheckBox(materialCheckBox: MaterialCheckBox) { + withScheme(materialCheckBox.context) { scheme -> + materialCheckBox.buttonTintList = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) + ) + + materialCheckBox.buttonIconTintList = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_checked to android.R.color.transparent + ) + } + } + + fun colorMaterialSwitch(materialSwitch: MaterialSwitch) { + withScheme(materialSwitch.context) { scheme -> + materialSwitch.thumbTintList = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onPrimary().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) + ) + + materialSwitch.trackTintList = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.primary().getArgb(scheme), + // XXX: specs use surfaceContainerHighest + -android.R.attr.state_checked to dynamicColor.surface().getArgb(scheme) + ) + + materialSwitch.trackDecorationTintList = + buildColorStateList( + android.R.attr.state_checked to android.R.color.transparent, + -android.R.attr.state_checked to dynamicColor.outline().getArgb(scheme) + ) + } + } + + fun colorChipBackground(chip: Chip) { + withScheme(chip) { scheme -> + chip.chipBackgroundColor = ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) + chip.setTextColor(dynamicColor.onPrimary().getArgb(scheme)) } } + + fun colorChipDrawable(context: Context, chipDrawable: ChipDrawable) { + withScheme(context) { scheme -> + chipDrawable.chipBackgroundColor = ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) + chipDrawable.setTextColor(dynamicColor.onPrimary().getArgb(scheme)) + } + } + + fun colorChipOutlined(chip: Chip, strokeWidth: Float) { + withScheme(chip) { scheme -> + chip.chipBackgroundColor = ColorStateList.valueOf(Color.TRANSPARENT) + chip.chipStrokeWidth = strokeWidth + chip.chipStrokeColor = ColorStateList.valueOf(dynamicColor.primary().getArgb(scheme)) + chip.setTextColor(dynamicColor.primary().getArgb(scheme)) + } + } + + fun themeSnackbar(snackbar: Snackbar) { + withScheme(snackbar.context) { scheme -> + snackbar.setBackgroundTint(dynamicColor.inverseSurface().getArgb(scheme)) + snackbar.setActionTextColor(dynamicColor.inversePrimary().getArgb(scheme)) + snackbar.setTextColor(dynamicColor.inverseOnSurface().getArgb(scheme)) + } + } + + fun themeChipSuggestion(chip: Chip) { + withScheme(chip.context) { scheme -> + chip.chipStrokeColor = chipOutlineColorList(scheme) + chip.setTextColor(chipSuggestionInputTextColorList(scheme)) + } + } + + fun themeChipFilter(chip: Chip) { + withScheme(chip.context) { scheme -> + val backgroundColors = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.surface().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.secondaryContainer().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.secondaryContainer().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.secondaryContainer().getArgb(scheme) + ) + + val iconColors = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) + ) + + val textColors = + buildColorStateList( + android.R.attr.state_checked to dynamicColor.onSecondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSecondaryContainer().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSecondaryContainer().getArgb(scheme) + ) + + chip.chipBackgroundColor = backgroundColors + chip.chipStrokeColor = chipOutlineFilterColorList(scheme) + chip.setTextColor(textColors) + chip.checkedIconTint = iconColors + } + } + + fun themeChipAssist(chip: Chip) { + withScheme(chip.context) { scheme -> + val iconColors = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.primary().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + android.R.attr.state_focused to dynamicColor.primary().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.primary().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.primary().getArgb(scheme) + ) + + val textColors = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onSurface().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + android.R.attr.state_hovered to dynamicColor.onSurface().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSurface().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSurface().getArgb(scheme) + ) + + chip.chipStrokeColor = chipOutlineColorList(scheme) + chip.setTextColor(textColors) + chip.chipIconTint = iconColors + } + } + + fun themeChipInput(chip: Chip) { + withScheme(chip.context) { scheme -> + val iconColors = + buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onSurfaceVariant().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + android.R.attr.state_focused to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_hovered to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSurfaceVariant().getArgb(scheme) + ) + + chip.chipStrokeColor = chipOutlineFilterColorList(scheme) + chip.setTextColor(chipSuggestionInputTextColorList(scheme)) + chip.chipIconTint = iconColors + } + } + + private fun chipOutlineColorList(scheme: DynamicScheme) = buildColorStateList( + android.R.attr.state_enabled to dynamicColor.outlineVariant().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED + ), + android.R.attr.state_hovered to dynamicColor.outlineVariant().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.outlineVariant().getArgb(scheme) + ) + + private fun chipOutlineFilterColorList(scheme: DynamicScheme) = buildColorStateList( + android.R.attr.state_checked to dynamicColor.secondaryContainer().getArgb(scheme), + -android.R.attr.state_checked to dynamicColor.outlineVariant().getArgb(scheme) + ) + + private fun chipSuggestionInputTextColorList(scheme: DynamicScheme) = buildColorStateList( + android.R.attr.state_enabled to dynamicColor.onSurfaceVariant().getArgb(scheme), + -android.R.attr.state_enabled to + colorUtil.adjustOpacity( + dynamicColor.onSurface().getArgb(scheme), + ON_SURFACE_OPACITY_BUTTON_DISABLED + ), + android.R.attr.state_hovered to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_focused to dynamicColor.onSurfaceVariant().getArgb(scheme), + android.R.attr.state_pressed to dynamicColor.onSurfaceVariant().getArgb(scheme) + ) + + private fun rippleColor(scheme: DynamicScheme) = buildColorStateList( + android.R.attr.state_pressed to + colorUtil.adjustOpacity( + dynamicColor.primary().getArgb(scheme), + SURFACE_OPACITY_BUTTON_DISABLED + ) + ) + + companion object { + private const val SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.12f + private const val ON_SURFACE_OPACITY_BUTTON_OUTLINE_DISABLED: Float = 0.12f + private const val ON_SURFACE_OPACITY_BUTTON_DISABLED: Float = 0.38f + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt index ce8ed864..a5b9c025 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt @@ -21,15 +21,9 @@ import dynamiccolor.MaterialDynamicColors internal object SwitchColorUtils { private const val SWITCH_COMPAT_TRACK_ALPHA: Int = 77 - data class SwitchColors( - val thumbColor: ColorStateList, - val trackColor: ColorStateList - ) + data class SwitchColors(val thumbColor: ColorStateList, val trackColor: ColorStateList) - fun calculateSwitchColors( - context: Context, - scheme: DynamicScheme - ): SwitchColors { + fun calculateSwitchColors(context: Context, scheme: DynamicScheme): SwitchColors { val dynamicColor = MaterialDynamicColors() val thumbUncheckedColor = @@ -55,15 +49,15 @@ internal object SwitchColorUtils { return SwitchColors( thumbColor = - ColorStateList( - arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), - intArrayOf(dynamicColor.primary().getArgb(scheme), thumbUncheckedColor) - ), + ColorStateList( + arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), + intArrayOf(dynamicColor.primary().getArgb(scheme), thumbUncheckedColor) + ), trackColor = - ColorStateList( - arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), - intArrayOf(trackColor, trackUncheckedColor) - ) + ColorStateList( + arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()), + intArrayOf(trackColor, trackUncheckedColor) + ) ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt index 1f6f4c51..fca33d23 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt @@ -50,9 +50,7 @@ fun AppCompatActivity.applyEdgeToEdgeWithSystemBarPadding( * @warning On Android 15+, this is a visual workaround only. The actual status bar * color cannot be modified and will remain transparent with system-managed contrast. */ -fun AppCompatActivity.initStatusBar( - @ColorInt color: Int -) { +fun AppCompatActivity.initStatusBar(@ColorInt color: Int) { window.decorView.setOnApplyWindowInsetsListener { view, insets -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { val statusBarHeight = insets.getInsets(WindowInsets.Type.statusBars()).top diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/SchemeExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/SchemeExtensions.kt index 5dd57071..aabdd239 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/SchemeExtensions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/SchemeExtensions.kt @@ -13,54 +13,53 @@ import androidx.compose.ui.graphics.Color import dynamiccolor.DynamicScheme import dynamiccolor.MaterialDynamicColors -fun DynamicScheme.toColorScheme(): ColorScheme = - ColorScheme( - primary = Color(MaterialDynamicColors().primary().getArgb(this)), - onPrimary = Color(MaterialDynamicColors().onPrimary().getArgb(this)), - primaryContainer = Color(MaterialDynamicColors().primaryContainer().getArgb(this)), - onPrimaryContainer = Color(MaterialDynamicColors().onPrimaryContainer().getArgb(this)), - inversePrimary = Color(MaterialDynamicColors().inversePrimary().getArgb(this)), - secondary = Color(MaterialDynamicColors().secondary().getArgb(this)), - onSecondary = Color(MaterialDynamicColors().onSecondary().getArgb(this)), - secondaryContainer = Color(MaterialDynamicColors().secondaryContainer().getArgb(this)), - onSecondaryContainer = Color(MaterialDynamicColors().onSecondaryContainer().getArgb(this)), - tertiary = Color(MaterialDynamicColors().tertiary().getArgb(this)), - onTertiary = Color(MaterialDynamicColors().onTertiary().getArgb(this)), - tertiaryContainer = Color(MaterialDynamicColors().tertiaryContainer().getArgb(this)), - onTertiaryContainer = Color(MaterialDynamicColors().onTertiaryContainer().getArgb(this)), - background = Color(MaterialDynamicColors().background().getArgb(this)), - onBackground = Color(MaterialDynamicColors().onBackground().getArgb(this)), - surface = Color(MaterialDynamicColors().surface().getArgb(this)), - onSurface = Color(MaterialDynamicColors().onSurface().getArgb(this)), - surfaceVariant = Color(MaterialDynamicColors().surfaceVariant().getArgb(this)), - onSurfaceVariant = Color(MaterialDynamicColors().onSurfaceVariant().getArgb(this)), - surfaceTint = Color(MaterialDynamicColors().surfaceVariant().getArgb(this)), - inverseSurface = Color(MaterialDynamicColors().inverseSurface().getArgb(this)), - inverseOnSurface = Color(MaterialDynamicColors().inverseOnSurface().getArgb(this)), - error = Color(MaterialDynamicColors().error().getArgb(this)), - onError = Color(MaterialDynamicColors().onError().getArgb(this)), - errorContainer = Color(MaterialDynamicColors().errorContainer().getArgb(this)), - onErrorContainer = Color(MaterialDynamicColors().onErrorContainer().getArgb(this)), - outline = Color(MaterialDynamicColors().outline().getArgb(this)), - outlineVariant = Color(MaterialDynamicColors().outlineVariant().getArgb(this)), - scrim = Color(MaterialDynamicColors().scrim().getArgb(this)), - surfaceBright = Color(MaterialDynamicColors().surfaceBright().getArgb(this)), - surfaceDim = Color(MaterialDynamicColors().surfaceDim().getArgb(this)), - surfaceContainer = Color(MaterialDynamicColors().surfaceContainer().getArgb(this)), - surfaceContainerHigh = Color(MaterialDynamicColors().surfaceContainerHigh().getArgb(this)), - surfaceContainerHighest = Color(MaterialDynamicColors().surfaceContainerHighest().getArgb(this)), - surfaceContainerLow = Color(MaterialDynamicColors().surfaceContainerLow().getArgb(this)), - surfaceContainerLowest = Color(MaterialDynamicColors().surfaceContainerLowest().getArgb(this)), - primaryFixed = Color(MaterialDynamicColors().primaryFixed().getArgb(this)), - primaryFixedDim = Color(MaterialDynamicColors().primaryFixedDim().getArgb(this)), - onPrimaryFixed = Color(MaterialDynamicColors().onPrimaryFixed().getArgb(this)), - onPrimaryFixedVariant = Color(MaterialDynamicColors().onPrimaryFixedVariant().getArgb(this)), - secondaryFixed = Color(MaterialDynamicColors().secondaryFixed().getArgb(this)), - secondaryFixedDim = Color(MaterialDynamicColors().secondaryFixedDim().getArgb(this)), - onSecondaryFixed = Color(MaterialDynamicColors().onSecondaryFixed().getArgb(this)), - onSecondaryFixedVariant = Color(MaterialDynamicColors().onSecondaryFixedVariant().getArgb(this)), - tertiaryFixed = Color(MaterialDynamicColors().tertiaryFixed().getArgb(this)), - tertiaryFixedDim = Color(MaterialDynamicColors().tertiaryFixedDim().getArgb(this)), - onTertiaryFixed = Color(MaterialDynamicColors().onTertiaryFixed().getArgb(this)), - onTertiaryFixedVariant = Color(MaterialDynamicColors().onTertiaryFixedVariant().getArgb(this)) - ) +fun DynamicScheme.toColorScheme(): ColorScheme = ColorScheme( + primary = Color(MaterialDynamicColors().primary().getArgb(this)), + onPrimary = Color(MaterialDynamicColors().onPrimary().getArgb(this)), + primaryContainer = Color(MaterialDynamicColors().primaryContainer().getArgb(this)), + onPrimaryContainer = Color(MaterialDynamicColors().onPrimaryContainer().getArgb(this)), + inversePrimary = Color(MaterialDynamicColors().inversePrimary().getArgb(this)), + secondary = Color(MaterialDynamicColors().secondary().getArgb(this)), + onSecondary = Color(MaterialDynamicColors().onSecondary().getArgb(this)), + secondaryContainer = Color(MaterialDynamicColors().secondaryContainer().getArgb(this)), + onSecondaryContainer = Color(MaterialDynamicColors().onSecondaryContainer().getArgb(this)), + tertiary = Color(MaterialDynamicColors().tertiary().getArgb(this)), + onTertiary = Color(MaterialDynamicColors().onTertiary().getArgb(this)), + tertiaryContainer = Color(MaterialDynamicColors().tertiaryContainer().getArgb(this)), + onTertiaryContainer = Color(MaterialDynamicColors().onTertiaryContainer().getArgb(this)), + background = Color(MaterialDynamicColors().background().getArgb(this)), + onBackground = Color(MaterialDynamicColors().onBackground().getArgb(this)), + surface = Color(MaterialDynamicColors().surface().getArgb(this)), + onSurface = Color(MaterialDynamicColors().onSurface().getArgb(this)), + surfaceVariant = Color(MaterialDynamicColors().surfaceVariant().getArgb(this)), + onSurfaceVariant = Color(MaterialDynamicColors().onSurfaceVariant().getArgb(this)), + surfaceTint = Color(MaterialDynamicColors().surfaceVariant().getArgb(this)), + inverseSurface = Color(MaterialDynamicColors().inverseSurface().getArgb(this)), + inverseOnSurface = Color(MaterialDynamicColors().inverseOnSurface().getArgb(this)), + error = Color(MaterialDynamicColors().error().getArgb(this)), + onError = Color(MaterialDynamicColors().onError().getArgb(this)), + errorContainer = Color(MaterialDynamicColors().errorContainer().getArgb(this)), + onErrorContainer = Color(MaterialDynamicColors().onErrorContainer().getArgb(this)), + outline = Color(MaterialDynamicColors().outline().getArgb(this)), + outlineVariant = Color(MaterialDynamicColors().outlineVariant().getArgb(this)), + scrim = Color(MaterialDynamicColors().scrim().getArgb(this)), + surfaceBright = Color(MaterialDynamicColors().surfaceBright().getArgb(this)), + surfaceDim = Color(MaterialDynamicColors().surfaceDim().getArgb(this)), + surfaceContainer = Color(MaterialDynamicColors().surfaceContainer().getArgb(this)), + surfaceContainerHigh = Color(MaterialDynamicColors().surfaceContainerHigh().getArgb(this)), + surfaceContainerHighest = Color(MaterialDynamicColors().surfaceContainerHighest().getArgb(this)), + surfaceContainerLow = Color(MaterialDynamicColors().surfaceContainerLow().getArgb(this)), + surfaceContainerLowest = Color(MaterialDynamicColors().surfaceContainerLowest().getArgb(this)), + primaryFixed = Color(MaterialDynamicColors().primaryFixed().getArgb(this)), + primaryFixedDim = Color(MaterialDynamicColors().primaryFixedDim().getArgb(this)), + onPrimaryFixed = Color(MaterialDynamicColors().onPrimaryFixed().getArgb(this)), + onPrimaryFixedVariant = Color(MaterialDynamicColors().onPrimaryFixedVariant().getArgb(this)), + secondaryFixed = Color(MaterialDynamicColors().secondaryFixed().getArgb(this)), + secondaryFixedDim = Color(MaterialDynamicColors().secondaryFixedDim().getArgb(this)), + onSecondaryFixed = Color(MaterialDynamicColors().onSecondaryFixed().getArgb(this)), + onSecondaryFixedVariant = Color(MaterialDynamicColors().onSecondaryFixedVariant().getArgb(this)), + tertiaryFixed = Color(MaterialDynamicColors().tertiaryFixed().getArgb(this)), + tertiaryFixedDim = Color(MaterialDynamicColors().tertiaryFixedDim().getArgb(this)), + onTertiaryFixed = Color(MaterialDynamicColors().onTertiaryFixed().getArgb(this)), + onTertiaryFixedVariant = Color(MaterialDynamicColors().onTertiaryFixedVariant().getArgb(this)) +) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index cae207bb..9d4e42ee 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -45,6 +45,40 @@ Failed to update share. Please try again in a few seconds. + Show recipients + Hide recipients + +%1$d + , + Remove recipient + More options + Permissions for %1$s + Custom permissions + + + %1$d person + %1$d people + + + %1$d group + %1$d groups + + + %1$d team + %1$d teams + + + %1$d email + %1$d emails + + + %1$d link + %1$d links + + + %1$d recipient + %1$d recipients + + Participants Participants Can… diff --git a/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt b/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt index c4661a51..4d37e3f6 100644 --- a/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt +++ b/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt @@ -18,7 +18,6 @@ import java.time.format.FormatStyle import java.util.Locale class ShareDateFormatterTest { - private lateinit var originalLocale: Locale @Before @@ -58,10 +57,11 @@ class ShareDateFormatterTest { } private fun assertLocalizedDisplay(locale: Locale) { - val expected = DateTimeFormatter - .ofLocalizedDate(FormatStyle.SHORT) - .withLocale(locale) - .format(DATE) + val expected = + DateTimeFormatter + .ofLocalizedDate(FormatStyle.SHORT) + .withLocale(locale) + .format(DATE) assertEquals(expected, displayFor(locale)) } From 1a1451a304f0491ac67ae2d24207cc58eab8ffcb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 14:23:18 +0200 Subject: [PATCH 151/174] wip Signed-off-by: alperozturk96 --- build.gradle | 3 +-- .../android/common/sample/MainActivity.kt | 1 + .../ui/component/ContentUnavailableView.kt | 2 ++ .../ui/network/http/NextcloudHttpClient.kt | 1 + .../network/http/NextcloudHttpClientLogger.kt | 8 ++++++-- .../android/common/ui/share/ShareScreen.kt | 2 ++ .../share/component/CollapsibleShareSection.kt | 2 ++ .../common/ui/share/component/CustomLink.kt | 2 ++ .../ui/share/component/RecipientAvatar.kt | 2 ++ .../ui/share/component/RecipientAvatarStack.kt | 2 ++ .../ui/share/component/SelectRecipientField.kt | 3 ++- .../ui/share/component/ShareContextMenu.kt | 2 ++ .../common/ui/share/component/ShareListItem.kt | 2 ++ .../ui/share/component/SharePermissionChip.kt | 2 ++ .../ui/share/component/ShareRecipientRow.kt | 2 ++ .../common/ui/share/component/ShareSwitch.kt | 2 ++ .../bottomsheet/AddOrEditShareBottomSheet.kt | 2 ++ .../QuickSharePermissionBottomSheet.kt | 2 ++ .../RecipientPermissionBottomSheet.kt | 2 ++ .../dialog/DeleteShareConfirmationDialog.kt | 2 ++ .../property/SharePropertyEnumField.kt | 2 ++ .../component/property/SharePropertyMessage.kt | 2 ++ .../component/property/SharePropertyView.kt | 2 ++ .../property/datepicker/ShareDatePicker.kt | 2 ++ .../common/ui/share/model/api/share/Share.kt | 9 +++++---- .../ui/share/repository/MockShareRepository.kt | 1 + .../share/repository/ShareRemoteRepository.kt | 1 + .../ui/share/repository/ShareRepository.kt | 1 + .../common/ui/share/viewmodel/ShareViewModel.kt | 1 + .../{StringExtensions.kt => Strings.kt} | 0 ui/src/main/res/drawable/ic_anyone.xml | 6 ++++++ ui/src/main/res/drawable/ic_calendar.xml | 17 ++++------------- ui/src/main/res/drawable/ic_cancel.xml | 17 ++++------------- ui/src/main/res/drawable/ic_person_add.xml | 6 ++++++ .../main/res/drawable/ic_person_add_filled.xml | 17 ++++------------- 35 files changed, 82 insertions(+), 48 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/{StringExtensions.kt => Strings.kt} (100%) diff --git a/build.gradle b/build.gradle index 53eb9fdf..cc41287a 100644 --- a/build.gradle +++ b/build.gradle @@ -61,8 +61,7 @@ subprojects { kotlin { target '**/*.kt' ktlint().editorConfigOverride([ - 'ktlint_function_naming_ignore_when_annotated_with': 'Composable', - 'ktlint_standard_filename' : 'disabled' + 'ktlint_function_naming_ignore_when_annotated_with': 'Composable' ]) } } diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt index bbbbe8c8..a4a7ab06 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt @@ -31,6 +31,7 @@ import com.nextcloud.android.common.ui.util.extensions.addSystemBarPaddings class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { val style = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) enableEdgeToEdge(style, style) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt index e5d6fd78..61bc9436 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.component import androidx.compose.foundation.Image diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index d0f9454f..ee5ff4fe 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -53,6 +53,7 @@ class NextcloudHttpClient private constructor( private fun errorMeta(code: Int): Meta = Meta(status = OCS_FAILURE, statusCode = code, message = "") + @Suppress("TooGenericExceptionCaught") suspend fun executeRequest( endpoint: String, method: HttpMethod, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt index da36f41d..6ca3c500 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt @@ -16,6 +16,9 @@ internal class NextcloudHttpClientLogger(private val enabled: Boolean) { companion object { private const val TAG = "NextcloudHttpClient" + private val successStatusCodes = 200..299 + private val redirectStatusCodes = 300..399 + private val prettyJson = Json { prettyPrint = true @@ -58,11 +61,12 @@ internal class NextcloudHttpClientLogger(private val enabled: Boolean) { private fun statusEmojiFor(code: Int, isError: Boolean): String = when { isError -> "🔴" - code in 200..299 -> "🟢" - code in 300..399 -> "🟡" + code in successStatusCodes -> "🟢" + code in redirectStatusCodes -> "🟡" else -> "🔴" } + @Suppress("TooGenericExceptionCaught") private fun RequestBody?.readableString(): String { if (this == null) return "" return try { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 220b36c0..8809f99e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming", "LongMethod") + package com.nextcloud.android.common.ui.share import androidx.compose.foundation.background diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt index a31eecf0..a089a808 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.animation.AnimatedVisibility diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index fc59caed..ff164656 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.layout.Column diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt index e97028e6..f66f5255 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.background diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt index 5d8a9dd2..26d360a4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.border diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 41f2b8e2..7fda2eb8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.isSystemInDarkTheme @@ -181,7 +183,6 @@ private fun RecipientDropdownContent(results: List, onSelect: (Recipi } } -// TODO: can this come from backend? @Composable fun PublicLinkIcon(modifier: Modifier = Modifier) { Icon( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt index b06a3eba..f485c618 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.layout.size diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index 8f283ab5..68bc0e6b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.animation.AnimatedVisibility diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt index 08f1035f..804f966b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.background diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt index 5e068e60..406d8a0c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.clickable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index d50f99f9..c00ecf8f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.layout.Row diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 81fa788e..8572c83c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +@file:Suppress("FunctionNaming", "LongMethod", "UnusedPrivateMember") + package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.layout.Arrangement diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt index 399910d6..b74162cb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.clickable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt index f9dfa566..3721a586 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.clickable diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt index b55eb7af..a6c757e9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component.dialog import androidx.compose.material3.AlertDialog diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt index 9331e88e..5428a394 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component.property import androidx.compose.foundation.layout.fillMaxWidth diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt index 77363da8..aa19b6a1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component.property import androidx.compose.foundation.layout.padding diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index aa38992a..979fb4da 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming") + package com.nextcloud.android.common.ui.share.component.property import androidx.compose.foundation.layout.Column diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index a8187db6..e794f4e8 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: MIT */ +@file:Suppress("FunctionNaming", "UnusedPrivateMember") + package com.nextcloud.android.common.ui.share.component.property.datepicker import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 67171514..d54f31ee 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -66,12 +66,13 @@ data class Share( fun toActiveShare(): ActiveShareState = ActiveShareState.Editing(this) fun getClipEntry(internalLink: String, category: ShareCategory): ClipEntry? { - if (category == ShareCategory.Anyone) { - val recipient = customLinkRecipient ?: return null - return recipient.secret.url?.let { ClipData.newPlainText(recipient.displayName, it).toClipEntry() } + if (category != ShareCategory.Anyone) { + return recipients.firstOrNull()?.let { ClipData.newPlainText(it.displayName, internalLink).toClipEntry() } } - return recipients.firstOrNull()?.let { ClipData.newPlainText(it.displayName, internalLink).toClipEntry() } + return customLinkRecipient?.let { recipient -> + recipient.secret.url?.let { ClipData.newPlainText(recipient.displayName, it).toClipEntry() } + } } fun getHeadline(context: Context, shares: List): String { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 41ad2f52..166ea86b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -32,6 +32,7 @@ import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.api.user.User +@Suppress("TooManyFunctions", "LongParameterList") class MockShareRepository : ShareRepository { private val mockOwner = User( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 4eb4ef62..aa6ca4cd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -35,6 +35,7 @@ import java.net.URLEncoder private fun String.urlEncoded(): String = URLEncoder.encode(this, Charsets.UTF_8.name()) +@Suppress("TooManyFunctions") class ShareRemoteRepository( private val client: NextcloudHttpClient, private val json: kotlinx.serialization.json.Json = OCSSerializer.json diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index c7918d1e..2ba00aa5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -21,6 +21,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateR import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState +@Suppress("TooManyFunctions") interface ShareRepository { suspend fun fetchRecipients( recipientTypeClasses: List?, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 114cd9b5..a3db8fa0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -55,6 +55,7 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeMark import kotlin.time.TimeSource +@Suppress("TooManyFunctions") class ShareViewModel( private val repository: ShareRepository, private val sourceId: String, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/StringExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/Strings.kt similarity index 100% rename from ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/StringExtensions.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/Strings.kt diff --git a/ui/src/main/res/drawable/ic_anyone.xml b/ui/src/main/res/drawable/ic_anyone.xml index 4e16a67b..e5d39e72 100644 --- a/ui/src/main/res/drawable/ic_anyone.xml +++ b/ui/src/main/res/drawable/ic_anyone.xml @@ -1,3 +1,9 @@ + + ~ SPDX-FileCopyrightText: 2018-2025 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> + ~ SPDX-FileCopyrightText: 2018-2025 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> diff --git a/ui/src/main/res/drawable/ic_person_add.xml b/ui/src/main/res/drawable/ic_person_add.xml index db9f2514..f3767fe0 100644 --- a/ui/src/main/res/drawable/ic_person_add.xml +++ b/ui/src/main/res/drawable/ic_person_add.xml @@ -1,3 +1,9 @@ + + ~ SPDX-FileCopyrightText: 2018-2025 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> From 96a4a412080a6078404da63635a3f97445fc9ee5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 14:30:01 +0200 Subject: [PATCH 152/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 10 --- .../ui/share/component/DeleteShareButton.kt | 70 +++++++++++++++++++ .../ui/share/component/ShareContextMenu.kt | 49 +------------ .../ui/share/component/ShareListItem.kt | 4 +- .../bottomsheet/AddOrEditShareBottomSheet.kt | 12 +++- .../QuickSharePermissionBottomSheet.kt | 4 +- .../RecipientPermissionBottomSheet.kt | 2 +- .../dialog/DeleteShareConfirmationDialog.kt | 2 +- .../ui/share/model/ui/ShareEditorEntry.kt | 3 +- .../ui/share/model/ui/ShareListItemActions.kt | 1 - .../common/ui/share/model/ui/ShareOverlay.kt | 11 --- ui/src/main/res/values/strings.xml | 3 +- 12 files changed, 87 insertions(+), 84 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 8809f99e..1c9eab1f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -86,7 +86,6 @@ import com.nextcloud.android.common.ui.share.component.ShareListItem import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePermissionBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.RecipientPermissionBottomSheet -import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption @@ -219,14 +218,6 @@ private fun ShareItemOverlay( ) } - is ShareOverlay.DeleteConfirmation -> DeleteShareConfirmationDialog( - onConfirm = { - onDismiss() - viewModel.deleteShare(share.id) - }, - onDismiss = onDismiss - ) - ShareOverlay.None -> Unit } } @@ -247,7 +238,6 @@ private fun ShareList( val actions = remember(viewModel, onShowOverlay) { ShareListItemActions( onSelectShare = { share -> viewModel.setActiveShare(share, ShareEditorEntry.EDIT) }, - onSendEmail = { share -> viewModel.setActiveShare(share, ShareEditorEntry.SEND_EMAIL) }, onToggleExpanded = { share -> expandedShareIds = if (share.id in expandedShareIds) { expandedShareIds - share.id diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt new file mode 100644 index 00000000..5807a740 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt @@ -0,0 +1,70 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +@file:Suppress("FunctionNaming") + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R +import com.nextcloud.android.common.ui.share.component.dialog.DeleteShareConfirmationDialog + +private val BUTTON_VERTICAL_PADDING = 8.dp +private val ICON_SIZE = 18.dp +private val ICON_SPACING = 8.dp + +@Composable +fun DeleteShareButton(onDelete: () -> Unit) { + var showConfirmation by remember { mutableStateOf(false) } + + Row(modifier = Modifier.fillMaxWidth().padding(vertical = BUTTON_VERTICAL_PADDING)) { + TextButton( + onClick = { showConfirmation = true }, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(ICON_SIZE) + ) + + Spacer(modifier = Modifier.width(ICON_SPACING)) + + Text(stringResource(R.string.share_view_delete_action)) + } + } + + if (!showConfirmation) return + + DeleteShareConfirmationDialog( + onConfirm = { + showConfirmation = false + onDelete() + }, + onDismiss = { showConfirmation = false } + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt index f485c618..91ce7916 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt @@ -11,14 +11,10 @@ package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,13 +25,7 @@ import com.nextcloud.android.common.ui.R private val MENU_ICON_SIZE = 18.dp @Composable -fun ShareContextMenu( - expanded: Boolean, - onDismiss: () -> Unit, - onEdit: () -> Unit, - onSendEmail: () -> Unit, - onDelete: () -> Unit -) { +fun ShareContextMenu(expanded: Boolean, onDismiss: () -> Unit, onEdit: () -> Unit) { DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { DropdownMenuItem( leadingIcon = { @@ -51,42 +41,5 @@ fun ShareContextMenu( onEdit() } ) - - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.AutoMirrored.Filled.Send, - contentDescription = null, - modifier = Modifier.size(MENU_ICON_SIZE) - ) - }, - text = { Text(stringResource(R.string.share_view_list_item_send_email)) }, - onClick = { - onDismiss() - onSendEmail() - } - ) - - HorizontalDivider() - - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - modifier = Modifier.size(MENU_ICON_SIZE) - ) - }, - text = { - Text( - text = stringResource(R.string.share_view_list_item_delete), - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { - onDismiss() - onDelete() - } - ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index 68bc0e6b..3b8cbaef 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -140,9 +140,7 @@ private fun ShareItemTrailingContent( ShareContextMenu( expanded = isContextMenuExpanded, onDismiss = { onContextMenuExpandedChange(false) }, - onEdit = { actions.onSelectShare(state.share) }, - onSendEmail = { actions.onSendEmail(state.share) }, - onDelete = { actions.onShowOverlay(ShareOverlay.DeleteConfirmation(state.share.id)) } + onEdit = { actions.onSelectShare(state.share) } ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 8572c83c..2c3d3f42 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -2,7 +2,7 @@ * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: MIT */ @file:Suppress("FunctionNaming", "LongMethod", "UnusedPrivateMember") @@ -64,6 +64,7 @@ import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.CustomLink +import com.nextcloud.android.common.ui.share.component.DeleteShareButton import com.nextcloud.android.common.ui.share.component.SelectRecipientField import com.nextcloud.android.common.ui.share.component.ShareSwitch import com.nextcloud.android.common.ui.share.component.property.SharePropertyView @@ -110,7 +111,7 @@ fun AddOrEditShareBottomSheet( var selectedCategory by rememberSaveable(share.id) { mutableStateOf(if (share.belongsAnyoneTab) ShareCategory.Anyone else ShareCategory.Invited) } - var showAdvancedSettings by rememberSaveable(share.id) { mutableStateOf(entry == ShareEditorEntry.SEND_EMAIL) } + var showAdvancedSettings by rememberSaveable(share.id) { mutableStateOf(false) } val context = LocalContext.current val propertyErrors by viewModel.propertyErrors.collectAsStateWithLifecycle() val pendingProperties by viewModel.pendingProperties.collectAsStateWithLifecycle() @@ -378,7 +379,8 @@ private fun AdvancedSettingsSection( propertyErrors: Map, viewModel: ShareViewModel ) { - if (!share.isAdvancedSectionAvailable) { + val isDeletable = share.shareState != ShareState.DRAFT + if (!share.isAdvancedSectionAvailable && !isDeletable) { return } @@ -406,6 +408,10 @@ private fun AdvancedSettingsSection( } ) } + + if (isDeletable) { + DeleteShareButton(onDelete = { viewModel.deleteShare(share.id) }) + } } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt index b74162cb..263e5c7f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt @@ -1,8 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: MIT */ @file:Suppress("FunctionNaming") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt index 3721a586..b849654e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -2,7 +2,7 @@ * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: MIT */ @file:Suppress("FunctionNaming") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt index a6c757e9..76897822 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt @@ -2,7 +2,7 @@ * Nextcloud Android Common Library * * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: MIT */ @file:Suppress("FunctionNaming") diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt index b4208f34..33e42aec 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt @@ -9,6 +9,5 @@ package com.nextcloud.android.common.ui.share.model.ui enum class ShareEditorEntry { EDIT, - CUSTOMIZE_PERMISSION, - SEND_EMAIL + CUSTOMIZE_PERMISSION } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt index cef5c5a1..8c912db0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt @@ -12,7 +12,6 @@ import com.nextcloud.android.common.ui.share.model.api.share.Share data class ShareListItemActions( val onSelectShare: (Share) -> Unit, - val onSendEmail: (Share) -> Unit, val onToggleExpanded: (Share) -> Unit, val onShowOverlay: (ShareOverlay) -> Unit, val onRemoveRecipient: (Share, Recipient) -> Unit diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt index 16aa8866..078ac46d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt @@ -19,8 +19,6 @@ sealed class ShareOverlay { data class QuickShare(override val shareId: String) : ShareOverlay() - data class DeleteConfirmation(override val shareId: String) : ShareOverlay() - data class RecipientPermission( override val shareId: String, val recipientClass: String, @@ -30,7 +28,6 @@ sealed class ShareOverlay { companion object { private const val QUICK_SHARE = "quick_share" - private const val DELETE_CONFIRMATION = "delete_confirmation" private const val RECIPIENT_PERMISSION = "recipient_permission" private const val RECIPIENT_CLASS_INDEX = 2 private const val RECIPIENT_VALUE_INDEX = 3 @@ -48,10 +45,6 @@ sealed class ShareOverlay { listOf(QUICK_SHARE, overlay.shareId) } - is DeleteConfirmation -> { - listOf(DELETE_CONFIRMATION, overlay.shareId) - } - is RecipientPermission -> { listOf( RECIPIENT_PERMISSION, @@ -70,10 +63,6 @@ sealed class ShareOverlay { shareId?.let(::QuickShare) } - DELETE_CONFIRMATION -> { - shareId?.let(::DeleteConfirmation) - } - RECIPIENT_PERMISSION -> { shareId?.let { RecipientPermission( diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 9d4e42ee..9ef96d8e 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -33,8 +33,7 @@ Share public link… Edit - Send email - Delete + Delete share Delete share? This share will be permanently removed. From 0842a2b57fb5b5a0f66b033fa468f17502ad039e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 15:17:54 +0200 Subject: [PATCH 153/174] wip Signed-off-by: alperozturk96 --- ui/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 9ef96d8e..4b5e073f 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Settings - Recipient + Participant Clear recipient Custom link @@ -33,13 +33,13 @@ Share public link… Edit - Delete share - Delete share? + Remove share + Remove share? This share will be permanently removed. Failed to fetch shares. Pull down to try again. - Failed to delete share. + Failed to remove share. Failed to create share. Failed to update share. Please try again in a few seconds. From 24d3767cda94bf581fe18a742b817ad238478459 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 15:27:32 +0200 Subject: [PATCH 154/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 5 +- .../ui/share/component/ShareContextMenu.kt | 45 ---------- .../ui/share/component/ShareListItem.kt | 82 ++++++------------- .../ui/share/component/ShareRecipientRow.kt | 60 ++------------ .../common/ui/share/component/ShareSwitch.kt | 6 +- .../RecipientPermissionBottomSheet.kt | 74 ++++++++++++++--- .../ui/share/model/ui/ShareListItemActions.kt | 4 +- .../ui/share/viewmodel/ShareViewModel.kt | 45 +++++++--- ui/src/main/res/values/strings.xml | 6 +- 9 files changed, 138 insertions(+), 189 deletions(-) delete mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 1c9eab1f..99e58761 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -245,10 +245,7 @@ private fun ShareList( expandedShareIds + share.id } }, - onShowOverlay = onShowOverlay, - onRemoveRecipient = { share, recipient -> - viewModel.removeRecipient(share.id, recipient.clazz, recipient.value, recipient.instance) - } + onShowOverlay = onShowOverlay ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt deleted file mode 100644 index 91ce7916..00000000 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareContextMenu.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Nextcloud Android Common Library - * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: MIT - */ - -@file:Suppress("FunctionNaming") - -package com.nextcloud.android.common.ui.share.component - -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.nextcloud.android.common.ui.R - -private val MENU_ICON_SIZE = 18.dp - -@Composable -fun ShareContextMenu(expanded: Boolean, onDismiss: () -> Unit, onEdit: () -> Unit) { - DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = null, - modifier = Modifier.size(MENU_ICON_SIZE) - ) - }, - text = { Text(stringResource(R.string.share_view_list_item_edit)) }, - onClick = { - onDismiss() - onEdit() - } - ) - } -} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index 3b8cbaef..b697c7fc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -11,15 +11,14 @@ package com.nextcloud.android.common.ui.share.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -27,18 +26,12 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.key -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.draw.rotate import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -60,8 +53,6 @@ private val LEADING_ICON_SIZE = 24.dp @Composable fun ShareListItem(state: ShareListItemState, permissionPresets: List, actions: ShareListItemActions) { val share = state.share - var contextMenuExpanded by remember { mutableStateOf(false) } - val haptics = LocalHapticFeedback.current Column( modifier = Modifier @@ -70,13 +61,7 @@ fun ShareListItem(state: ShareListItemState, permissionPresets: List Unit, - actions: ShareListItemActions -) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (state.share.hasMultipleRecipients) { - ExpandRecipientsButton( - isExpanded = state.isExpanded, - onClick = { actions.onToggleExpanded(state.share) } - ) - } - - Box { - IconButton(onClick = { onContextMenuExpandedChange(true) }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.share_view_list_item_more_options) - ) - } - - ShareContextMenu( - expanded = isContextMenuExpanded, - onDismiss = { onContextMenuExpandedChange(false) }, - onEdit = { actions.onSelectShare(state.share) } - ) - } +private fun ShareItemTrailingContent(state: ShareListItemState, actions: ShareListItemActions) { + if (state.share.hasMultipleRecipients) { + ExpandRecipientsButton( + isExpanded = state.isExpanded, + onClick = { actions.onToggleExpanded(state.share) } + ) + return } + + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) } @Composable @@ -164,8 +127,7 @@ private fun ShareRecipientList(share: Share, permissionPresets: List, - onEditPermissions: () -> Unit, - onRemove: () -> Unit + onEditPermissions: () -> Unit ) { ListItem( modifier = Modifier @@ -74,43 +62,13 @@ fun ShareRecipientRow( onClick = onEditPermissions ) }, - trailingContent = { RecipientRowMenu(onRemove = onRemove) }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent) - ) -} - -@Composable -private fun RecipientRowMenu(onRemove: () -> Unit) { - var expanded by remember { mutableStateOf(false) } - - Box { - IconButton(onClick = { expanded = true }) { + trailingContent = { Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.share_view_list_item_more_options) + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant ) - } - - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - DropdownMenuItem( - leadingIcon = { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - modifier = Modifier.size(MENU_ICON_SIZE) - ) - }, - text = { - Text( - text = stringResource(R.string.share_view_recipient_remove), - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { - expanded = false - onRemove() - } - ) - } - } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index c00ecf8f..200e12fd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun ShareSwitch(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { +fun ShareSwitch(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true) { Row( modifier = Modifier .fillMaxWidth() @@ -35,6 +35,7 @@ fun ShareSwitch(label: String, checked: Boolean, onCheckedChange: (Boolean) -> U Text( text = label, style = MaterialTheme.typography.bodyLarge, + color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.outline, modifier = Modifier.weight(1f) ) @@ -42,7 +43,8 @@ fun ShareSwitch(label: String, checked: Boolean, onCheckedChange: (Boolean) -> U Switch( checked = checked, - onCheckedChange = onCheckedChange + onCheckedChange = onCheckedChange, + enabled = enabled ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt index b849654e..e1da8899 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -41,7 +42,6 @@ import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.ui.PermissionPresetOption import com.nextcloud.android.common.ui.share.model.ui.effectivePermissionsFor -import com.nextcloud.android.common.ui.share.model.ui.grantedPermissions import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.model.ui.presetOption import com.nextcloud.android.common.ui.share.model.ui.recipientPresetOptions @@ -86,6 +86,7 @@ fun RecipientPermissionBottomSheet( PresetOptionItem( option = option, isSelected = option == if (showCustomPermissions) PermissionPresetOption.Custom else selectedOption, + isShareDefault = option.presetClass != null && option.presetClass == share.permissionPreset, onClick = { val presetClass = option.presetClass if (presetClass == null) { @@ -99,19 +100,49 @@ fun RecipientPermissionBottomSheet( ) } - if (!showCustomPermissions) return@Column + if (showCustomPermissions) { + HorizontalDivider() + + CustomPermissions( + share = share, + recipient = recipient, + viewModel = viewModel + ) + } HorizontalDivider() - CustomPermissions( - share = share, - recipient = recipient, - viewModel = viewModel + RemoveRecipientItem( + onClick = { + onDismiss() + viewModel.removeRecipient(share.id, recipient.clazz, recipient.value, recipient.instance) + } ) } } } +@Composable +private fun RemoveRecipientItem(onClick: () -> Unit) { + ListItem( + modifier = Modifier.clickable { onClick() }, + headlineContent = { + Text( + text = stringResource(R.string.share_view_recipient_remove), + color = MaterialTheme.colorScheme.error + ) + }, + leadingContent = { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) +} + @Composable private fun CustomPermissions(share: Share, recipient: Recipient, viewModel: ShareViewModel) { val effectivePermissions = share.effectivePermissionsFor(recipient) @@ -124,7 +155,7 @@ private fun CustomPermissions(share: Share, recipient: Recipient, viewModel: Sha modifier = Modifier.padding(vertical = CONTENT_PADDING) ) - share.grantedPermissions().forEach { permission -> + share.permissions.forEach { permission -> key(permission.clazz) { val isEnabled = effectivePermissions.first { it.clazz == permission.clazz }.enabled var checked by remember(permission.clazz, isEnabled) { mutableStateOf(isEnabled) } @@ -135,18 +166,41 @@ private fun CustomPermissions(share: Share, recipient: Recipient, viewModel: Sha onCheckedChange = { value -> checked = value viewModel.updateRecipientPermission(share.id, recipient, permission.clazz, value) - } + }, + enabled = permission.enabled ) } } + + Text( + text = stringResource(R.string.share_view_recipient_permission_cap_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = CONTENT_PADDING) + ) } } @Composable -private fun PresetOptionItem(option: PermissionPresetOption, isSelected: Boolean, onClick: () -> Unit) { +private fun PresetOptionItem( + option: PermissionPresetOption, + isSelected: Boolean, + isShareDefault: Boolean, + onClick: () -> Unit +) { + val label = option.label() + ListItem( modifier = Modifier.clickable { onClick() }, - headlineContent = { Text(option.label()) }, + headlineContent = { + Text( + text = if (isShareDefault) { + stringResource(R.string.share_view_recipient_permission_default, label) + } else { + label + } + ) + }, trailingContent = { if (isSelected) { Icon( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt index 8c912db0..1b627818 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt @@ -7,12 +7,10 @@ package com.nextcloud.android.common.ui.share.model.ui -import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share data class ShareListItemActions( val onSelectShare: (Share) -> Unit, val onToggleExpanded: (Share) -> Unit, - val onShowOverlay: (ShareOverlay) -> Unit, - val onRemoveRecipient: (Share, Recipient) -> Unit + val onShowOverlay: (ShareOverlay) -> Unit ) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index a3db8fa0..baa5e42c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -423,7 +423,7 @@ class ShareViewModel( // region permissions fun updateRecipientPermission(shareId: String, recipient: Recipient, clazz: String, enabled: Boolean) { - viewModelScope.launch { applyRecipientPermission(shareId, recipient, clazz, enabled) } + viewModelScope.launch { applyRecipientPermissions(shareId, recipient, mapOf(clazz to enabled)) } } fun updateRecipientPermissionPreset(shareId: String, recipient: Recipient, presetClass: String) { @@ -431,14 +431,27 @@ class ShareViewModel( val changes = share.recipientPermissionChanges(recipient, presetClass) if (changes.isEmpty()) return - viewModelScope.launch { - changes.forEach { (clazz, enabled) -> - applyRecipientPermission(shareId, recipient, clazz, enabled) ?: return@launch - } + viewModelScope.launch { applyRecipientPermissions(shareId, recipient, changes) } + } + + private suspend fun applyRecipientPermissions( + shareId: String, + recipient: Recipient, + changes: Map + ) { + var applied: Share? = null + var hasFailed = false + + changes.forEach { (clazz, enabled) -> + val updated = requestRecipientPermission(shareId, recipient, clazz, enabled) + if (updated == null) hasFailed = true else applied = updated } + + val resolved = if (hasFailed) fetchShareOrNull(shareId) else applied + resolved?.let { publishUpdatedShare(it) } } - private suspend fun applyRecipientPermission( + private suspend fun requestRecipientPermission( shareId: String, recipient: Recipient, clazz: String, @@ -453,13 +466,21 @@ class ShareViewModel( enabled = enabled ) val result = repository.updateShareRecipientPermission(shareId, request) - val updated = - result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } - ?: return null + return result.dataOrElse { _errorMessageId.update { updateErrorMessageId(result) } } + } - refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - return updated + private suspend fun fetchShareOrNull(shareId: String): Share? = + (repository.fetchShare(shareId) as? NetworkResult.Success)?.data + + private fun publishUpdatedShare(share: Share) { + refreshActiveShare(share.toActiveShare()) + replaceInList(share) + } + + private fun updateErrorMessageId(result: NetworkResult): Int = if (result.isRateLimited) { + R.string.share_view_rate_limited_message + } else { + R.string.share_view_update_error_message } fun updatePermission(id: String, clazz: String, enabled: Boolean) { diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 4b5e073f..b799cf3c 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -32,7 +32,6 @@ Send Share public link… - Edit Remove share Remove share? This share will be permanently removed. @@ -47,11 +46,12 @@ Show recipients Hide recipients +%1$d - , + ", " Remove recipient - More options Permissions for %1$s Custom permissions + %1$s (default) + You can only grant the same or fewer permissions than the share. %1$d person From 422cc3b6286671246665d7bdc5136aef9c48b0d9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 15:38:37 +0200 Subject: [PATCH 155/174] wip Signed-off-by: alperozturk96 --- .../ui/share/component/ShareListItem.kt | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index b697c7fc..e1f2669b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -78,7 +77,18 @@ fun ShareListItem(state: ShareListItemState, permissionPresets: List, actions: ShareListItemActions) { Column { From 2f2d49cfc142a36d5288362c1d3169ce1a2f3923 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 15:57:51 +0200 Subject: [PATCH 156/174] wip Signed-off-by: alperozturk96 --- .../ui/share/component/ShareListItem.kt | 42 +++++---------- .../ui/share/component/ShareRecipientRow.kt | 11 +--- .../ui/share/component/ShareTrailingIcon.kt | 52 +++++++++++++++++++ .../RecipientPermissionBottomSheet.kt | 49 +++++++++++------ 4 files changed, 98 insertions(+), 56 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index e1f2669b..f90e2e85 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -16,10 +16,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowDown 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 @@ -29,7 +26,6 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -44,8 +40,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareOverlay private const val ITEM_WIDTH_FRACTION = 0.9f private const val CONTAINER_ALPHA = 0.5f -private const val COLLAPSED_CHEVRON_ROTATION = -90f -private const val EXPANDED_CHEVRON_ROTATION = 0f private val LEADING_ICON_SIZE = 24.dp @@ -77,18 +71,7 @@ fun ShareListItem(state: ShareListItemState, permissionPresets: List Unit) { - val descriptionId = if (isExpanded) { +private fun ShareItemTrailingIcon(state: ShareListItemState, actions: ShareListItemActions) { + if (!state.share.hasMultipleRecipients) { + ShareTrailingIcon(onClick = { actions.onSelectShare(state.share) }) + return + } + + val descriptionId = if (state.isExpanded) { R.string.share_view_recipients_collapse } else { R.string.share_view_recipients_expand } - IconButton(onClick = onClick) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = stringResource(descriptionId), - modifier = Modifier.rotate( - if (isExpanded) EXPANDED_CHEVRON_ROTATION else COLLAPSED_CHEVRON_ROTATION - ) - ) - } + ShareTrailingIcon( + contentDescription = stringResource(descriptionId), + isPointingDown = state.isExpanded, + onClick = { actions.onToggleExpanded(state.share) } + ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt index 8da0dcf2..3bcb67b4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt @@ -11,9 +11,6 @@ package com.nextcloud.android.common.ui.share.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme @@ -62,13 +59,7 @@ fun ShareRecipientRow( onClick = onEditPermissions ) }, - trailingContent = { - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, + trailingContent = { ShareTrailingIcon() }, colors = ListItemDefaults.colors(containerColor = Color.Transparent) ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt new file mode 100644 index 00000000..5ca50f0d --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt @@ -0,0 +1,52 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +@file:Suppress("FunctionNaming") + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.unit.dp + +private val TOUCH_TARGET_SIZE = 48.dp +private const val POINTING_DOWN_ROTATION = 90f +private const val POINTING_END_ROTATION = 0f + +@Composable +fun ShareTrailingIcon( + contentDescription: String? = null, + isPointingDown: Boolean = false, + onClick: (() -> Unit)? = null +) { + val box = Modifier + .size(TOUCH_TARGET_SIZE) + .clip(CircleShape) + .let { if (onClick == null) it else it.clickable(onClick = onClick) } + + Box(modifier = box, contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.rotate( + if (isPointingDown) POINTING_DOWN_ROTATION else POINTING_END_ROTATION + ) + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt index e1da8899..fcf720bd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -11,8 +11,14 @@ package com.nextcloud.android.common.ui.share.component.bottomsheet import androidx.compose.foundation.clickable 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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete @@ -31,6 +37,7 @@ import androidx.compose.runtime.key 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.graphics.Color import androidx.compose.ui.res.stringResource @@ -49,6 +56,8 @@ import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel private val SHEET_BOTTOM_PADDING = 32.dp private val CONTENT_PADDING = 16.dp +private val REMOVE_VERTICAL_PADDING = 14.dp +private val REMOVE_ICON_SIZE = 22.dp @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -73,6 +82,7 @@ fun RecipientPermissionBottomSheet( Column( modifier = Modifier .fillMaxWidth() + .verticalScroll(rememberScrollState()) .padding(bottom = SHEET_BOTTOM_PADDING) ) { Text( @@ -124,23 +134,28 @@ fun RecipientPermissionBottomSheet( @Composable private fun RemoveRecipientItem(onClick: () -> Unit) { - ListItem( - modifier = Modifier.clickable { onClick() }, - headlineContent = { - Text( - text = stringResource(R.string.share_view_recipient_remove), - color = MaterialTheme.colorScheme.error - ) - }, - leadingContent = { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = CONTENT_PADDING, vertical = REMOVE_VERTICAL_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(REMOVE_ICON_SIZE) + ) + + Spacer(modifier = Modifier.width(CONTENT_PADDING)) + + Text( + text = stringResource(R.string.share_view_recipient_remove), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error + ) + } } @Composable From 41f78bd8e282d814ae87158c7fd1cc405599810e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 15:58:45 +0200 Subject: [PATCH 157/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 28 ------------------- .../share/component/SelectRecipientField.kt | 2 +- .../ui/share/component/ShareListItem.kt | 1 - .../ui/share/component/ShareRecipientRow.kt | 2 -- .../bottomsheet/AddOrEditShareBottomSheet.kt | 2 +- .../share/repository/MockShareRepository.kt | 2 +- .../share/repository/ShareRemoteRepository.kt | 4 +-- .../ui/share/repository/ShareRepository.kt | 2 +- .../ui/share/viewmodel/ShareViewModel.kt | 4 +-- 9 files changed, 8 insertions(+), 39 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 99e58761..1ad86e8e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -9,45 +9,23 @@ package com.nextcloud.android.common.ui.share -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Send -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api 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 import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -59,18 +37,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -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.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 7fda2eb8..63247812 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -44,10 +44,10 @@ import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.svg.SvgDecoder import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.model.api.icon.Icon import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel private val FIELD_VERTICAL_PADDING = 12.dp private val CHIP_SPACING = 8.dp diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index f90e2e85..8ab547b2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt index 3bcb67b4..211f1198 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt @@ -18,10 +18,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.share.Share diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 2c3d3f42..37007711 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -61,7 +61,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import com.nextcloud.android.common.ui.share.component.CollapsibleShareSection import com.nextcloud.android.common.ui.share.component.CustomLink import com.nextcloud.android.common.ui.share.component.DeleteShareButton @@ -84,6 +83,7 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareCategory import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.MockShareRepository +import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index 166ea86b..ff1e842e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -22,8 +22,8 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest -import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.secret.Secret diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index aa6ca4cd..6cd979ed 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -13,6 +13,7 @@ import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.OcsResponse import com.nextcloud.android.common.ui.network.serialization.OCSSerializer +import com.nextcloud.android.common.ui.share.model.api.apiValue import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilities import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest @@ -20,11 +21,10 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest -import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest -import com.nextcloud.android.common.ui.share.model.api.apiValue import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.android.common.ui.share.model.api.state.ShareState import kotlinx.serialization.json.JsonObject diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 2ba00aa5..132fec93 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -14,8 +14,8 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.GetShareRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest -import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index baa5e42c..965a5c9e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -20,8 +20,8 @@ import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientReque import com.nextcloud.android.common.ui.share.model.api.request.AddSourceRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionPresetRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePermissionRequest -import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateSharePropertyRequest +import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientPermissionRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share @@ -35,9 +35,9 @@ import com.nextcloud.android.common.ui.share.model.ui.recipientPermissionChanges import com.nextcloud.android.common.ui.share.repository.ShareRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow From f4f840590f70dee09e9dccff7b98041a6687c5ef Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 08:27:47 +0200 Subject: [PATCH 158/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 93 ++++++++++--------- .../ui/share/viewmodel/ShareViewModel.kt | 2 - 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 1ad86e8e..1459448b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -148,52 +148,6 @@ private fun ShareScreen(internalLink: String, viewModel: ShareViewModel) { ) } -@Composable -private fun ShareItemOverlay( - overlay: ShareOverlay, - shares: List, - permissionPresets: List, - onDismiss: () -> Unit, - viewModel: ShareViewModel -) { - val share = overlay.shareId?.let { id -> shares.firstOrNull { it.id == id } } ?: return - - when (overlay) { - is ShareOverlay.QuickShare -> QuickSharePermissionBottomSheet( - options = PermissionPresetOption.optionsFor(share, permissionPresets), - selectedOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), - onOptionSelected = { option -> - onDismiss() - val presetClass = option.presetClass - if (presetClass != null) { - viewModel.updatePermissionPreset(share.id, presetClass, updateActiveShare = false) - } else { - viewModel.setActiveShare(share, ShareEditorEntry.CUSTOMIZE_PERMISSION) - } - }, - onDismiss = onDismiss - ) - - is ShareOverlay.RecipientPermission -> { - val recipient = share.recipients.firstOrNull { - it.clazz == overlay.recipientClass && - it.value == overlay.recipientValue && - it.instance == overlay.recipientInstance - } ?: return - - RecipientPermissionBottomSheet( - share = share, - recipient = recipient, - permissionPresets = permissionPresets, - viewModel = viewModel, - onDismiss = onDismiss - ) - } - - ShareOverlay.None -> Unit - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ShareList( @@ -262,6 +216,53 @@ private fun ShareList( } } + +@Composable +private fun ShareItemOverlay( + overlay: ShareOverlay, + shares: List, + permissionPresets: List, + onDismiss: () -> Unit, + viewModel: ShareViewModel +) { + val share = overlay.shareId?.let { id -> shares.firstOrNull { it.id == id } } ?: return + + when (overlay) { + is ShareOverlay.QuickShare -> QuickSharePermissionBottomSheet( + options = PermissionPresetOption.optionsFor(share, permissionPresets), + selectedOption = PermissionPresetOption.from(share.permissionPreset, permissionPresets), + onOptionSelected = { option -> + onDismiss() + val presetClass = option.presetClass + if (presetClass != null) { + viewModel.updatePermissionPreset(share.id, presetClass, updateActiveShare = false) + } else { + viewModel.setActiveShare(share, ShareEditorEntry.CUSTOMIZE_PERMISSION) + } + }, + onDismiss = onDismiss + ) + + is ShareOverlay.RecipientPermission -> { + val recipient = share.recipients.firstOrNull { + it.clazz == overlay.recipientClass && + it.value == overlay.recipientValue && + it.instance == overlay.recipientInstance + } ?: return + + RecipientPermissionBottomSheet( + share = share, + recipient = recipient, + permissionPresets = permissionPresets, + viewModel = viewModel, + onDismiss = onDismiss + ) + } + + ShareOverlay.None -> Unit + } +} + @Composable private fun SharePlaceholder(state: ShareScreenState) { if (state is ShareScreenState.Error) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 965a5c9e..37f95b72 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -126,8 +126,6 @@ class ShareViewModel( } override fun onCleared() { - super.onCleared() - val draft = _activeShare.value.shareOrNull?.takeIf { it.shareState == ShareState.DRAFT } ?: return CoroutineScope(SupervisorJob()).launch { repository.deleteShare(draft.id) } } From f9de11866dd1cd4bb2e4da73d748285f894b39c6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 09:37:50 +0200 Subject: [PATCH 159/174] wip Signed-off-by: alperozturk96 --- .../ui/share/viewmodel/ShareViewModel.kt | 92 ++++++++++++------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 37f95b72..14f7cf89 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -48,7 +48,9 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -68,6 +70,7 @@ class ShareViewModel( private const val RECIPIENT_SEARCH_OFFSET = 0 private val DRAFT_CREATION_INTERVAL = 5.seconds + private val PENDING_MUTATION_JOIN_TIMEOUT = 15.seconds } private val savedState = ShareSavedState(savedStateHandle) @@ -105,6 +108,8 @@ class ShareViewModel( get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() private val propertyUpdateJobs = mutableMapOf() + private val pendingUpdateJobs = mutableSetOf() + private val activatingShareIds = mutableSetOf() val searchQuery: StateFlow = savedState.searchQuery @@ -261,14 +266,19 @@ class ShareViewModel( // region state fun updateState(id: String, shareState: ShareState) { viewModelScope.launch { - val updated = applyState(id, shareState) ?: return@launch + if (shareState == ShareState.ACTIVE) activatingShareIds += id + try { + val updated = applyState(id, shareState) ?: return@launch - if (shareState == ShareState.ACTIVE) { - updateActiveShare(ActiveShareState.None) - } else { - refreshActiveShare(updated.toActiveShare()) + if (shareState == ShareState.ACTIVE) { + updateActiveShare(ActiveShareState.None) + } else { + refreshActiveShare(updated.toActiveShare()) + } + replaceInList(updated) + } finally { + if (shareState == ShareState.ACTIVE) activatingShareIds -= id } - replaceInList(updated) } } @@ -277,14 +287,18 @@ class ShareViewModel( viewModelScope.launch { _isPreparingLink.update { true } + activatingShareIds += id - val updated = applyState(id, ShareState.ACTIVE) - if (updated != null) { - refreshActiveShare(ActiveShareState.Activating(updated)) - replaceInList(updated) + try { + val updated = applyState(id, ShareState.ACTIVE) + if (updated != null) { + refreshActiveShare(ActiveShareState.Activating(updated)) + replaceInList(updated) + } + } finally { + activatingShareIds -= id + _isPreparingLink.update { false } } - - _isPreparingLink.update { false } } } @@ -306,7 +320,6 @@ class ShareViewModel( result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) } // endregion @@ -318,8 +331,7 @@ class ShareViewModel( result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - } + }.trackPendingUpdateJobs() } fun selectCategory(category: ShareCategory, share: Share) { @@ -355,8 +367,7 @@ class ShareViewModel( result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - } + }.trackPendingUpdateJobs() } fun updateRecipientSecret(shareId: String, recipient: Recipient, secret: String) { @@ -375,8 +386,7 @@ class ShareViewModel( result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - } + }.trackPendingUpdateJobs() } suspend fun generateSecret(): String? { @@ -399,7 +409,6 @@ class ShareViewModel( is NetworkResult.Success -> { _propertyErrors.update { it - clazz } refreshActiveShare(result.data.toActiveShare()) - replaceInList(result.data) } is NetworkResult.ServerError -> { @@ -415,13 +424,15 @@ class ShareViewModel( } } _pendingProperties.update { it - clazz } - } + }.trackPendingUpdateJobs() } // endregion // region permissions fun updateRecipientPermission(shareId: String, recipient: Recipient, clazz: String, enabled: Boolean) { - viewModelScope.launch { applyRecipientPermissions(shareId, recipient, mapOf(clazz to enabled)) } + viewModelScope + .launch { applyRecipientPermissions(shareId, recipient, mapOf(clazz to enabled)) } + .trackPendingUpdateJobs() } fun updateRecipientPermissionPreset(shareId: String, recipient: Recipient, presetClass: String) { @@ -429,7 +440,9 @@ class ShareViewModel( val changes = share.recipientPermissionChanges(recipient, presetClass) if (changes.isEmpty()) return - viewModelScope.launch { applyRecipientPermissions(shareId, recipient, changes) } + viewModelScope + .launch { applyRecipientPermissions(shareId, recipient, changes) } + .trackPendingUpdateJobs() } private suspend fun applyRecipientPermissions( @@ -446,7 +459,7 @@ class ShareViewModel( } val resolved = if (hasFailed) fetchShareOrNull(shareId) else applied - resolved?.let { publishUpdatedShare(it) } + resolved?.let { refreshActiveShare(it.toActiveShare()) } } private suspend fun requestRecipientPermission( @@ -470,11 +483,6 @@ class ShareViewModel( private suspend fun fetchShareOrNull(shareId: String): Share? = (repository.fetchShare(shareId) as? NetworkResult.Success)?.data - private fun publishUpdatedShare(share: Share) { - refreshActiveShare(share.toActiveShare()) - replaceInList(share) - } - private fun updateErrorMessageId(result: NetworkResult): Int = if (result.isRateLimited) { R.string.share_view_rate_limited_message } else { @@ -488,8 +496,7 @@ class ShareViewModel( result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch refreshActiveShare(updated.toActiveShare()) - replaceInList(updated) - } + }.trackPendingUpdateJobs() } fun updatePermissionPreset(id: String, presetClass: String, updateActiveShare: Boolean) { @@ -501,8 +508,7 @@ class ShareViewModel( if (updateActiveShare) { refreshActiveShare(updated.toActiveShare()) } - replaceInList(updated) - } + }.trackPendingUpdateJobs() } // endregion @@ -531,12 +537,23 @@ class ShareViewModel( val active = _activeShare.value.shareOrNull ?: return if (active.id != id) return - if (active.shareState == ShareState.DRAFT) { - deleteShare(id) + when { + id in activatingShareIds -> Unit + active.shareState == ShareState.DRAFT -> deleteShare(id) + else -> refreshSharesAfterPendingUpdates() } setActiveShare(null) } + private fun refreshSharesAfterPendingUpdates() { + viewModelScope.launch { + withTimeoutOrNull(PENDING_MUTATION_JOIN_TIMEOUT) { + pendingUpdateJobs.toList().joinAll() + } + refreshShares() + } + } + fun setActiveShare(value: Share?, entry: ShareEditorEntry = ShareEditorEntry.EDIT) { _propertyErrors.update { emptyMap() } _pendingProperties.update { emptySet() } @@ -562,6 +579,11 @@ class ShareViewModel( updateActiveShare(value) } + private fun Job.trackPendingUpdateJobs(): Job = also { job -> + pendingUpdateJobs += job + job.invokeOnCompletion { pendingUpdateJobs -= job } + } + // The list only ever holds active shares, so a draft being edited never reaches it and an // activated one is inserted rather than silently dropped. private fun replaceInList(updated: Share) { From 38895820e205316ffc9827e52d4b3a3a5470e947 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 10:12:43 +0200 Subject: [PATCH 160/174] wip Signed-off-by: alperozturk96 --- .../ui/share/viewmodel/ShareViewModel.kt | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 14f7cf89..85080b51 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -157,7 +157,6 @@ class ShareViewModel( val restored = savedState.activeShareId?.let { fetchRestorableShare(it) } ?: return updateActiveShare(ActiveShareState.Editing(restored)) - replaceInList(restored) } private suspend fun fetchRestorableShare(id: String): Share? { @@ -518,7 +517,10 @@ class ShareViewModel( val result = repository.deleteShare(id) result.dataOrElse { _errorMessageId.update { R.string.share_view_delete_error_message } } ?: return@launch - publishShares(currentShares.filterNot { it.id == id }) + _state.update { state -> + val shares = ((state as? ShareScreenState.Loaded)?.shares ?: emptyList()).filterNot { it.id == id } + if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares) + } val editingShare = _activeShare.value if (editingShare is ActiveShareState.Editing && editingShare.share.id == id) { @@ -584,22 +586,18 @@ class ShareViewModel( job.invokeOnCompletion { pendingUpdateJobs -= job } } - // The list only ever holds active shares, so a draft being edited never reaches it and an - // activated one is inserted rather than silently dropped. private fun replaceInList(updated: Share) { - val current = currentShares - val index = current.indexOfFirst { it.id == updated.id } - val isActive = updated.shareState == ShareState.ACTIVE - - val shares = - when { - index >= 0 && isActive -> current.toMutableList().apply { this[index] = updated } - index >= 0 -> current.filterNot { it.id == updated.id } - isActive -> listOf(updated) + current - else -> return - } - - publishShares(shares) + _state.update { state -> + val current = (state as? ShareScreenState.Loaded)?.shares ?: emptyList() + val index = current.indexOfFirst { it.id == updated.id } + val shares = + if (index >= 0) { + current.toMutableList().apply { this[index] = updated } + } else { + listOf(updated) + current + } + ShareScreenState.Loaded(shares) + } } private fun publishShares(shares: List) { From 539494831bf5a934cd360dc3b6b34c05db3c2426 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 10:16:10 +0200 Subject: [PATCH 161/174] wip Signed-off-by: alperozturk96 --- .../component/property/datepicker/ShareDatePicker.kt | 4 ++-- ...SelectableDates.kt => FutureOnlySelectableDates.kt} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/{NoPastSelectableDates.kt => FutureOnlySelectableDates.kt} (61%) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index e794f4e8..89c526e1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.nextcloud.android.common.ui.R -import com.nextcloud.android.common.ui.share.component.property.datepicker.util.NoPastSelectableDates +import com.nextcloud.android.common.ui.share.component.property.datepicker.util.FutureOnlySelectableDates import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.util.extensions.Strings @@ -103,7 +103,7 @@ private fun DatePickerModal( onDismiss: () -> Unit ) { val datePickerState = rememberDatePickerState( - selectableDates = remember { NoPastSelectableDates() } + selectableDates = remember { FutureOnlySelectableDates() } ) DatePickerDialog( diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt similarity index 61% rename from ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt rename to ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt index 5888b2bb..5e0fc528 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/NoPastSelectableDates.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt @@ -11,11 +11,11 @@ import androidx.compose.material3.SelectableDates import java.time.LocalDate import java.time.ZoneOffset -class NoPastSelectableDates : SelectableDates { - private val today: LocalDate = LocalDate.now(ZoneOffset.UTC) - private val todayUtcMillis: Long = today.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() +class FutureOnlySelectableDates : SelectableDates { + private val tomorrow: LocalDate = LocalDate.now(ZoneOffset.UTC).plusDays(1) + private val tomorrowUtcMillis: Long = tomorrow.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() - override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= todayUtcMillis + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= tomorrowUtcMillis - override fun isSelectableYear(year: Int): Boolean = year >= today.year + override fun isSelectableYear(year: Int): Boolean = year >= tomorrow.year } From 5417dea7ab8da089cb6da0b737dcfe3ee3ffd23c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 10:27:25 +0200 Subject: [PATCH 162/174] wip Signed-off-by: alperozturk96 --- .../common/ui/network/model/NetworkResult.kt | 5 --- .../ui/share/viewmodel/ShareViewModel.kt | 41 ++----------------- ui/src/main/res/values/strings.xml | 1 - 3 files changed, 4 insertions(+), 43 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt index 2fcb0fcd..a3fa0a1e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt @@ -7,8 +7,6 @@ package com.nextcloud.android.common.ui.network.model -private const val HTTP_TOO_MANY_REQUESTS = 429 - sealed class NetworkResult { data class Success(val data: T) : NetworkResult() @@ -23,9 +21,6 @@ sealed class NetworkResult { } } -val NetworkResult<*>.isRateLimited: Boolean - get() = this is NetworkResult.ServerError && response.ocs.meta.statusCode == HTTP_TOO_MANY_REQUESTS - inline fun NetworkResult.dataOrElse(onError: () -> Unit): T? = when (this) { is NetworkResult.Success -> { data diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 85080b51..1ede0fd2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.viewModelScope import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.network.model.NetworkResult import com.nextcloud.android.common.ui.network.model.dataOrElse -import com.nextcloud.android.common.ui.network.model.isRateLimited import com.nextcloud.android.common.ui.share.model.api.permission.PermissionPreset import com.nextcloud.android.common.ui.share.model.api.recipients.Recipient import com.nextcloud.android.common.ui.share.model.api.request.AddRecipientRequest @@ -54,8 +53,6 @@ import kotlinx.coroutines.withTimeoutOrNull import java.util.UUID import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -import kotlin.time.TimeMark -import kotlin.time.TimeSource @Suppress("TooManyFunctions") class ShareViewModel( @@ -68,8 +65,6 @@ class ShareViewModel( private const val SEARCH_SUBSCRIPTION_TIMEOUT = 5_000L private const val RECIPIENT_SEARCH_LIMIT = 10 private const val RECIPIENT_SEARCH_OFFSET = 0 - - private val DRAFT_CREATION_INTERVAL = 5.seconds private val PENDING_MUTATION_JOIN_TIMEOUT = 15.seconds } @@ -124,8 +119,6 @@ class ShareViewModel( private var secretUpdateJob: Job? = null - private var lastDraftCreation: TimeMark? = null - init { loadInitialData() } @@ -220,20 +213,16 @@ class ShareViewModel( // region create fun createDraftShare() { - if (isWithinDraftCreationInterval()) { - _errorMessageId.update { R.string.share_view_rate_limited_message } - return - } - if (!isCreatingDraft.compareAndSet(expect = false, update = true)) return viewModelScope.launch { try { _errorMessageId.update { null } - lastDraftCreation = TimeSource.Monotonic.markNow() val result = repository.createDraftShare() - val draft = result.dataOrElse { onDraftCreationFailed(result) } ?: return@launch + val draft = result.dataOrElse { + _errorMessageId.update { R.string.share_view_create_error_message } + } ?: return@launch updateEditorEntry(ShareEditorEntry.EDIT) updateActiveShare(draft.toActiveShare()) @@ -244,22 +233,6 @@ class ShareViewModel( } } } - - private fun onDraftCreationFailed(result: NetworkResult) { - val messageId = - if (result.isRateLimited) { - R.string.share_view_rate_limited_message - } else { - R.string.share_view_create_error_message - } - - _errorMessageId.update { messageId } - } - - private fun isWithinDraftCreationInterval(): Boolean { - val elapsed = lastDraftCreation?.elapsedNow() ?: return false - return elapsed < DRAFT_CREATION_INTERVAL - } // endregion // region state @@ -476,18 +449,12 @@ class ShareViewModel( enabled = enabled ) val result = repository.updateShareRecipientPermission(shareId, request) - return result.dataOrElse { _errorMessageId.update { updateErrorMessageId(result) } } + return result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } } private suspend fun fetchShareOrNull(shareId: String): Share? = (repository.fetchShare(shareId) as? NetworkResult.Success)?.data - private fun updateErrorMessageId(result: NetworkResult): Int = if (result.isRateLimited) { - R.string.share_view_rate_limited_message - } else { - R.string.share_view_update_error_message - } - fun updatePermission(id: String, clazz: String, enabled: Boolean) { viewModelScope.launch { val result = repository.updateSharePermission(id, UpdateSharePermissionRequest(clazz, enabled)) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index b799cf3c..bfcc51ea 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -41,7 +41,6 @@ Failed to remove share. Failed to create share. Failed to update share. - Please try again in a few seconds. Show recipients Hide recipients From 0bc236e3b8ca5a2e241b1270841a969f8902dc41 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 11:02:24 +0200 Subject: [PATCH 163/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 12 +- .../ui/share/component/ShareListShimmer.kt | 153 ++++++++++++++++++ .../ui/share/viewmodel/ShareViewModel.kt | 2 +- 3 files changed, 156 insertions(+), 11 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 1459448b..b9ffded7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton @@ -55,6 +54,7 @@ import com.nextcloud.android.common.ui.component.ContentUnavailableView import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient import com.nextcloud.android.common.ui.share.component.ShareListItem +import com.nextcloud.android.common.ui.share.component.ShareListShimmer import com.nextcloud.android.common.ui.share.component.bottomsheet.AddOrEditShareBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.QuickSharePermissionBottomSheet import com.nextcloud.android.common.ui.share.component.bottomsheet.RecipientPermissionBottomSheet @@ -111,14 +111,7 @@ private fun ShareScreen(internalLink: String, viewModel: ShareViewModel) { containerColor = Color.Transparent ) { paddingValues -> when (val state = screenState) { - is ShareScreenState.Loading -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + is ShareScreenState.Loading -> ShareListShimmer(modifier = Modifier.padding(paddingValues)) else -> ShareList( state = state, @@ -216,7 +209,6 @@ private fun ShareList( } } - @Composable private fun ShareItemOverlay( overlay: ShareOverlay, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt new file mode 100644 index 00000000..d05fe057 --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt @@ -0,0 +1,153 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +@file:Suppress("FunctionNaming") + +package com.nextcloud.android.common.ui.share.component + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +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.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.share.model.ui.ShareItemType + +private const val SHIMMER_ROW_COUNT = 6 +private const val ITEM_WIDTH_FRACTION = 0.9f +private const val CONTAINER_ALPHA = 0.5f +private const val HEADLINE_WIDTH_FRACTION = 0.5f +private const val CHIP_CORNER_PERCENT = 50 +private const val SHIMMER_MIN_ALPHA = 0.12f +private const val SHIMMER_MAX_ALPHA = 0.28f +private const val SHIMMER_ANIMATION_DURATION_MILLIS = 900 + +private val FIRST_ITEM_TOP_SPACING = 16.dp +private val ITEM_SPACING = 2.dp +private val ROW_PADDING = 16.dp +private val CONTENT_SPACING = 16.dp +private val LEADING_ICON_SIZE = 24.dp +private val TRAILING_ICON_SIZE = 48.dp +private val HEADLINE_HEIGHT = 16.dp +private val LINE_SPACING = 8.dp +private val CHIP_WIDTH = 90.dp +private val CHIP_HEIGHT = 20.dp +private val BAR_CORNER_RADIUS = 4.dp + +@Composable +fun ShareListShimmer(modifier: Modifier = Modifier) { + val alpha = rememberShimmerAlpha() + + LazyColumn( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + userScrollEnabled = false + ) { + items(SHIMMER_ROW_COUNT) { index -> + Spacer(modifier = Modifier.height(if (index == 0) FIRST_ITEM_TOP_SPACING else ITEM_SPACING)) + ShareListItemShimmer( + type = ShareItemType.type(index, SHIMMER_ROW_COUNT - 1), + alpha = alpha + ) + } + } +} + +@Composable +private fun rememberShimmerAlpha(): Float { + val transition = rememberInfiniteTransition(label = "shareListShimmer") + val alpha by transition.animateFloat( + initialValue = SHIMMER_MIN_ALPHA, + targetValue = SHIMMER_MAX_ALPHA, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = SHIMMER_ANIMATION_DURATION_MILLIS, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "shareListShimmerAlpha" + ) + return alpha +} + +@Composable +private fun ShareListItemShimmer(type: ShareItemType, alpha: Float) { + Column( + modifier = Modifier + .fillMaxWidth(ITEM_WIDTH_FRACTION) + .clip(type.getShape()) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = CONTAINER_ALPHA)) + .padding(ROW_PADDING) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + ShimmerBox( + modifier = Modifier + .size(LEADING_ICON_SIZE) + .clip(CircleShape), + alpha = alpha + ) + + Spacer(modifier = Modifier.width(CONTENT_SPACING)) + + Column(modifier = Modifier.weight(1f)) { + ShimmerBox( + modifier = Modifier + .fillMaxWidth(HEADLINE_WIDTH_FRACTION) + .height(HEADLINE_HEIGHT) + .clip(RoundedCornerShape(BAR_CORNER_RADIUS)), + alpha = alpha + ) + + Spacer(modifier = Modifier.height(LINE_SPACING)) + + ShimmerBox( + modifier = Modifier + .width(CHIP_WIDTH) + .height(CHIP_HEIGHT) + .clip(RoundedCornerShape(percent = CHIP_CORNER_PERCENT)), + alpha = alpha + ) + } + + Spacer(modifier = Modifier.width(CONTENT_SPACING)) + + ShimmerBox( + modifier = Modifier + .size(TRAILING_ICON_SIZE) + .clip(CircleShape), + alpha = alpha + ) + } + } +} + +@Composable +private fun ShimmerBox(modifier: Modifier, alpha: Float) { + Box( + modifier = modifier.background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha)) + ) +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 1ede0fd2..b0d85821 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -449,7 +449,7 @@ class ShareViewModel( enabled = enabled ) val result = repository.updateShareRecipientPermission(shareId, request) - return result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } + return result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } } private suspend fun fetchShareOrNull(shareId: String): Share? = From 5ae852f46d180d952d80148dd05be05585aaaa69 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 12:14:01 +0200 Subject: [PATCH 164/174] wip Signed-off-by: alperozturk96 --- .../RecipientPermissionBottomSheet.kt | 2 +- .../ui/share/viewmodel/ShareViewModel.kt | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt index fcf720bd..9cae6b4a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -105,7 +105,7 @@ fun RecipientPermissionBottomSheet( } onDismiss() - viewModel.updateRecipientPermissionPreset(share.id, recipient, presetClass) + viewModel.updateRecipientPermissionPreset(share, recipient, presetClass) } ) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index b0d85821..1ecfcb0a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -99,9 +99,6 @@ class ShareViewModel( private val _pendingProperties = MutableStateFlow>(emptySet()) val pendingProperties: StateFlow> = _pendingProperties.asStateFlow() - private val currentShares: List - get() = (_state.value as? ShareScreenState.Loaded)?.shares ?: emptyList() - private val propertyUpdateJobs = mutableMapOf() private val pendingUpdateJobs = mutableSetOf() private val activatingShareIds = mutableSetOf() @@ -339,7 +336,8 @@ class ShareViewModel( result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return@launch refreshActiveShare(updated.toActiveShare()) - }.trackPendingUpdateJobs() + replaceInList(updated) + } } fun updateRecipientSecret(shareId: String, recipient: Recipient, secret: String) { @@ -402,19 +400,18 @@ class ShareViewModel( // region permissions fun updateRecipientPermission(shareId: String, recipient: Recipient, clazz: String, enabled: Boolean) { - viewModelScope - .launch { applyRecipientPermissions(shareId, recipient, mapOf(clazz to enabled)) } - .trackPendingUpdateJobs() + viewModelScope.launch { + applyRecipientPermissions(shareId, recipient, mapOf(clazz to enabled)) + } } - fun updateRecipientPermissionPreset(shareId: String, recipient: Recipient, presetClass: String) { - val share = currentShares.firstOrNull { it.id == shareId } ?: return + fun updateRecipientPermissionPreset(share: Share, recipient: Recipient, presetClass: String) { val changes = share.recipientPermissionChanges(recipient, presetClass) if (changes.isEmpty()) return - viewModelScope - .launch { applyRecipientPermissions(shareId, recipient, changes) } - .trackPendingUpdateJobs() + viewModelScope.launch { + applyRecipientPermissions(share.id, recipient, changes) + } } private suspend fun applyRecipientPermissions( @@ -431,7 +428,10 @@ class ShareViewModel( } val resolved = if (hasFailed) fetchShareOrNull(shareId) else applied - resolved?.let { refreshActiveShare(it.toActiveShare()) } + resolved?.let { + refreshActiveShare(it.toActiveShare()) + replaceInList(it) + } } private suspend fun requestRecipientPermission( @@ -474,7 +474,8 @@ class ShareViewModel( if (updateActiveShare) { refreshActiveShare(updated.toActiveShare()) } - }.trackPendingUpdateJobs() + replaceInList(updated) + } } // endregion From c1b5e67a5c918413677cd818a7d8258086970eba Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 12:49:39 +0200 Subject: [PATCH 165/174] wip Signed-off-by: alperozturk96 --- .../ui/component/ContentUnavailableView.kt | 43 +++++++++++++++---- .../android/common/ui/share/ShareScreen.kt | 1 + ui/src/main/res/drawable/ic_error.xml | 11 +++++ 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 ui/src/main/res/drawable/ic_error.xml diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt index 61bc9436..97c1b0cd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: MIT */ -@file:Suppress("FunctionNaming") +@file:Suppress("FunctionNaming", "UnusedPrivateMember") package com.nextcloud.android.common.ui.component @@ -22,43 +22,68 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import com.nextcloud.android.common.ui.R + +private val CONTENT_PADDING = 24.dp +private val ICON_SIZE = 64.dp +private val ICON_TITLE_SPACING = 16.dp +private val TITLE_DESCRIPTION_SPACING = 4.dp @Composable fun ContentUnavailableView(title: String, description: String? = null, iconId: Int? = null) { Column( modifier = Modifier .fillMaxSize() - .padding(16.dp), + .padding(CONTENT_PADDING), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { iconId?.let { Image( painter = painterResource(iconId), - modifier = Modifier.size(48.dp), - contentDescription = "" + modifier = Modifier.size(ICON_SIZE), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), + contentDescription = null ) + + Spacer(modifier = Modifier.height(ICON_TITLE_SPACING)) } Text( text = title, - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center ) description?.let { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(TITLE_DESCRIPTION_SPACING)) Text( text = description, - style = MaterialTheme.typography.headlineSmall, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center ) - - Spacer(modifier = Modifier.height(32.dp)) } } } + +@PreviewLightDark +@Composable +private fun ContentUnavailableViewPreview() { + MaterialTheme { + ContentUnavailableView( + iconId = R.drawable.ic_error, + title = stringResource(R.string.share_view_fetch_error_message), + description = stringResource(R.string.share_view_fetch_error_description) + ) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index b9ffded7..7c81a74a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -259,6 +259,7 @@ private fun ShareItemOverlay( private fun SharePlaceholder(state: ShareScreenState) { if (state is ShareScreenState.Error) { ContentUnavailableView( + iconId = R.drawable.ic_error, title = stringResource(R.string.share_view_fetch_error_message), description = stringResource(R.string.share_view_fetch_error_description) ) diff --git a/ui/src/main/res/drawable/ic_error.xml b/ui/src/main/res/drawable/ic_error.xml new file mode 100644 index 00000000..2d63c638 --- /dev/null +++ b/ui/src/main/res/drawable/ic_error.xml @@ -0,0 +1,11 @@ + + + + + + From 140b9760b0d03754b9751ceb98c848ba104013c6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 12:50:01 +0200 Subject: [PATCH 166/174] wip Signed-off-by: alperozturk96 --- ui/src/main/res/drawable/ic_error.xml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/src/main/res/drawable/ic_error.xml b/ui/src/main/res/drawable/ic_error.xml index 2d63c638..812532d0 100644 --- a/ui/src/main/res/drawable/ic_error.xml +++ b/ui/src/main/res/drawable/ic_error.xml @@ -1,11 +1,18 @@ - - - - + ~ SPDX-FileCopyrightText: 2018-2025 Google LLC + ~ SPDX-License-Identifier: Apache-2.0 +--> + + + + From bc53ee35587306cce61121d7df84ecfe06bae2e4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 13:21:00 +0200 Subject: [PATCH 167/174] wip Signed-off-by: alperozturk96 --- gradle/verification-keyring.keys | 111 +++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index 7a6489f1..7d6aa8af 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -1304,6 +1304,21 @@ fW1AkBVEk6siyL8PXfxmj9ev3H9xiQVLyJ6HpdHTLVjHjFkgNOLd =R7zg -----END PGP PUBLIC KEY BLOCK----- +pub CFAE163B64AC9189 +uid JetBrains Compose Team + +sub 57CE36BB68F1BC57 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEYYx3eRYJKwYBBAHaRw8BAQdAV7zh1T+xL7mD2O63rTIvRfQ9kwL2Gvq/Q6PD +9apCe2K0LkpldEJyYWlucyBDb21wb3NlIFRlYW0gPGNvbXBvc2VAamV0YnJhaW5z +LmNvbT64OARhjHd5EgorBgEEAZdVAQUBAQdA/AOwflH/rum1g1gvFPBcqhvFOcN8 +0VUb7OqoENSzUD4DAQgHiH4EGBYKACYWIQQgcjpjmbwGAVQoOzfPrhY7ZKyRiQUC +YYx3eQIbDAUJA8JnAAAKCRDPrhY7ZKyRiSq0AQCEwGRGd5MOTT2kKJf9CphX0LOs +qVBBSNvzoloV04wzywD+P4TmIz/CsigszDQy8yl1/rGvszo7gaUwnfnqN61JHgA= +=Y19b +-----END PGP PUBLIC KEY BLOCK----- + pub D041CAD2E452550F uid Deanna @@ -2389,6 +2404,34 @@ xV+8+asS+ID6jUp5FJ4izH6U90j4iiBfqIu+UEw+0gvD+TVqpqcj5pNlgU45HA== =Cl60 -----END PGP PUBLIC KEY BLOCK----- +pub 1188B69F6D6259CA +uid Chris Banes + +sub 0888B86856F9D71A +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsBNBF2hcBkBCAC2H5WcFoeByKBUAjRDjmP+5P6FRsZjLe6c1wy7G1ha6/EQUVK4 +gZUZYE9W7l/4QKHvAu4PvFWdD+5FXGZuB/2kw348CtabAlJTehm1QlPt5//ODkxB +fWPPz3uHBo5PQJZuLpStQn+aEkTTHK6Sk5033e6fa7mV5X/c8pTmTzkunG58NFbj +5VSVbks4pbafKoMY7rSN0/I2hEApCjB9tx/7DJuQ9gbaGhmabnhBTnwHXEV1/hIF +369lfiqeoeqDMOKj99C6KFD/NPRrRLfoPRpqL3LPsPp7P+TzyZN2q89Xqn2ysyI9 +jDtrlssEXLskU5kA8fVa179V/QR6QtrJ29m/ABEBAAG0HENocmlzIEJhbmVzIDxj +aHJpc0BiYW5lcy5tZT7OwE0EXaFwGQEIALaSv8Um4LBbyPM8PIfcSwl5sqBQal1n +w7461u8gGAi3uikn62Q7IGO3SRRuJcR6hkY5Dyq1qPwEPmcBjK0Nlx1NTtk4TwDT +ZsxexNV40qQ+BuZhCb0NmiQBRBY7o3GiFa8mqcNeTNnIxvqMYInIYcvHps3r77QN +Y8PZznfDNpJkuD/vvoFnDjncdaQd+Kia2gqAFHQMw6DnV1oCyPx38CqK10rw3DPt +yYo/sVo67mVLJZvoXx255DbKNATKSrhiuCIsK9r+lIvY9zXoI2cJFsNA1QSiDkt1 +yjW1us89+LMt5dVCanq0bfhprU4J3ZYqzUS1sVAekVY2jRw/ssidX+sAEQEAAcLA +fAQYAQgAJhYhBGS5sJ8WSqC/iHQuthGItp9tYlnKBQJdoXAZAhsMBQkDwmcAAAoJ +EBGItp9tYlnKg94H/2GsZGvWK8+dA4y0AmCtP4B6/WAUonGBMmUH1Ax8GaPKIQuC +ePBbQI5v2dGBqUOTjTh/qcpwfTHSMSWFRTv2HZDP82gY/tAHJrKZ14+Pr1JB4Nvd +PIj60dKFfyveC3t8TJ4kVjKvvDzGY7oHsZ1OmW1Nytsbe6Taj57UY8v67zpLRCEb +ZHIEo4hYtehEIlCWi7M2qJmXtWK/wU3bw0C8KL4Kun968+0005iOdQ/Iig4oI4K/ +UW2HkfQM1AwuwJlsc8uXUvH4A3kkVHTGnvvi4aTMtDWPKpdNu/eufWwuMrS6Gxdu +v7wjtN+wTGesv0OYCfxHScTi49XbBbU3PBq+pxQ= +=KzdM +-----END PGP PUBLIC KEY BLOCK----- + pub 15C71C0A4E0B8EDD uid Matthias Bläsing @@ -3529,6 +3572,34 @@ AGg2/CyrKszDpsR8R04jHL/ZAxrodA4Awc3BTpNxQDPfdRPc8w== =SIQO -----END PGP PUBLIC KEY BLOCK----- +pub 4D176DC503FB4267 +uid Colin White + +sub 5686B45C142551D3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF1EtnUBCAChtyYd/4eMAxTz5DVmO+8QOrTA1cf9bprQhtXD5pVbw8/IGKN+ +EqXmvt7AGy+4O633g7ec5iyirwCfEP+4YDv8k1LOvY9C5+tOwfK+FxAPRVc1AAB5 +q23x4yMI7aDdvN52/jqpREeBBWPcrnEIET68RApayALenjUP6Kx5K0ge9aU8uaSB +HsVpHSr05L1nKuuqh0LDNXIYDLCpo0LnwPNfLrJ2xT6gNTwHBY5mncDy4m01rdxD +3to/s2Uu/9Xkfz3+BJKRD2kr5txiqoW69H/qsg7u8tTOr9FhB1T21pjFZsdpzbOq +cKd7UyD0RpyQYHmTeUIhKSI0Su7pKT+RTL9BABEBAAG0IUNvbGluIFdoaXRlIDxj +b2xpbkBjb2xpbndoaXRlLm1lPrkBDQRdRLZ1AQgAxOP3klOByUo1Kyl1O6rZqj9e +yts/oXtuDTISfcRkZyg6fmkhT4kpd4xLSP3xHvRwJugybyTedDHzXXCOSjl3EFfU +GaKJuMqSKs5YjQOWk8S9BegAPeiq6PGV6gbHZQ3Xqy+XE+TLy5N96zu7th/YJraE +NpS79sj//mJQE2d49YrxhZwtMj64X0B8/mDmED+D2cPXAoLxNh//LaV26gpOC9yV +YTXrCq7ODPE5LyoljhBmPZxoapcn/39V6UvVSG8Dq6R5QdahDgNnCEjVPtSzGtbC +B4zpSv/LvZkm8gKlaxUcra5clXL7p4NWGx1C1Ap4+H6U2h53lC7FuYl8k9mBNwAR +AQABiQE2BBgBCAAgAhsMFiEE3yXTxIaP7BdxgvrWTRdtxQP7QmcFAl1P4JcACgkQ +TRdtxQP7QmcrkAgAiSZ2VRgHTY8SV4dXXjYBO6WM2Yzamt4bO5Uqflw7gPO44AtJ +h+Pn89iz37RnGL+bAq2MCNsgV4G+KaLJrE0c/rt/szC7UmwEJmy4DcH5B80wzBvo +2kpN1Uyr7+1+IgHogL6HjLxccoBpawpm0AnhNYrd2ml7STBCfNmilztWGrTkerb/ +2qOY7PmgWEad7whM0OEj1OE+M2IYZUga3gxf0B94GtQNwmI4xv5qXGskTFAlM9ko +94NfvOxEkgCGtnicN4Md74od0POEVrZxBpXuJGt60XVGvOr1iLmlpypWo+PkHhZe +Wt45mNAFy6vraTYYCkGXQKqx1kxg+OurYgIFgw== +=zwEJ +-----END PGP PUBLIC KEY BLOCK----- + pub 530AA5F25C25011F uid Gary Gregory @@ -3689,6 +3760,34 @@ oLIHbQ4IMkRivPgSpuxw53Hofe7A =9YZ+ -----END PGP PUBLIC KEY BLOCK----- +pub 597852B0B140F0BC +uid AndroidSVG (Key for signing maven packages) + +sub E0A468B5546CD437 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFFDPRgBCAC7Z/0fkdEkWVCD5BCDrIj/YOZBFOLHAZWLiVhyX0iDO1fhJcj7 +8PKGiv+tnLz/FC2gk6ZJCbwbAlxb5hXLvPWe1SAn6KjOvqNKx+qZ9LNMo4dCqlBp ++tAB0yUQVAorl33vHJWV93WaiKwsFpl6Gzr4i5Fmo4G5isXaDMO6NXbNbx45tmfU +V1RPI3EcIl/h5jNyRlJqDkoNz3TmgVpt9UO9hH2cGsejdj0c+56/4+/OE+IH15pt +w+oRt1dmGFxLlSA6k60rIzc27stABX0ZXi0WoPKGW8psRnZKNVf8/9d8IaGQivWu +sytoVGsaUmIjlJnZYEIJtk7xOMrWqmJdqsHvABEBAAG0SkFuZHJvaWRTVkcgKEtl +eSBmb3Igc2lnbmluZyBtYXZlbiBwYWNrYWdlcykgPGFuZHJvaWRzdmdmZWVkYmFj +a0BnbWFpbC5jb20+uQENBFFDPRgBCADYyjiQvAnrxZ7j3sGXw0ExSbUm0ccOwZn5 +ENsUTOjVvHmqSEQEyWQea6JXN1+8EH3LUCTww8s4bZCEtqIDM0qbmrjAD5vqmFhq +U/Kcqu+nd4eHaObpvc8BiEHZCtGMkXx1Xcgs8j8RPBVGJgYe+6KAiyAPIoZdBxXv +L40X/wWGRfQvW3CIfq/9SzTsHQCpA60iEegOjImsfLyYbxoskM/W4qrjIIMki+qQ +3ePJLc+UIazRnHtHduW+vfEEqmHlVhceup4q6EdXpNP5YZ8w7KytMlXCklMtNapz +PQ+N4Ohpidr2IKuPud4ej9dr2NHJNip8xRaTpeDNkvZPHMieXX1nABEBAAGJAR4E +GAECAAkFAlFDPRgCGwwACgkQWXhSsLFA8LxY2QfzBI7TCC3MSigxB+xu6w2cb5yM +2cD22EJuKuW84F/84CHIyzQW2NvqZYu8Ekn6Za6VzX5DUUhNpKMsQTjs0Y1TckXA +P5inx7bqPn5FBKnaeJVzOhX0gWmr/ahgUW1NKd10jmIoCfhB6JmEbmiFzx4uwDgL +RttGzWtyHoxUE/HXC49WUtf2Txaz3Fmsv2w2wEKR6G7/+TMtOEJm4FKpr1dhVsaH +Y03O49Sl4CDbseRODa7osGgh1aB0dBf3e/cB7F79bNyE2Zi2RW7DE9QrrLmThkWQ +WUf7+NXNXdibtyh/xufvR7JXwrK1msSv7jeBKnO/4bwuQjrBr7WD6h7RxUl1 +=ES/b +-----END PGP PUBLIC KEY BLOCK----- + pub 59A252FB1199D873 uid Tagir Valeev @@ -4539,6 +4638,18 @@ ymOZIduHXX1X+KzlRZTXvv4tSFnIQ0mWY1ySiOJQJS2WABVwFpFc8rECm6eN =z4dc -----END PGP PUBLIC KEY BLOCK----- +pub 793FD5751A0F0780 +sub 59EF9FBBED4216F5 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEZzuZjRYJKwYBBAHaRw8BAQdA/FAoxAeXKY80R8UZ35NNr06SMFqC3Pv/LtP2 +sZS+7N7OOARnO5mNEgorBgEEAZdVAQUBAQdAcU+jcl01TAmiI2RmtjPv4c/sW4nE +TaR06bnQoHFL3HMDAQgHwn4EGBYKACYWIQQdIX+Ede7p8Zq43Wt5P9V1Gg8HgAUC +ZzuZjQIbDAUJEswDAAAKCRB5P9V1Gg8HgJgoAP9sdWX3itgWylKDw70h5G3ZLNSt +zqKyEx/3NURzCWXtUwD/dkcAz5qsu2LGQynLaRXgNI4ZVgTb2zUzZM0TbshIzAo= +=BMAA +-----END PGP PUBLIC KEY BLOCK----- + pub 79752DB6C966F0B8 sub AC9F6F1991913E30 -----BEGIN PGP PUBLIC KEY BLOCK----- From c8862c5dc836bcaf0567358a5f4e66cd756d99d3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 09:32:23 +0200 Subject: [PATCH 168/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 8 ++++- .../property/datepicker/ShareDatePicker.kt | 33 ++++++++++++++++++- .../common/ui/share/model/api/share/Share.kt | 2 +- .../model/api/share/ShareCreationOrder.kt | 21 ++++++++++++ .../ui/share/viewmodel/ShareViewModel.kt | 11 +++++-- 5 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 7c81a74a..fa9bbdfb 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -37,9 +37,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -74,6 +76,7 @@ import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModelFactory private val FIRST_ITEM_TOP_SPACING = 16.dp private val ITEM_SPACING = 2.dp +private val FAB_CLEARANCE = 88.dp private val ExpandedShareIdsSaver = listSaver, String>( save = { it.toList() }, @@ -176,7 +179,10 @@ private fun ShareList( .padding(paddingValues) ) { LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .nestedScroll(rememberNestedScrollInteropConnection()), + contentPadding = PaddingValues(bottom = FAB_CLEARANCE), horizontalAlignment = Alignment.CenterHorizontally ) { if (shares.isEmpty()) { diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 89c526e1..418b79ab 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -11,9 +11,15 @@ package com.nextcloud.android.common.ui.share.component.property.datepicker import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDefaults import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.DatePickerState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -28,15 +34,22 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.nextcloud.android.common.ui.R import com.nextcloud.android.common.ui.share.component.property.datepicker.util.FutureOnlySelectableDates import com.nextcloud.android.common.ui.share.component.property.datepicker.util.ShareDateFormatter import com.nextcloud.android.common.ui.share.model.api.property.PropertyDate import com.nextcloud.android.common.ui.util.extensions.Strings +private val TITLE_MIN_WINDOW_HEIGHT = 560.dp +private val TITLE_PADDING = PaddingValues(start = 24.dp, end = 12.dp, top = 16.dp) + @Composable fun ShareDatePicker(property: PropertyDate, isError: Boolean, onDateSelected: (String) -> Unit) { val formatter = remember { ShareDateFormatter() } @@ -105,6 +118,9 @@ private fun DatePickerModal( val datePickerState = rememberDatePickerState( selectableDates = remember { FutureOnlySelectableDates() } ) + val windowHeight = with(LocalDensity.current) { + LocalWindowInfo.current.containerSize.height.toDp() + } DatePickerDialog( onDismissRequest = onDismiss, @@ -125,7 +141,22 @@ private fun DatePickerModal( } } ) { - DatePicker(state = datePickerState) + DatePicker( + state = datePickerState, + modifier = Modifier.verticalScroll(rememberScrollState()), + title = datePickerTitle(datePickerState, windowHeight) + ) + } +} + +private fun datePickerTitle(state: DatePickerState, windowHeight: Dp): (@Composable () -> Unit)? { + if (windowHeight < TITLE_MIN_WINDOW_HEIGHT) return null + + return { + DatePickerDefaults.DatePickerTitle( + displayMode = state.displayMode, + modifier = Modifier.padding(TITLE_PADDING) + ) } } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index d54f31ee..2a27eb18 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -89,7 +89,7 @@ data class Share( return context.getString(R.string.share_view_public_link) } - val position = publicLinks.sortedBy { it.lastUpdated }.indexOfFirst { it.id == id } + 1 + val position = publicLinks.sortedWith(ShareCreationOrder).indexOfFirst { it.id == id } + 1 return context.getString(R.string.share_view_public_link_numbered, position) } diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt new file mode 100644 index 00000000..be887e1f --- /dev/null +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Android Common Library + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.android.common.ui.share.model.api.share + +object ShareCreationOrder : Comparator { + override fun compare(first: Share, second: Share): Int { + val firstId = first.id.toBigIntegerOrNull() + val secondId = second.id.toBigIntegerOrNull() + + if (firstId == null || secondId == null) { + return first.id.compareTo(second.id) + } + + return firstId.compareTo(secondId) + } +} diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 1ecfcb0a..d066ffc2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -24,6 +24,7 @@ import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipi import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareRecipientSecretRequest import com.nextcloud.android.common.ui.share.model.api.request.UpdateShareStateRequest import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.android.common.ui.share.model.api.share.ShareCreationOrder import com.nextcloud.android.common.ui.share.model.api.source.Source import com.nextcloud.android.common.ui.share.model.api.state.ShareState import com.nextcloud.android.common.ui.share.model.ui.ActiveShareState @@ -562,15 +563,19 @@ class ShareViewModel( if (index >= 0) { current.toMutableList().apply { this[index] = updated } } else { - listOf(updated) + current + current + updated } - ShareScreenState.Loaded(shares) + ShareScreenState.Loaded(shares.sortedWith(ShareCreationOrder)) } } private fun publishShares(shares: List) { _state.update { - if (shares.isEmpty()) ShareScreenState.Empty else ShareScreenState.Loaded(shares) + if (shares.isEmpty()) { + ShareScreenState.Empty + } else { + ShareScreenState.Loaded(shares.sortedWith(ShareCreationOrder)) + } } } // endregion From 2456fb53ff9a3506ada7754a743cf11780e9b96b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 09:42:49 +0200 Subject: [PATCH 169/174] wip Signed-off-by: alperozturk96 --- .../common/core/utils/ecosystem/AccountReceiverCallback.kt | 1 + .../android/common/core/utils/ecosystem/EcosystemApp.kt | 1 + .../android/common/core/utils/ecosystem/EcosystemManager.kt | 1 + core/src/main/res/values/strings.xml | 1 + .../android/common/core/utils/AccountNamePatternTest.kt | 1 + .../com/nextcloud/android/common/core/utils/EcosystemAppTest.kt | 1 + .../java/com/nextcloud/android/common/sample/MainActivity.kt | 1 + .../java/com/nextcloud/android/common/sample/MainViewModel.kt | 1 + sample/src/main/res/layout/activity_main.xml | 1 + .../main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt | 1 + .../android/common/ui/component/ContentUnavailableView.kt | 1 + .../com/nextcloud/android/common/ui/network/PredefinedStatus.kt | 1 + .../nextcloud/android/common/ui/network/UserStatusRepository.kt | 1 + .../nextcloud/android/common/ui/network/auth/AuthInterceptor.kt | 1 + .../android/common/ui/network/auth/ServerCredentials.kt | 1 + .../com/nextcloud/android/common/ui/network/http/HttpMethod.kt | 1 + .../android/common/ui/network/http/NextcloudHttpClient.kt | 1 + .../android/common/ui/network/http/NextcloudHttpClientLogger.kt | 1 + .../nextcloud/android/common/ui/network/http/RequestBuilder.kt | 1 + .../nextcloud/android/common/ui/network/model/NetworkResult.kt | 1 + .../com/nextcloud/android/common/ui/network/model/OcsResponse.kt | 1 + .../android/common/ui/network/serialization/OCSSerializer.kt | 1 + .../java/com/nextcloud/android/common/ui/share/ShareScreen.kt | 1 + .../android/common/ui/share/avatar/ShareAvatarRepository.kt | 1 + .../android/common/ui/share/component/CollapsibleShareSection.kt | 1 + .../nextcloud/android/common/ui/share/component/CustomLink.kt | 1 + .../android/common/ui/share/component/DeleteShareButton.kt | 1 + .../android/common/ui/share/component/RecipientAvatar.kt | 1 + .../android/common/ui/share/component/RecipientAvatarStack.kt | 1 + .../android/common/ui/share/component/SelectRecipientField.kt | 1 + .../nextcloud/android/common/ui/share/component/ShareListItem.kt | 1 + .../android/common/ui/share/component/ShareListShimmer.kt | 1 + .../android/common/ui/share/component/SharePermissionChip.kt | 1 + .../android/common/ui/share/component/ShareRecipientRow.kt | 1 + .../nextcloud/android/common/ui/share/component/ShareSwitch.kt | 1 + .../android/common/ui/share/component/ShareTrailingIcon.kt | 1 + .../ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt | 1 + .../component/bottomsheet/QuickSharePermissionBottomSheet.kt | 1 + .../component/bottomsheet/RecipientPermissionBottomSheet.kt | 1 + .../ui/share/component/dialog/DeleteShareConfirmationDialog.kt | 1 + .../common/ui/share/component/property/SharePropertyEnumField.kt | 1 + .../common/ui/share/component/property/SharePropertyMessage.kt | 1 + .../common/ui/share/component/property/SharePropertyView.kt | 1 + .../ui/share/component/property/datepicker/ShareDatePicker.kt | 1 + .../property/datepicker/util/FutureOnlySelectableDates.kt | 1 + .../component/property/datepicker/util/ShareDateFormatter.kt | 1 + .../android/common/ui/share/model/api/EnumExtensions.kt | 1 + .../ui/share/model/api/capabilities/SharingCapabilities.kt | 1 + .../android/common/ui/share/model/api/capabilities/SourceType.kt | 1 + .../com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt | 1 + .../android/common/ui/share/model/api/permission/Permission.kt | 1 + .../common/ui/share/model/api/permission/PermissionPreset.kt | 1 + .../android/common/ui/share/model/api/property/Property.kt | 1 + .../android/common/ui/share/model/api/recipients/Recipient.kt | 1 + .../android/common/ui/share/model/api/request/ShareRequests.kt | 1 + .../nextcloud/android/common/ui/share/model/api/secret/Secret.kt | 1 + .../nextcloud/android/common/ui/share/model/api/share/Share.kt | 1 + .../common/ui/share/model/api/share/ShareCreationOrder.kt | 1 + .../nextcloud/android/common/ui/share/model/api/source/Source.kt | 1 + .../android/common/ui/share/model/api/state/ShareState.kt | 1 + .../android/common/ui/share/model/api/state/ShareUserStatus.kt | 1 + .../com/nextcloud/android/common/ui/share/model/api/user/User.kt | 1 + .../android/common/ui/share/model/ui/ActiveShareState.kt | 1 + .../android/common/ui/share/model/ui/PermissionPresetOption.kt | 1 + .../common/ui/share/model/ui/PermissionPresetOptionLabel.kt | 1 + .../android/common/ui/share/model/ui/RecipientPermissions.kt | 1 + .../android/common/ui/share/model/ui/RecipientSummary.kt | 1 + .../nextcloud/android/common/ui/share/model/ui/ShareCategory.kt | 1 + .../android/common/ui/share/model/ui/ShareEditorEntry.kt | 1 + .../nextcloud/android/common/ui/share/model/ui/ShareItemType.kt | 1 + .../android/common/ui/share/model/ui/ShareListItemActions.kt | 1 + .../android/common/ui/share/model/ui/ShareListItemState.kt | 1 + .../nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt | 1 + .../android/common/ui/share/model/ui/ShareScreenState.kt | 1 + .../android/common/ui/share/repository/MockShareRepository.kt | 1 + .../android/common/ui/share/repository/ShareRemoteRepository.kt | 1 + .../android/common/ui/share/repository/ShareRepository.kt | 1 + .../android/common/ui/share/viewmodel/ShareSavedState.kt | 1 + .../android/common/ui/share/viewmodel/ShareViewModel.kt | 1 + .../android/common/ui/share/viewmodel/ShareViewModelFactory.kt | 1 + .../com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt | 1 + .../android/common/ui/theme/utils/AndroidViewThemeUtils.kt | 1 + .../android/common/ui/theme/utils/AndroidXViewThemeUtils.kt | 1 + .../android/common/ui/theme/utils/DialogViewThemeUtils.kt | 1 + .../android/common/ui/theme/utils/MaterialViewThemeUtils.kt | 1 + .../nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt | 1 + .../common/ui/util/extensions/AppCompatActivityExtensions.kt | 1 + .../com/nextcloud/android/common/ui/util/extensions/Strings.kt | 1 + .../android/common/ui/util/extensions/WindowExtensions.kt | 1 + ui/src/main/res/values/strings.xml | 1 + .../component/property/datepicker/util/ShareDateFormatterTest.kt | 1 + 91 files changed, 91 insertions(+) diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/AccountReceiverCallback.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/AccountReceiverCallback.kt index c655777f..efe42368 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/AccountReceiverCallback.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/AccountReceiverCallback.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt index 87a0f130..14e8df42 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemApp.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt index 9daf67f1..57f79c2b 100644 --- a/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt +++ b/core/src/main/java/com/nextcloud/android/common/core/utils/ecosystem/EcosystemManager.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index b38b3f67..aa0b34f4 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ diff --git a/core/src/test/java/com/nextcloud/android/common/core/utils/AccountNamePatternTest.kt b/core/src/test/java/com/nextcloud/android/common/core/utils/AccountNamePatternTest.kt index 1950d2a9..c861d2d9 100644 --- a/core/src/test/java/com/nextcloud/android/common/core/utils/AccountNamePatternTest.kt +++ b/core/src/test/java/com/nextcloud/android/common/core/utils/AccountNamePatternTest.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/core/src/test/java/com/nextcloud/android/common/core/utils/EcosystemAppTest.kt b/core/src/test/java/com/nextcloud/android/common/core/utils/EcosystemAppTest.kt index 628009d3..2ad03650 100644 --- a/core/src/test/java/com/nextcloud/android/common/core/utils/EcosystemAppTest.kt +++ b/core/src/test/java/com/nextcloud/android/common/core/utils/EcosystemAppTest.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt index a4a7ab06..f533ae8c 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainActivity.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2023 Andy Scherzinger * SPDX-FileCopyrightText: 2023 Stefan Niedermann diff --git a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt index 99e6f533..51f21bd5 100644 --- a/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt +++ b/sample/src/main/java/com/nextcloud/android/common/sample/MainViewModel.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2023 Stefan Niedermann * SPDX-License-Identifier: MIT diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 2e4f232b..79ea423f 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -2,6 +2,7 @@ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt b/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt index 5c9c335e..10d7d340 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/color/ColorUtil.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger * SPDX-FileCopyrightText: 2022 Álvaro Brey diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt index 97c1b0cd..4fae6511 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/component/ContentUnavailableView.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt index 817e8def..052371c2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/PredefinedStatus.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt index b7ee92fb..40bcca8c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/UserStatusRepository.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt index 654d0313..af24de26 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/AuthInterceptor.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt index ffb2a18c..ceed33bf 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/auth/ServerCredentials.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt index b03aa4c0..e13f2643 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/HttpMethod.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt index ee5ff4fe..4f1a2629 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClient.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt index 6ca3c500..cd6f3e3d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/NextcloudHttpClientLogger.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt index 3bbaf7a2..946988a2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/http/RequestBuilder.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt index a3fa0a1e..3b69edce 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/NetworkResult.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt index af1c40cc..af559cae 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/model/OcsResponse.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt b/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt index aae4ab75..bcefafa5 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/network/serialization/OCSSerializer.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index fa9bbdfb..525e19a9 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt index d05adc9f..6206067b 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/avatar/ShareAvatarRepository.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt index a089a808..d3e4ce70 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CollapsibleShareSection.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt index ff164656..373543ba 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/CustomLink.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt index 5807a740..8fe2b721 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/DeleteShareButton.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt index f66f5255..e29b58ab 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatar.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt index 26d360a4..63f868b6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/RecipientAvatarStack.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt index 63247812..dca34421 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SelectRecipientField.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt index 8ab547b2..3e7ee4bd 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListItem.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt index d05fe057..3bac4535 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareListShimmer.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt index 804f966b..fb99ba47 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/SharePermissionChip.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt index 211f1198..5f14d926 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareRecipientRow.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt index 200e12fd..64ff7132 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareSwitch.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt index 5ca50f0d..bd97cb41 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/ShareTrailingIcon.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 37007711..3d896ae3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt index 263e5c7f..d3d9dcec 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/QuickSharePermissionBottomSheet.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt index 9cae6b4a..76713f49 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/RecipientPermissionBottomSheet.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt index 76897822..bdbeef75 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/dialog/DeleteShareConfirmationDialog.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt index 5428a394..2310f621 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyEnumField.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt index aa19b6a1..c5229ca4 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyMessage.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt index 979fb4da..094179e0 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/SharePropertyView.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt index 418b79ab..2992a1c2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/ShareDatePicker.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt index 5e0fc528..f020608f 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/FutureOnlySelectableDates.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt index 84d963a3..b4943899 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatter.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt index 109c69d6..2f2bc6ca 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/EnumExtensions.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt index 33884193..9e5fcca7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SharingCapabilities.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt index a1d4d945..9480c5a7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/capabilities/SourceType.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt index 8b122817..584e9f02 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/icon/Icon.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt index c0e9f07c..4f763cce 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/Permission.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt index f884c132..56b95d7c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/permission/PermissionPreset.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt index 07f3aeb7..bdc07b66 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/property/Property.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt index 94b38c55..55771472 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/recipients/Recipient.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt index c6a24f60..3fb56fd7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/request/ShareRequests.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt index 2a9a29e6..921d5bc6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/secret/Secret.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt index 2a27eb18..b37fcea6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/Share.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt index be887e1f..c2091de2 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/share/ShareCreationOrder.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt index e9eaea43..165b0977 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/source/Source.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt index 2846dc06..4efd2333 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareState.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt index b8bf6044..7560c742 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/state/ShareUserStatus.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt index 73745398..9384a013 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/api/user/User.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt index 2cd8b708..6cd968be 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ActiveShareState.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt index 819c4f4a..7522d4c3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOption.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt index f36a5e28..7f37d4a1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/PermissionPresetOptionLabel.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt index fe0a70c2..de63341a 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientPermissions.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt index a7a75064..e4883bee 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/RecipientSummary.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt index 6522188e..115b2bbf 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareCategory.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt index 33e42aec..f6584467 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareEditorEntry.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt index 92e6a21e..36b81043 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareItemType.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt index 1b627818..cc622c73 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemActions.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt index 28fd4af0..f80afd20 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareListItemState.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt index 078ac46d..f6211b2d 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareOverlay.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt index ef96dc23..0bae4057 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/model/ui/ShareScreenState.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt index ff1e842e..929591e3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/MockShareRepository.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt index 6cd979ed..532d8509 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRemoteRepository.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt index 132fec93..ed3b8d14 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/repository/ShareRepository.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt index 3436fd5b..b2ded119 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareSavedState.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index d066ffc2..95a25584 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt index 728e9049..7a74855c 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModelFactory.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt index 82c1d6ef..d8a8b975 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/ViewThemeUtilsBase.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2024-2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger * SPDX-FileCopyrightText: 2022 Álvaro Brey diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt index 5731b58e..3a0096d6 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidViewThemeUtils.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger * SPDX-FileCopyrightText: 2023 Stefan Niedermann diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt index 9eeaaa1e..9cfd1c24 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/AndroidXViewThemeUtils.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger * SPDX-FileCopyrightText: 2023 Thore Goebel diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt index c12db763..eba54e8e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/DialogViewThemeUtils.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger * SPDX-FileCopyrightText: 2022 Álvaro Brey diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt index ca3185a4..4cc60d4e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/MaterialViewThemeUtils.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022-2023 Andy Scherzinger * SPDX-FileCopyrightText: 2023 Stefan Niedermann diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt index a5b9c025..e52a3ca3 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/theme/utils/SwitchColorUtils.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-License-Identifier: MIT diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt index fca33d23..8c0aefd7 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/AppCompatActivityExtensions.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2025-2026 Alper Ozturk * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/Strings.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/Strings.kt index 954245fe..00d83581 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/Strings.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/Strings.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/WindowExtensions.kt b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/WindowExtensions.kt index 6f5c9e74..6e9a699e 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/WindowExtensions.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/util/extensions/WindowExtensions.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index bfcc51ea..268cd2e3 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ diff --git a/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt b/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt index 4d37e3f6..6e1098a0 100644 --- a/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt +++ b/ui/src/test/java/com/nextcloud/android/common/ui/share/component/property/datepicker/util/ShareDateFormatterTest.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ From 05145b1a8d927ac2191183533ea25701b56fe77b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 09:54:08 +0200 Subject: [PATCH 170/174] wip Signed-off-by: alperozturk96 --- build.gradle | 8 ++++++ detekt.yml | 15 ++++++++++ gradle/verification-metadata.xml | 48 ++++++++++++++++++++++++++++++++ ui/build.gradle | 17 +++++------ 4 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 detekt.yml diff --git a/build.gradle b/build.gradle index cc41287a..bdbc915b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ @@ -9,6 +10,12 @@ buildscript { ext { daggerVersion = "2.60.1" kotlinVersion = "2.4.20" + lifecycleVersion = "2.11.0" + composeUiToolingVersion = "1.12.0" + coilVersion = "3.6.2" + okHttpVersion = "5.5.0" + composeBomVersion = "2026.08.00" + kotlinxSerializationVersion = "1.11.0" jUnitVersion = "4.13.2" androidXTestVersion = "1.3.0" androidXEspressoTestVersion = "3.7.0" @@ -67,5 +74,6 @@ subprojects { } detekt { buildUponDefaultConfig = true + config.setFrom(rootProject.file('detekt.yml')) } } diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..1360f33b --- /dev/null +++ b/detekt.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +# Applied on top of the detekt default config (buildUponDefaultConfig = true). +comments: + UndocumentedPublicClass: + active: false + UndocumentedPublicFunction: + active: false + UndocumentedPublicProperty: + active: false + +complexity: + LabeledExpression: + active: false diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ee463678..6cf0a25d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -44,6 +44,9 @@ + + + @@ -253,6 +256,7 @@ + @@ -15713,6 +15717,14 @@ + + + + + + + + @@ -15721,11 +15733,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -20954,6 +20997,11 @@ + + + + + diff --git a/ui/build.gradle b/ui/build.gradle index e180ed50..4d03eb60 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -1,6 +1,7 @@ /* * Nextcloud Android Common Library * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: MIT */ @@ -48,11 +49,11 @@ android { dependencies { // region Jetpack Compose - implementation 'androidx.compose.ui:ui-tooling-preview:1.11.4' - debugImplementation 'androidx.compose.ui:ui-tooling:1.11.4' + implementation "androidx.compose.ui:ui-tooling-preview:$composeUiToolingVersion" + debugImplementation "androidx.compose.ui:ui-tooling:$composeUiToolingVersion" kapt "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}" - implementation(platform("androidx.compose:compose-bom:2026.08.00")) + implementation(platform("androidx.compose:compose-bom:$composeBomVersion")) implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.material3:material3") @@ -65,9 +66,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion" // endregion - implementation("io.coil-kt.coil3:coil-compose:3.5.0") - implementation("io.coil-kt.coil3:coil-network-okhttp:3.5.0") - implementation("io.coil-kt.coil3:coil-svg:3.5.0") + implementation("io.coil-kt.coil3:coil-compose:$coilVersion") + implementation("io.coil-kt.coil3:coil-network-okhttp:$coilVersion") + implementation("io.coil-kt.coil3:coil-svg:$coilVersion") implementation("com.vanniktech:ui:0.10.0") @@ -78,12 +79,12 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' // region okhttp - implementation(platform("com.squareup.okhttp3:okhttp-bom:5.4.0")) + implementation(platform("com.squareup.okhttp3:okhttp-bom:$okHttpVersion")) implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.okhttp3:logging-interceptor") // endregion - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") implementation project(':core') api project(':material-color-utilities') From 2da18d9dcd1da718f3e6f1b92c285a1361977a66 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 09:59:30 +0200 Subject: [PATCH 171/174] wip Signed-off-by: alperozturk96 --- detekt.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/detekt.yml b/detekt.yml index 1360f33b..5279506f 100644 --- a/detekt.yml +++ b/detekt.yml @@ -3,6 +3,10 @@ # Applied on top of the detekt default config (buildUponDefaultConfig = true). comments: + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false UndocumentedPublicClass: active: false UndocumentedPublicFunction: From c5431308062c74e155e7e182273e17de9e9ff95a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 14:57:04 +0200 Subject: [PATCH 172/174] wip Signed-off-by: alperozturk96 --- .../common/ui/share/viewmodel/ShareViewModel.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 95a25584..5d45a7bc 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -192,7 +192,6 @@ class ShareViewModel( } publishShares(fetched) - viewModelScope.launch { deleteAbandonedDrafts() } } private suspend fun fetchShares(filterState: ShareState): NetworkResult> = repository.fetchShares( @@ -200,14 +199,6 @@ class ShareViewModel( filterSourceTypeClass = Source.NODE_SOURCE_CLASS, filterState = filterState ) - - private suspend fun deleteAbandonedDrafts() { - if (isCreatingDraft.value) return - - val drafts = (fetchShares(ShareState.DRAFT) as? NetworkResult.Success)?.data ?: return - val openShareIds = setOfNotNull(_activeShare.value.shareOrNull?.id, savedState.activeShareId) - drafts.filterNot { it.id in openShareIds }.forEach { repository.deleteShare(it.id) } - } // endregion // region create @@ -224,8 +215,6 @@ class ShareViewModel( } ?: return@launch updateEditorEntry(ShareEditorEntry.EDIT) - updateActiveShare(draft.toActiveShare()) - applySource(draft.id, sourceId) } finally { isCreatingDraft.value = false @@ -290,7 +279,7 @@ class ShareViewModel( val updated = result.dataOrElse { _errorMessageId.update { R.string.share_view_update_error_message } } ?: return - refreshActiveShare(updated.toActiveShare()) + updateActiveShare(updated.toActiveShare()) } // endregion From 68e7d5fbedca2df88cdd09cb54c63a7a4e3de88b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 15:41:52 +0200 Subject: [PATCH 173/174] wip Signed-off-by: alperozturk96 --- .../bottomsheet/AddOrEditShareBottomSheet.kt | 8 ----- .../ui/share/viewmodel/ShareViewModel.kt | 29 +++++++------------ 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt index 3d896ae3..316478ef 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/component/bottomsheet/AddOrEditShareBottomSheet.kt @@ -49,7 +49,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard @@ -85,7 +84,6 @@ import com.nextcloud.android.common.ui.share.model.ui.ShareEditorEntry import com.nextcloud.android.common.ui.share.model.ui.label import com.nextcloud.android.common.ui.share.repository.MockShareRepository import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModel -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch private const val CUSTOM_SELECTION = "custom" @@ -119,12 +117,6 @@ fun AddOrEditShareBottomSheet( val hasPropertyErrors = propertyErrors.isNotEmpty() val sendEnabled = !hasPropertyErrors && pendingProperties.isEmpty() - LaunchedEffect(sheetState, share.id) { - snapshotFlow { sheetState.isVisible }.first { it } - snapshotFlow { sheetState.isVisible }.first { !it } - dismissSheet() - } - ModalBottomSheet( onDismissRequest = dismissSheet, sheetState = sheetState, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 5d45a7bc..737df0fe 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -103,7 +103,6 @@ class ShareViewModel( private val propertyUpdateJobs = mutableMapOf() private val pendingUpdateJobs = mutableSetOf() - private val activatingShareIds = mutableSetOf() val searchQuery: StateFlow = savedState.searchQuery @@ -226,19 +225,14 @@ class ShareViewModel( // region state fun updateState(id: String, shareState: ShareState) { viewModelScope.launch { - if (shareState == ShareState.ACTIVE) activatingShareIds += id - try { - val updated = applyState(id, shareState) ?: return@launch + val updated = applyState(id, shareState) ?: return@launch - if (shareState == ShareState.ACTIVE) { - updateActiveShare(ActiveShareState.None) - } else { - refreshActiveShare(updated.toActiveShare()) - } - replaceInList(updated) - } finally { - if (shareState == ShareState.ACTIVE) activatingShareIds -= id + if (shareState == ShareState.ACTIVE) { + updateActiveShare(ActiveShareState.None) + } else { + refreshActiveShare(updated.toActiveShare()) } + replaceInList(updated) } } @@ -247,7 +241,6 @@ class ShareViewModel( viewModelScope.launch { _isPreparingLink.update { true } - activatingShareIds += id try { val updated = applyState(id, ShareState.ACTIVE) @@ -256,7 +249,6 @@ class ShareViewModel( replaceInList(updated) } } finally { - activatingShareIds -= id _isPreparingLink.update { false } } } @@ -498,11 +490,12 @@ class ShareViewModel( val active = _activeShare.value.shareOrNull ?: return if (active.id != id) return - when { - id in activatingShareIds -> Unit - active.shareState == ShareState.DRAFT -> deleteShare(id) - else -> refreshSharesAfterPendingUpdates() + if (active.shareState == ShareState.DRAFT) { + deleteShare(id) + } else if (active.shareState == ShareState.ACTIVE) { + refreshSharesAfterPendingUpdates() } + setActiveShare(null) } From f21fbd840f6daab2662f1fad8c902df1c448314d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 8 Sep 2026 15:50:33 +0200 Subject: [PATCH 174/174] wip Signed-off-by: alperozturk96 --- .../android/common/ui/share/ShareScreen.kt | 16 +++++++++++++++- .../common/ui/share/viewmodel/ShareViewModel.kt | 7 ++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt index 525e19a9..b0d5b9b1 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/ShareScreen.kt @@ -16,12 +16,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -78,6 +81,8 @@ import com.nextcloud.android.common.ui.share.viewmodel.ShareViewModelFactory private val FIRST_ITEM_TOP_SPACING = 16.dp private val ITEM_SPACING = 2.dp private val FAB_CLEARANCE = 88.dp +private val FAB_PROGRESS_SIZE = 24.dp +private val FAB_PROGRESS_STROKE = 2.dp private val ExpandedShareIdsSaver = listSaver, String>( save = { it.toList() }, @@ -89,6 +94,7 @@ private fun ShareScreen(internalLink: String, viewModel: ShareViewModel) { val errorMessageId by viewModel.errorMessageId.collectAsStateWithLifecycle() val screenState by viewModel.state.collectAsStateWithLifecycle() val activeShare by viewModel.activeShare.collectAsStateWithLifecycle() + val isCreatingDraft by viewModel.isCreatingDraft.collectAsStateWithLifecycle() val permissionPresets by viewModel.permissionPresets.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val resources = LocalResources.current @@ -108,7 +114,15 @@ private fun ShareScreen(internalLink: String, viewModel: ShareViewModel) { FloatingActionButton( onClick = { viewModel.createDraftShare() } ) { - Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") + if (isCreatingDraft) { + CircularProgressIndicator( + modifier = Modifier.size(FAB_PROGRESS_SIZE), + strokeWidth = FAB_PROGRESS_STROKE, + color = LocalContentColor.current + ) + } else { + Icon(painterResource(R.drawable.ic_person_add), contentDescription = "Add") + } } }, snackbarHost = { SnackbarHost(snackbarHostState) }, diff --git a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt index 737df0fe..6a6b4c89 100644 --- a/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt +++ b/ui/src/main/java/com/nextcloud/android/common/ui/share/viewmodel/ShareViewModel.kt @@ -81,7 +81,8 @@ class ShareViewModel( private val _isPreparingLink = MutableStateFlow(false) val isPreparingLink: StateFlow = _isPreparingLink.asStateFlow() - private val isCreatingDraft = MutableStateFlow(false) + private val _isCreatingDraft = MutableStateFlow(false) + val isCreatingDraft: StateFlow = _isCreatingDraft.asStateFlow() private val _activeShare = MutableStateFlow(ActiveShareState.None) val activeShare: StateFlow = _activeShare.asStateFlow() @@ -202,7 +203,7 @@ class ShareViewModel( // region create fun createDraftShare() { - if (!isCreatingDraft.compareAndSet(expect = false, update = true)) return + if (!_isCreatingDraft.compareAndSet(expect = false, update = true)) return viewModelScope.launch { try { @@ -216,7 +217,7 @@ class ShareViewModel( updateEditorEntry(ShareEditorEntry.EDIT) applySource(draft.id, sourceId) } finally { - isCreatingDraft.value = false + _isCreatingDraft.value = false } } }