From 3af4afafe6275f6ad4c74b5676bb31522e30dda0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 20 Apr 2026 16:00:01 +0200 Subject: [PATCH 1/8] 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 a225095f..4820c3b4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -324,6 +324,11 @@ + + + + + diff --git a/material-color-utilities/build.gradle b/material-color-utilities/build.gradle index 19996c1f..d1e3b6ac 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 1c11fea6..2bdecc19 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.03.01")) 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 d8a9e28430f26a0988b487b6d012cd7ee732d897 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 09:52:07 +0200 Subject: [PATCH 2/8] 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 18f509a73a289a7496b224ef6d4f51b02b312bc1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 10:08:58 +0200 Subject: [PATCH 3/8] 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 15e14858aa58c0e7483ea3f3aab43cc08db569da Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 12:41:47 +0200 Subject: [PATCH 4/8] 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 452c3ab0a4adc3f02c48e7f7909c7ff2f836f3a6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 12:44:48 +0200 Subject: [PATCH 5/8] 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 e95f0925dbfb092b5ee487ed6465aaaf4a83971e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 21 Apr 2026 15:25:47 +0200 Subject: [PATCH 6/8] 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 3795480fdf5c62ba5dc58eac60d1858b23fda17a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Apr 2026 10:38:07 +0200 Subject: [PATCH 7/8] add test ocs call Signed-off-by: alperozturk96 --- build.gradle | 1 + gradle/verification-metadata.xml | 22 ++++++ sample/build.gradle | 1 + .../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 | 7 ++ .../common/ui/network/NextcloudCredentials.kt | 14 ++++ .../common/ui/network/NextcloudHttpClient.kt | 74 +++++++++++++++++++ .../common/ui/network/PredefinedStatus.kt | 24 ++++++ .../common/ui/network/UserStatusService.kt | 61 +++++++++++++++ 12 files changed, 311 insertions(+), 1 deletion(-) 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/build.gradle b/build.gradle index 16cff00e..972590d4 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ buildscript { // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id 'org.jetbrains.kotlin.plugin.compose' version '2.3.20' apply false + id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion" apply false id 'com.android.library' version '9.1.1' apply false id 'org.jetbrains.kotlin.android' version "$kotlinVersion" apply false id 'com.android.legacy-kapt' version '9.1.1' apply false diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4820c3b4..bfe8235f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -92,6 +92,7 @@ + @@ -236,6 +237,7 @@ + @@ -17854,6 +17856,11 @@ + + + + + @@ -19239,6 +19246,11 @@ + + + + + @@ -20704,6 +20716,11 @@ + + + + + @@ -21407,6 +21424,11 @@ + + + + + diff --git a/sample/build.gradle b/sample/build.gradle index c35d96aa..e06fc6ad 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.10.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.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/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 2bdecc19..45b88a2a 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' id 'com.android.library' id 'com.android.built-in-kotlin' id 'com.android.legacy-kapt' @@ -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.7.3") + 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 630a957796d368a0033af865e9472dce3b61eed7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 24 Apr 2026 10:39:10 +0200 Subject: [PATCH 8/8] 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