mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Add shared media toolbar to new media send flow.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
+16
-94
@@ -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 <reified E : Enum<E>> 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
|
||||
|
||||
+3
-3
@@ -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()
|
||||
Reference in New Issue
Block a user