@@ -11,8 +11,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
1111import androidx.activity.result.PickVisualMediaRequest
1212import androidx.activity.result.contract.ActivityResultContracts
1313import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
14+ import androidx.compose.foundation.border
1415import androidx.compose.foundation.isSystemInDarkTheme
16+ import androidx.compose.foundation.layout.Arrangement
17+ import androidx.compose.foundation.layout.Box
1518import androidx.compose.foundation.layout.Column
19+ import androidx.compose.foundation.layout.PaddingValues
1620import androidx.compose.foundation.layout.Row
1721import androidx.compose.foundation.layout.fillMaxSize
1822import androidx.compose.foundation.layout.fillMaxWidth
@@ -23,15 +27,23 @@ import androidx.compose.foundation.layout.padding
2327import androidx.compose.foundation.layout.size
2428import androidx.compose.foundation.layout.statusBarsPadding
2529import androidx.compose.foundation.pager.rememberPagerState
30+ import androidx.compose.foundation.shape.CircleShape
31+ import androidx.compose.foundation.shape.RoundedCornerShape
2632import androidx.compose.material.icons.Icons
33+ import androidx.compose.material.icons.filled.ArrowDropDown
34+ import androidx.compose.material.icons.filled.Check
2735import androidx.compose.material.icons.filled.Close
36+ import androidx.compose.material.icons.filled.Edit
37+ import androidx.compose.material.icons.filled.EditOff
38+ import androidx.compose.material3.Button
39+ import androidx.compose.material3.ButtonDefaults
40+ import androidx.compose.material3.DropdownMenu
41+ import androidx.compose.material3.DropdownMenuItem
2842import androidx.compose.material3.HorizontalDivider
2943import androidx.compose.material3.Icon
3044import androidx.compose.material3.IconButton
45+ import androidx.compose.material3.LocalContentColor
3146import androidx.compose.material3.MaterialTheme
32- import androidx.compose.material3.SegmentedButton
33- import androidx.compose.material3.SegmentedButtonDefaults
34- import androidx.compose.material3.SingleChoiceSegmentedButtonRow
3547import androidx.compose.material3.Surface
3648import androidx.compose.material3.Text
3749import androidx.compose.material3.darkColorScheme
@@ -47,8 +59,8 @@ import androidx.compose.runtime.setValue
4759import androidx.compose.ui.Alignment
4860import androidx.compose.ui.Modifier
4961import androidx.compose.ui.platform.LocalContext
50- import androidx.compose.ui.res.painterResource
5162import androidx.compose.ui.res.stringResource
63+ import androidx.compose.ui.text.font.FontWeight
5264import androidx.compose.ui.text.style.TextOverflow
5365import androidx.compose.ui.tooling.preview.Preview
5466import androidx.compose.ui.unit.dp
@@ -67,20 +79,22 @@ private const val APP_BAR_HORIZONTAL_PADDING_DP = 4
6779 * hosted by [FileAttachmentPreviewFragment]. [viewModel] owns the file list and its (IO-derived)
6880 * descriptions so both survive configuration changes; everything else here is ephemeral UI state.
6981 */
70- @Suppress(" LongMethod" )
82+ @Suppress(" LongMethod" , " LongParameterList " )
7183@Composable
7284internal fun FileAttachmentPreviewContent (
7385 viewModel : FileAttachmentPreviewViewModel ,
7486 conversationName : String ,
7587 initialCompressImages : Boolean ,
88+ showFilePermissionsOption : Boolean = false,
7689 onDismiss : () -> Unit ,
77- onSend : (files: List <String >, caption: String , compressImages: Boolean ) -> Unit
90+ onSend : (files: List <String >, caption: String , compressImages: Boolean , allowUpdate: Boolean ) -> Unit
7891) {
7992 val context = LocalContext .current
8093 val currentFiles = viewModel.files
8194 val hasCompressibleMedia = currentFiles.any { isCompressible(FileUtils .resolveMimeType(context, it.toUri())) }
8295 var caption by rememberSaveable { mutableStateOf(" " ) }
8396 var compressImages by rememberSaveable { mutableStateOf(hasCompressibleMedia && initialCompressImages) }
97+ var allowUpdate by rememberSaveable { mutableStateOf(false ) }
8498
8599 LaunchedEffect (currentFiles.toSet(), compressImages) {
86100 viewModel.describeFiles(compressImages)
@@ -139,21 +153,34 @@ internal fun FileAttachmentPreviewContent(
139153 }
140154 }
141155
142- if (hasCompressibleMedia) {
143- MediaQualitySegmentedButton (
144- highQuality = ! compressImages,
145- onHighQualityChange = { highQuality -> compressImages = ! highQuality },
156+ if (showFilePermissionsOption || hasCompressibleMedia) {
157+ Row (
146158 modifier = Modifier
147159 .fillMaxWidth()
148- .padding(horizontal = 16 .dp, vertical = 8 .dp)
149- )
160+ .padding(horizontal = 16 .dp, vertical = 8 .dp),
161+ horizontalArrangement = Arrangement .spacedBy(8 .dp, Alignment .CenterHorizontally )
162+ ) {
163+ if (showFilePermissionsOption) {
164+ FilePermissionOptionButton (
165+ allowUpdate = allowUpdate,
166+ onAllowUpdateChange = { allowUpdate = it }
167+ )
168+ }
169+
170+ if (hasCompressibleMedia) {
171+ MediaQualityOptionButton (
172+ highQuality = ! compressImages,
173+ onHighQualityChange = { highQuality -> compressImages = ! highQuality }
174+ )
175+ }
176+ }
150177 }
151178
152179 CaptionInputBar (
153180 caption = caption,
154181 onCaptionChange = { caption = it },
155182 sendEnabled = currentFiles.isNotEmpty(),
156- onSend = { onSend(currentFiles.toList(), caption, compressImages) }
183+ onSend = { onSend(currentFiles.toList(), caption, compressImages, allowUpdate ) }
157184 )
158185 }
159186 }
@@ -198,39 +225,134 @@ private fun PreviewTopBar(conversationName: String, onDismiss: () -> Unit) {
198225 }
199226}
200227
228+ private const val OPTION_BUTTON_ICON_SIZE_DP = 16
229+ private const val OPTION_BUTTON_HORIZONTAL_PADDING_DP = 12
230+ private const val OPTION_BUTTON_VERTICAL_PADDING_DP = 6
231+ private const val OPTION_BUTTON_LABEL_PADDING_DP = 4
232+ private const val TEXT_BADGE_BORDER_DP = 1
233+ private const val TEXT_BADGE_CORNER_RADIUS_DP = 4
234+ private const val TEXT_BADGE_HORIZONTAL_PADDING_DP = 3
235+
201236@Composable
202- private fun MediaQualitySegmentedButton (
203- highQuality : Boolean ,
204- onHighQualityChange : (Boolean ) -> Unit ,
205- modifier : Modifier = Modifier
206- ) {
207- SingleChoiceSegmentedButtonRow (modifier = modifier) {
208- SegmentedButton (
209- selected = highQuality,
210- onClick = { onHighQualityChange(true ) },
211- shape = SegmentedButtonDefaults .itemShape(index = 0 , count = 2 ),
212- icon = {
213- Icon (
214- painter = painterResource(R .drawable.high_quality_24px),
215- contentDescription = null ,
216- modifier = Modifier .size(SegmentedButtonDefaults .IconSize )
217- )
218- },
219- label = { Text (stringResource(R .string.nc_media_quality_original)) }
237+ private fun OptionButton (label : String , onClick : () -> Unit , icon : @Composable () -> Unit ) {
238+ Button (
239+ onClick = onClick,
240+ shape = CircleShape ,
241+ colors = ButtonDefaults .buttonColors(
242+ containerColor = MaterialTheme .colorScheme.surfaceVariant,
243+ contentColor = MaterialTheme .colorScheme.onSurfaceVariant
244+ ),
245+ contentPadding = PaddingValues (
246+ horizontal = OPTION_BUTTON_HORIZONTAL_PADDING_DP .dp,
247+ vertical = OPTION_BUTTON_VERTICAL_PADDING_DP .dp
248+ )
249+ ) {
250+ icon()
251+ Text (
252+ text = label,
253+ style = MaterialTheme .typography.labelMedium,
254+ maxLines = 1 ,
255+ overflow = TextOverflow .Ellipsis ,
256+ modifier = Modifier .padding(horizontal = OPTION_BUTTON_LABEL_PADDING_DP .dp)
220257 )
221- SegmentedButton (
222- selected = ! highQuality,
223- onClick = { onHighQualityChange(false ) },
224- shape = SegmentedButtonDefaults .itemShape(index = 1 , count = 2 ),
258+ Icon (
259+ imageVector = Icons .Filled .ArrowDropDown ,
260+ contentDescription = null ,
261+ modifier = Modifier .size(OPTION_BUTTON_ICON_SIZE_DP .dp)
262+ )
263+ }
264+ }
265+
266+ @Composable
267+ private fun TextBadge (text : String ) {
268+ Text (
269+ text = text,
270+ style = MaterialTheme .typography.labelSmall,
271+ fontWeight = FontWeight .Bold ,
272+ color = LocalContentColor .current,
273+ modifier = Modifier
274+ .border(
275+ width = TEXT_BADGE_BORDER_DP .dp,
276+ color = LocalContentColor .current,
277+ shape = RoundedCornerShape (TEXT_BADGE_CORNER_RADIUS_DP .dp)
278+ )
279+ .padding(horizontal = TEXT_BADGE_HORIZONTAL_PADDING_DP .dp)
280+ )
281+ }
282+
283+ @Composable
284+ private fun MediaQualityOptionButton (highQuality : Boolean , onHighQualityChange : (Boolean ) -> Unit ) {
285+ var expanded by remember { mutableStateOf(false ) }
286+
287+ Box {
288+ OptionButton (
289+ label = stringResource(
290+ if (highQuality) R .string.nc_media_quality_original else R .string.nc_media_quality_reduced
291+ ),
292+ onClick = { expanded = true },
293+ icon = { TextBadge (if (highQuality) " HD" else " SD" ) }
294+ )
295+ DropdownMenu (expanded = expanded, onDismissRequest = { expanded = false }) {
296+ DropdownMenuItem (
297+ text = { Text (stringResource(R .string.nc_media_quality_original)) },
298+ leadingIcon = { TextBadge (" HD" ) },
299+ trailingIcon = { if (highQuality) Icon (Icons .Filled .Check , contentDescription = null ) },
300+ onClick = {
301+ onHighQualityChange(true )
302+ expanded = false
303+ }
304+ )
305+ DropdownMenuItem (
306+ text = { Text (stringResource(R .string.nc_media_quality_reduced)) },
307+ leadingIcon = { TextBadge (" SD" ) },
308+ trailingIcon = { if (! highQuality) Icon (Icons .Filled .Check , contentDescription = null ) },
309+ onClick = {
310+ onHighQualityChange(false )
311+ expanded = false
312+ }
313+ )
314+ }
315+ }
316+ }
317+
318+ @Composable
319+ private fun FilePermissionOptionButton (allowUpdate : Boolean , onAllowUpdateChange : (Boolean ) -> Unit ) {
320+ var expanded by remember { mutableStateOf(false ) }
321+
322+ Box {
323+ OptionButton (
324+ label = stringResource(
325+ if (allowUpdate) R .string.nc_file_permission_editable else R .string.nc_file_permission_view_only
326+ ),
327+ onClick = { expanded = true },
225328 icon = {
226329 Icon (
227- painter = painterResource( R .drawable.high_quality_off_24px) ,
330+ imageVector = if (allowUpdate) Icons . Filled . Edit else Icons . Filled . EditOff ,
228331 contentDescription = null ,
229- modifier = Modifier .size(SegmentedButtonDefaults . IconSize )
332+ modifier = Modifier .size(OPTION_BUTTON_ICON_SIZE_DP .dp )
230333 )
231- },
232- label = { Text (stringResource(R .string.nc_media_quality_reduced)) }
334+ }
233335 )
336+ DropdownMenu (expanded = expanded, onDismissRequest = { expanded = false }) {
337+ DropdownMenuItem (
338+ text = { Text (stringResource(R .string.nc_file_permission_view_only)) },
339+ leadingIcon = { Icon (Icons .Filled .EditOff , contentDescription = null ) },
340+ trailingIcon = { if (! allowUpdate) Icon (Icons .Filled .Check , contentDescription = null ) },
341+ onClick = {
342+ onAllowUpdateChange(false )
343+ expanded = false
344+ }
345+ )
346+ DropdownMenuItem (
347+ text = { Text (stringResource(R .string.nc_file_permission_editable)) },
348+ leadingIcon = { Icon (Icons .Filled .Edit , contentDescription = null ) },
349+ trailingIcon = { if (allowUpdate) Icon (Icons .Filled .Check , contentDescription = null ) },
350+ onClick = {
351+ onAllowUpdateChange(true )
352+ expanded = false
353+ }
354+ )
355+ }
234356 }
235357}
236358
@@ -263,7 +385,7 @@ private fun FileAttachmentPreviewContentPreview() {
263385 conversationName = " Team Chat" ,
264386 initialCompressImages = true ,
265387 onDismiss = {},
266- onSend = { _, _, _ -> }
388+ onSend = { _, _, _, _ -> }
267389 )
268390 }
269391}
@@ -277,7 +399,7 @@ private fun FileAttachmentPreviewContentSingleFilePreview() {
277399 conversationName = " Team Chat" ,
278400 initialCompressImages = true ,
279401 onDismiss = {},
280- onSend = { _, _, _ -> }
402+ onSend = { _, _, _, _ -> }
281403 )
282404 }
283405}
0 commit comments