diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt index 8a35b8e73b..f442768333 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt @@ -69,8 +69,8 @@ import org.signal.mediasend.edit.image.ImageEditorToolbar import org.signal.mediasend.edit.image.ImageEditorUndoRedoButtons import org.signal.mediasend.edit.image.RotationDial import org.signal.mediasend.edit.video.VideoEditorFragment -import org.signal.mediasend.edit.video.VideoEditorToolbar import org.signal.mediasend.edit.video.VideoEditorViewModel +import org.signal.mediasend.edit.video.VideoTrimBar import org.signal.mediasend.rememberPreviewState @Composable @@ -284,10 +284,17 @@ fun MediaEditScreen( if (controller.isUserDrawing) { DrawModeColorBar(imageEditorController = controller) } + } - if (isSmallWindowBreakpoint) { - ImageEditorToolbar(imageEditorController = controller, state = state, onEvent = onEvent) - } + if (isSmallWindowBreakpoint) { + MediaToolbar( + focusedUri = focusedUri, + focusedEditorState = focusedEditorState, + state = state, + onEvent = onEvent, + imageController = imageController, + isTextEditing = isTextEditing + ) } } @@ -311,16 +318,16 @@ fun MediaEditScreen( } } - if (!isSmallWindowBreakpoint && imageController != null) { - ImageEditorToolbar( - imageEditorController = imageController, + if (!isSmallWindowBreakpoint) { + MediaToolbar( + focusedUri = focusedUri, + focusedEditorState = focusedEditorState, state = state, onEvent = onEvent, + imageController = imageController, + isTextEditing = isTextEditing, modifier = Modifier .align(Alignment.CenterEnd) - .navigationBarsPadding() - .padding(end = 24.dp) - .then(if (isTextEditing) Modifier.imePadding() else Modifier) ) } @@ -359,6 +366,49 @@ fun MediaEditScreen( } } +/** + * Toolbar allowing for common actions + */ +@Composable +private fun MediaToolbar( + state: MediaSendState, + onEvent: (MediaEditScreenEvent) -> Unit, + focusedUri: Uri?, + focusedEditorState: EditorState?, + imageController: ImageController?, + isTextEditing: Boolean, + modifier: Modifier = Modifier +) { + if (focusedUri == null) { + return + } + + when (focusedEditorState) { + null -> return + is EditorState.Image -> { + imageController?.let { + ImageEditorToolbar( + imageEditorController = it, + state = state, + onEvent = onEvent, + modifier = modifier + .navigationBarsPadding() + .padding(end = 24.dp) + .then(if (isTextEditing) Modifier.imePadding() else Modifier) + ) + } + } + + else -> MediaEditorToolbar(modifier = modifier) { + MediaEditorToolbarSharedButtons( + state = state, + onEvent = onEvent, + canSave = focusedEditorState is EditorState.VideoTrim + ) + } + } +} + /** * A control in the bottom stack, and the two ways it gets out of the user's way. Going not-[visible] gives up its layout * space, letting the rest of the stack settle into it, while [faded] holds onto the space -- releasing it mid-gesture @@ -404,7 +454,7 @@ private fun VideoTrimTimeline( } } - VideoEditorToolbar( + VideoTrimBar( videoUri = videoUri, mediaInputFactory = MediaSendDependencies.mediaInputFactory, videoTrimData = editorState.videoTrimData, diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt new file mode 100644 index 0000000000..5deaff46ae --- /dev/null +++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.mediasend.edit + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import org.signal.core.ui.WindowBreakpoint +import org.signal.core.ui.compose.IconButtons +import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.copied.androidx.compose.material3.IconButtonColors +import org.signal.core.ui.rememberWindowBreakpoint +import org.signal.mediasend.MediaSendState +import org.signal.mediasend.SentMediaQuality + +@Composable +internal fun MediaEditorToolbar( + modifier: Modifier = Modifier, + leading: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {}, + content: @Composable () -> Unit +) { + val windowBreakpoint = rememberWindowBreakpoint() + val isRow = windowBreakpoint is WindowBreakpoint.Small + + if (isRow) { + Row(modifier = modifier.height(48.dp)) { + leading() + + Row( + modifier = Modifier + .fillMaxHeight() + .background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(percent = 50)) + ) { + content() + } + + trailing() + } + } else { + Column(modifier = modifier.width(48.dp)) { + leading() + + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(percent = 50)) + ) { + content() + } + + trailing() + } + } +} + +@Composable +internal fun MediaEditorToolbarButton( + imageVector: ImageVector, + onClick: () -> Unit, + contentDescription: String? = null, + colors: IconButtonColors = IconButtons.iconButtonColors() +) { + IconButtons.IconButton( + onClick = onClick, + colors = colors + ) { + Icon(imageVector = imageVector, contentDescription = contentDescription, modifier = Modifier.size(24.dp)) + } +} + +@Composable +internal fun MediaEditorToolbarSharedButtons( + state: MediaSendState, + canSave: Boolean, + onEvent: (MediaEditScreenEvent) -> Unit +) { + MediaEditorToolbarButton( + imageVector = if (state.sentMediaQuality == SentMediaQuality.HIGH) { + SignalIcons.QualityHigh.imageVector + } else { + SignalIcons.QualityHighSlash.imageVector + }, + onClick = { onEvent(MediaEditScreenEvent.ToggleMediaQuality) } + ) + + if (canSave) { + MediaEditorToolbarButton( + imageVector = SignalIcons.Save.imageVector, + onClick = { onEvent(MediaEditScreenEvent.SaveMedia) } + ) + } + + MediaEditorToolbarButton( + imageVector = SignalIcons.Plus.imageVector, // TODO [alex] - wrong art asset + onClick = { onEvent(MediaEditScreenEvent.NavigateToGallery) } + ) +} diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/image/ImageEditorToolbar.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/image/ImageEditorToolbar.kt index 8f3a74a485..590b7818e4 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/edit/image/ImageEditorToolbar.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/image/ImageEditorToolbar.kt @@ -6,15 +6,7 @@ package org.signal.mediasend.edit.image import androidx.compose.animation.Crossfade -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -22,7 +14,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import org.signal.core.ui.WindowBreakpoint import org.signal.core.ui.compose.FoldablePortraitDayPreview import org.signal.core.ui.compose.FoldablePortraitNightPreview import org.signal.core.ui.compose.IconButtons @@ -31,16 +22,16 @@ import org.signal.core.ui.compose.PhonePortraitDayPreview import org.signal.core.ui.compose.PhonePortraitNightPreview import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons -import org.signal.core.ui.compose.copied.androidx.compose.material3.IconButtonColors import org.signal.core.ui.compose.theme.SignalTheme -import org.signal.core.ui.rememberWindowBreakpoint import org.signal.core.util.next import org.signal.imageeditor.core.model.EditorModel import org.signal.mediasend.MediaSendState -import org.signal.mediasend.SentMediaQuality import org.signal.mediasend.edit.ImageController import org.signal.mediasend.edit.MediaEditScreenDialogs import org.signal.mediasend.edit.MediaEditScreenEvent +import org.signal.mediasend.edit.MediaEditorToolbar +import org.signal.mediasend.edit.MediaEditorToolbarButton +import org.signal.mediasend.edit.MediaEditorToolbarSharedButtons import org.signal.mediasend.rememberPreviewState import java.util.EnumMap @@ -81,34 +72,21 @@ private fun ImageEditorNoneStateToolbar( onEvent: (MediaEditScreenEvent) -> Unit, modifier: Modifier = Modifier ) { - OrientedImageEditorToolbar(modifier) { - ImageEditorButton( + MediaEditorToolbar(modifier) { + MediaEditorToolbarButton( imageVector = SignalIcons.BrushPen.imageVector, onClick = imageEditorController::beginDrawEdit ) - ImageEditorButton( + MediaEditorToolbarButton( imageVector = SignalIcons.CropRotate.imageVector, onClick = imageEditorController::beginCropAndRotateEdit ) - ImageEditorButton( - imageVector = if (state.sentMediaQuality == SentMediaQuality.HIGH) { - SignalIcons.QualityHigh.imageVector - } else { - SignalIcons.QualityHighSlash.imageVector - }, - onClick = { onEvent(MediaEditScreenEvent.ToggleMediaQuality) } - ) - - ImageEditorButton( - imageVector = SignalIcons.Save.imageVector, - onClick = { onEvent(MediaEditScreenEvent.SaveMedia) } - ) - - ImageEditorButton( - imageVector = SignalIcons.Plus.imageVector, // TODO [alex] - wrong art asset - onClick = { onEvent(MediaEditScreenEvent.NavigateToGallery) } + MediaEditorToolbarSharedButtons( + state = state, + canSave = true, + onEvent = onEvent ) } } @@ -118,7 +96,7 @@ private fun ImageEditorDrawStateToolbar( imageEditorController: ImageController, modifier: Modifier = Modifier ) { - OrientedImageEditorToolbar( + MediaEditorToolbar( modifier = modifier, leading = { CommitButton(imageEditorController) @@ -174,7 +152,7 @@ private fun ImageEditorCropAndResizeToolbar( imageEditorController: ImageController, modifier: Modifier = Modifier ) { - OrientedImageEditorToolbar( + MediaEditorToolbar( modifier = modifier, leading = { CommitButton(imageEditorController) @@ -183,12 +161,12 @@ private fun ImageEditorCropAndResizeToolbar( DiscardButton(imageEditorController) } ) { - ImageEditorButton( + MediaEditorToolbarButton( imageVector = SignalIcons.CropRotate.imageVector, onClick = imageEditorController::rotate ) - ImageEditorButton( + MediaEditorToolbarButton( imageVector = SignalIcons.Flip.imageVector, onClick = imageEditorController::flip ) @@ -218,7 +196,7 @@ private fun ImageEditorCropAndResizeToolbar( @Composable private fun CommitButton(imageEditorController: ImageController) { - ImageEditorButton( + MediaEditorToolbarButton( imageVector = SignalIcons.Check.imageVector, onClick = imageEditorController::commitEdit, colors = IconButtons.iconButtonColors( @@ -236,7 +214,7 @@ private fun DiscardButton(imageEditorController: ImageController) { ) } - ImageEditorButton( + MediaEditorToolbarButton( imageVector = SignalIcons.X.imageVector, onClick = imageEditorController::requestCancelEdit, colors = IconButtons.iconButtonColors( @@ -264,21 +242,6 @@ private inline fun > IconCrossfadeToggleButton( } } -@Composable -private fun ImageEditorButton( - imageVector: ImageVector, - onClick: () -> Unit, - contentDescription: String? = null, - colors: IconButtonColors = IconButtons.iconButtonColors() -) { - IconButtons.IconButton( - onClick = onClick, - colors = colors - ) { - Icon(imageVector = imageVector, contentDescription = contentDescription, modifier = Modifier.size(24.dp)) - } -} - @Composable private fun ImageEditorToggleButton( imageVector: ImageVector, @@ -298,47 +261,6 @@ private fun ImageEditorToggleButton( } } -@Composable -private fun OrientedImageEditorToolbar( - modifier: Modifier = Modifier, - leading: @Composable () -> Unit = {}, - trailing: @Composable () -> Unit = {}, - content: @Composable () -> Unit -) { - val windowBreakpoint = rememberWindowBreakpoint() - val isRow = windowBreakpoint is WindowBreakpoint.Small - - if (isRow) { - Row(modifier = modifier.height(48.dp)) { - leading() - - Row( - modifier = Modifier - .fillMaxHeight() - .background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(percent = 50)) - ) { - content() - } - - trailing() - } - } else { - Column(modifier = modifier.width(48.dp)) { - leading() - - Column( - modifier = Modifier - .fillMaxWidth() - .background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(percent = 50)) - ) { - content() - } - - trailing() - } - } -} - @PhonePortraitDayPreview @PhonePortraitNightPreview @FoldablePortraitDayPreview diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/video/VideoEditorToolbar.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/video/VideoTrimBar.kt similarity index 98% rename from feature/media-send/src/main/java/org/signal/mediasend/edit/video/VideoEditorToolbar.kt rename to feature/media-send/src/main/java/org/signal/mediasend/edit/video/VideoTrimBar.kt index e28a666832..3473dff249 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/edit/video/VideoEditorToolbar.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/video/VideoTrimBar.kt @@ -44,7 +44,7 @@ private const val TAG = "VideoEditorToolbar" * caps how long a range the user can trim to (0 means no cap). */ @Composable -fun VideoEditorToolbar( +fun VideoTrimBar( videoUri: Uri, mediaInputFactory: MediaInputFactory, videoTrimData: VideoTrimData, @@ -140,9 +140,9 @@ fun VideoEditorToolbar( @Preview @Composable -fun VideoEditorToolbarPreview() { +fun VideoTrimBarPreview() { Previews.Preview { - VideoEditorToolbar( + VideoTrimBar( videoUri = Uri.EMPTY, mediaInputFactory = object : MediaInputFactory { override fun createForUri(context: Context, uri: Uri) = throw UnsupportedOperationException()