mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-13 17:23:56 +01:00
Add large-screen media send toolbars for image editing.
This commit is contained in:
committed by
Cody Henthorne
parent
b4bfb67a44
commit
773d6c36dc
+1
-1
@@ -205,7 +205,7 @@ private fun FeatureBullet(text: String) {
|
||||
modifier = Modifier.padding(vertical = 2.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_check_24),
|
||||
imageVector = ImageVector.vectorResource(id = CoreUiR.drawable.symbol_check_24),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
|
||||
+1
-1
@@ -24,6 +24,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.next
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
@@ -45,7 +46,6 @@ import org.thoughtcrime.securesms.jobs.InAppPaymentPurchaseTokenJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.next
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -72,6 +72,7 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -413,7 +414,7 @@ private fun IssueChip(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = if (isSelected) {
|
||||
ImageVector.vectorResource(R.drawable.symbol_check_24)
|
||||
ImageVector.vectorResource(CoreUiR.drawable.symbol_check_24)
|
||||
} else {
|
||||
ImageVector.vectorResource(issue.category.icon)
|
||||
},
|
||||
|
||||
+1
-1
@@ -704,7 +704,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
}
|
||||
|
||||
if (menuState.shouldShowSaveAttachmentAction()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_save_android_24, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD)));
|
||||
items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_save_android_24, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowCopyAction()) {
|
||||
|
||||
@@ -93,7 +93,7 @@ object PinnedContextMenu {
|
||||
message.slideDeck.getStickerSlide() == null
|
||||
) {
|
||||
add(
|
||||
ActionItem(R.drawable.symbol_save_android_24, context.getString(R.string.conversation_selection__menu_save)) {
|
||||
ActionItem(CoreUiR.drawable.symbol_save_android_24, context.getString(R.string.conversation_selection__menu_save)) {
|
||||
callbacks.onSave()
|
||||
}
|
||||
)
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ final class SafetyNumberChangeAdapter extends ListAdapter<ChangedRecipient, Safe
|
||||
if (changedRecipient.isUnverified() || changedRecipient.isVerified()) {
|
||||
subtitle.setText(R.string.safety_number_change_dialog__previous_verified);
|
||||
|
||||
Drawable check = DrawableUtil.tint(ContextUtil.requireDrawable(itemView.getContext(), R.drawable.symbol_check_24), ContextCompat.getColor(itemView.getContext(), R.color.signal_text_secondary));
|
||||
Drawable check = DrawableUtil.tint(ContextUtil.requireDrawable(itemView.getContext(), org.signal.core.ui.R.drawable.symbol_check_24), ContextCompat.getColor(itemView.getContext(), R.color.signal_text_secondary));
|
||||
check.setBounds(0, 0, ViewUtil.dpToPx(12), ViewUtil.dpToPx(12));
|
||||
subtitle.setCompoundDrawables(check, null, null, null);
|
||||
} else if (changedRecipient.getRecipient().hasAUserSetDisplayName(itemView.getContext())) {
|
||||
|
||||
+1
-1
@@ -2623,7 +2623,7 @@ class ConversationFragment :
|
||||
|
||||
if (menuState.shouldShowSaveAttachmentAction()) {
|
||||
items.add(
|
||||
ActionItem(R.drawable.symbol_save_android_24, resources.getString(R.string.conversation_selection__menu_save)) {
|
||||
ActionItem(CoreUiR.drawable.symbol_save_android_24, resources.getString(R.string.conversation_selection__menu_save)) {
|
||||
handleSaveAttachment(getSelectedConversationMessage().messageRecord as MmsMessageRecord)
|
||||
finishActionMode()
|
||||
}
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ fun LearnMoreSheet() {
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp, start = 40.dp, end = 32.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.symbol_save_android_24),
|
||||
imageVector = SignalIcons.Save.imageVector,
|
||||
contentDescription = stringResource(R.string.preferences__linked_devices),
|
||||
modifier = Modifier.size(24.dp).padding(top = 4.dp)
|
||||
)
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ class MediaOverviewContextMenu(
|
||||
private fun getSaveActionItem(mediaRecord: MediaTable.MediaRecord): ActionItem? {
|
||||
if (mediaRecord.attachment == null) return null
|
||||
return ActionItem(
|
||||
iconRes = R.drawable.symbol_save_android_24,
|
||||
iconRes = CoreUiR.drawable.symbol_save_android_24,
|
||||
title = fragment.getString(R.string.save)
|
||||
) {
|
||||
callbacks.onSave(mediaRecord)
|
||||
|
||||
+1
-1
@@ -511,7 +511,7 @@ public final class MediaOverviewPageFragment extends LoggingFragment
|
||||
int selectionCount = getListAdapter().getSectionCount();
|
||||
|
||||
bottomActionBar.setItems(Arrays.asList(
|
||||
new ActionItem(R.drawable.symbol_save_android_24, getResources().getQuantityString(R.plurals.MediaOverviewActivity_save_plural, selectionCount), () -> {
|
||||
new ActionItem(org.signal.core.ui.R.drawable.symbol_save_android_24, getResources().getQuantityString(R.plurals.MediaOverviewActivity_save_plural, selectionCount), () -> {
|
||||
Collection<MediaTable.MediaRecord> selected = getListAdapter().getSelectedMedia();
|
||||
|
||||
if (SignalStore.backup().getOptimizeStorage()) {
|
||||
|
||||
+4
-4
@@ -419,8 +419,8 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
}
|
||||
|
||||
val icon = when (state.quality) {
|
||||
SentMediaQuality.HIGH -> R.drawable.symbol_quality_high_24
|
||||
else -> R.drawable.symbol_quality_high_slash_24
|
||||
SentMediaQuality.HIGH -> CoreUiR.drawable.symbol_quality_high_24
|
||||
else -> CoreUiR.drawable.symbol_quality_high_slash_24
|
||||
}
|
||||
|
||||
MediaReviewToastPopupWindow.show(controls, icon, description)
|
||||
@@ -494,8 +494,8 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
}
|
||||
qualityButton.setImageResource(
|
||||
when (state.quality) {
|
||||
SentMediaQuality.STANDARD -> R.drawable.symbol_quality_high_slash_24
|
||||
SentMediaQuality.HIGH -> R.drawable.symbol_quality_high_24
|
||||
SentMediaQuality.STANDARD -> CoreUiR.drawable.symbol_quality_high_slash_24
|
||||
SentMediaQuality.HIGH -> CoreUiR.drawable.symbol_quality_high_24
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.mediasend.v2.text
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import org.thoughtcrime.securesms.util.next
|
||||
import org.signal.core.util.next
|
||||
|
||||
typealias OnTextAlignmentChanged = (TextAlignment) -> Unit
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.mediasend.v2.text
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import org.thoughtcrime.securesms.util.next
|
||||
import org.signal.core.util.next
|
||||
|
||||
typealias OnTextColorStyleChanged = (TextColorStyle) -> Unit
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.mediasend.v2.text
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import org.signal.core.util.next
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.util.next
|
||||
|
||||
typealias OnTextFontChanged = (TextFont) -> Unit
|
||||
|
||||
|
||||
+4
-3
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.Optional
|
||||
import androidx.core.app.Person as PersonCompat
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
private const val BIG_PICTURE_DIMEN = 500
|
||||
|
||||
@@ -237,7 +238,7 @@ sealed class NotificationBuilder(protected val context: Context) {
|
||||
val markAsRead: PendingIntent? = conversation.getMarkAsReadIntent(context)
|
||||
if (markAsRead != null) {
|
||||
val markAsReadAction: NotificationCompat.Action =
|
||||
NotificationCompat.Action.Builder(R.drawable.symbol_check_24, context.getString(R.string.MessageNotifier_mark_read), markAsRead)
|
||||
NotificationCompat.Action.Builder(CoreUiR.drawable.symbol_check_24, context.getString(R.string.MessageNotifier_mark_read), markAsRead)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
|
||||
.setShowsUserInterface(false)
|
||||
.build()
|
||||
@@ -251,7 +252,7 @@ sealed class NotificationBuilder(protected val context: Context) {
|
||||
val markAsRead: PendingIntent? = state.getMarkAsReadIntent(context)
|
||||
|
||||
if (markAsRead != null) {
|
||||
val markAllAsReadAction = NotificationCompat.Action(R.drawable.symbol_check_24, context.getString(R.string.MessageNotifier_mark_all_as_read), markAsRead)
|
||||
val markAllAsReadAction = NotificationCompat.Action(CoreUiR.drawable.symbol_check_24, context.getString(R.string.MessageNotifier_mark_all_as_read), markAsRead)
|
||||
builder.addAction(markAllAsReadAction)
|
||||
builder.extend(NotificationCompat.WearableExtender().addAction(markAllAsReadAction))
|
||||
}
|
||||
@@ -260,7 +261,7 @@ sealed class NotificationBuilder(protected val context: Context) {
|
||||
override fun addTurnOffJoinedNotificationsAction(pendingIntent: PendingIntent?) {
|
||||
if (pendingIntent != null) {
|
||||
val turnOffTheseNotifications = NotificationCompat.Action(
|
||||
R.drawable.symbol_check_24,
|
||||
CoreUiR.drawable.symbol_check_24,
|
||||
context.getString(R.string.MessageNotifier_turn_off_these_notifications),
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.scribbles.HSVColorSlider.setUpForColor
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -146,9 +147,9 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
cropAspectLockButton.setOnClickListener {
|
||||
listener?.onCropAspectLock()
|
||||
if (listener?.isCropAspectLocked == true) {
|
||||
cropAspectLockButton.setImageResource(R.drawable.symbol_crop_lock_24)
|
||||
cropAspectLockButton.setImageResource(CoreUiR.drawable.symbol_crop_lock_24)
|
||||
} else {
|
||||
cropAspectLockButton.setImageResource(R.drawable.symbol_crop_unlock_24)
|
||||
cropAspectLockButton.setImageResource(CoreUiR.drawable.symbol_crop_unlock_24)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +356,7 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
|
||||
private fun presentModeDraw() {
|
||||
drawButton.isSelected = true
|
||||
brushToggle.setImageResource(R.drawable.symbol_brush_pen_24)
|
||||
brushToggle.setImageResource(CoreUiR.drawable.symbol_brush_pen_24)
|
||||
widthSeekBar.progress = SignalStore.imageEditor.getMarkerPercentage()
|
||||
listener?.onColorChange(getActiveColor())
|
||||
updateColorIndicator()
|
||||
@@ -368,7 +369,7 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
|
||||
private fun presentModeHighlight() {
|
||||
drawButton.isSelected = true
|
||||
brushToggle.setImageResource(R.drawable.symbol_brush_highlighter_24)
|
||||
brushToggle.setImageResource(CoreUiR.drawable.symbol_brush_highlighter_24)
|
||||
widthSeekBar.progress = SignalStore.imageEditor.getHighlighterPercentage()
|
||||
listener?.onColorChange(getActiveColor())
|
||||
updateColorIndicator()
|
||||
|
||||
+2
-1
@@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressSt
|
||||
import org.thoughtcrime.securesms.stickers.StickerPreviewDataFactory
|
||||
import org.thoughtcrime.securesms.stickers.manage.AvailableStickerPack.DownloadStatus
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@Composable
|
||||
fun StickerPackSectionHeader(
|
||||
@@ -90,7 +91,7 @@ fun AvailableStickerPackRow(
|
||||
)
|
||||
|
||||
val readyIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_circle_down_24)
|
||||
val downloadedIcon = ImageVector.vectorResource(R.drawable.symbol_check_24)
|
||||
val downloadedIcon = ImageVector.vectorResource(CoreUiR.drawable.symbol_check_24)
|
||||
|
||||
val startButtonContentDesc = stringResource(R.string.StickerManagement_accessibility_download)
|
||||
val startButtonOnClickLabel = stringResource(R.string.StickerManagement_accessibility_download_pack, pack.record.title)
|
||||
|
||||
@@ -242,7 +242,7 @@ object StoryContextMenu {
|
||||
}
|
||||
)
|
||||
add(
|
||||
ActionItem(R.drawable.symbol_save_android_24, context.getString(R.string.save)) {
|
||||
ActionItem(CoreUiR.drawable.symbol_save_android_24, context.getString(R.string.save)) {
|
||||
callbacks.onSave()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -35,18 +35,27 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon {
|
||||
ArrowEnd(icon(R.drawable.symbol_arrow_end_24)),
|
||||
At(icon(R.drawable.symbol_at_24)),
|
||||
Backup(icon(R.drawable.symbol_backup_24)),
|
||||
Blur(icon(R.drawable.symbol_blur_24)),
|
||||
BrushPen(icon(R.drawable.symbol_brush_pen_24)),
|
||||
BrushHighlighter(icon(R.drawable.symbol_brush_highlighter_24)),
|
||||
Camera(icon(R.drawable.symbol_camera_24)),
|
||||
CameraSwitch(icon(R.drawable.symbol_switch_24)),
|
||||
Check(icon(R.drawable.symbol_check_24)),
|
||||
CheckCircle(icon(R.drawable.symbol_check_circle_24)),
|
||||
ChevronRight(icon(R.drawable.symbol_chevron_right_24)),
|
||||
Copy(icon(R.drawable.symbol_copy_android_24)),
|
||||
CropLock(icon(R.drawable.symbol_crop_lock_24)),
|
||||
CropRotate(icon(R.drawable.symbol_crop_rotate_24)),
|
||||
CropUnlock(icon(R.drawable.symbol_crop_unlock_24)),
|
||||
Draw(icon(R.drawable.symbol_draw_24)),
|
||||
Edit(icon(R.drawable.symbol_edit_24)),
|
||||
Emoji(icon(R.drawable.symbol_emoji_24)),
|
||||
ErrorCircle(icon(R.drawable.symbol_error_circle_fill_24)),
|
||||
File(icon(R.drawable.symbol_file_24)),
|
||||
FlashAuto(icon(R.drawable.symbol_flash_auto_24)),
|
||||
FlashOff(icon(R.drawable.symbol_flash_slash_24)),
|
||||
File(icon(R.drawable.symbol_file_24)),
|
||||
FlashOn(icon(R.drawable.symbol_flash_24)),
|
||||
Flip(icon(R.drawable.symbol_flip_24)),
|
||||
Forward(icon(R.drawable.symbol_forward_24)),
|
||||
Info(icon(R.drawable.symbol_info_24)),
|
||||
Keyboard(icon(R.drawable.ic_keyboard_24)),
|
||||
@@ -56,10 +65,15 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon {
|
||||
Phone(icon(R.drawable.symbol_phone_24)),
|
||||
Plus(icon(R.drawable.symbol_plus_24)),
|
||||
QrCode(icon(R.drawable.symbol_qrcode_24)),
|
||||
QualityHigh(icon(R.drawable.symbol_quality_high_24)),
|
||||
QualityHighSlash(icon(R.drawable.symbol_quality_high_slash_24)),
|
||||
Recent(icon(R.drawable.symbol_recent_24)),
|
||||
Save(icon(R.drawable.symbol_save_android_24)),
|
||||
Search(icon(R.drawable.symbol_search_24)),
|
||||
Settings(icon(R.drawable.symbol_settings_android_24)),
|
||||
Share(icon(R.drawable.symbol_share_android_24)),
|
||||
Sticker(icon(R.drawable.symbol_sticker_24)),
|
||||
Text(icon(R.drawable.symbol_text_24)),
|
||||
Trash(icon(R.drawable.symbol_trash_24)),
|
||||
X(icon(R.drawable.symbol_x_24)),
|
||||
XCircleFill(icon(R.drawable.symbol_x_circle_fill_24))
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.13 22c0 0.48 0.39 0.88 0.87 0.88s0.88-0.4 0.88-0.88V2c0-0.48-0.4-0.88-0.88-0.88S11.12 1.52 11.12 2v20Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.13 3.13c-0.98 0-1.76 0-2.4 0.04C5.08 3.22 4.52 3.32 4 3.56 2.91 4.05 2.05 4.9 1.56 5.99 1.32 6.52 1.22 7.1 1.17 7.73c-0.05 0.64-0.05 1.42-0.04 2.4v3.74c0 0.98 0 1.76 0.04 2.4 0.05 0.65 0.15 1.21 0.39 1.74 0.49 1.08 1.35 1.94 2.43 2.43 0.53 0.24 1.1 0.34 1.74 0.39 0.64 0.05 1.42 0.05 2.4 0.05h0.12c0.48 0 0.88-0.4 0.88-0.88s-0.4-0.88-0.88-0.88H8.16c-1.02 0-1.74 0-2.3-0.04-0.55-0.04-0.89-0.11-1.15-0.23-0.69-0.32-1.24-0.87-1.56-1.56-0.12-0.26-0.2-0.6-0.23-1.15-0.04-0.56-0.04-1.28-0.04-2.3v-3.68c0-1.02 0-1.74 0.04-2.3 0.04-0.55 0.11-0.89 0.23-1.15 0.32-0.69 0.87-1.24 1.56-1.56 0.26-0.12 0.6-0.2 1.15-0.23 0.56-0.04 1.28-0.04 2.3-0.04h0.09c0.48 0 0.88-0.4 0.88-0.88s-0.4-0.88-0.88-0.88H8.13Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.88 4c0-0.48 0.39-0.88 0.87-0.88h0.5c0.48 0 0.88 0.4 0.88 0.88s-0.4 0.88-0.88 0.88h-0.5c-0.48 0-0.88-0.4-0.88-0.88Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.57 3.39c-0.44-0.18-0.95 0.04-1.13 0.48-0.18 0.45 0.04 0.96 0.48 1.14l0.5 0.2c0.45 0.18 0.96-0.04 1.14-0.49 0.18-0.44-0.04-0.95-0.48-1.13l-0.5-0.2Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.45 5.61c0.46-0.14 0.95 0.13 1.09 0.59l0.15 0.5c0.14 0.46-0.13 0.95-0.59 1.09-0.46 0.14-0.95-0.13-1.09-0.59l-0.15-0.5C20.72 6.24 21 5.75 21.45 5.61Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22.88 10c0-0.48-0.4-0.88-0.88-0.88s-0.88 0.4-0.88 0.88v0.5c0 0.48 0.4 0.88 0.88 0.88s0.88-0.4 0.88-0.88V10Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.88 20c0 0.48 0.39 0.88 0.87 0.88h0.5c0.48 0 0.88-0.4 0.88-0.88s-0.4-0.88-0.88-0.88h-0.5c-0.48 0-0.88 0.4-0.88 0.88Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.57 20.61c-0.44 0.18-0.95-0.04-1.13-0.48-0.18-0.45 0.04-0.96 0.48-1.14l0.5-0.2c0.45-0.18 0.96 0.04 1.14 0.49 0.18 0.44-0.04 0.95-0.48 1.13l-0.5 0.2Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.45 18.39c0.46 0.14 0.95-0.13 1.09-0.59l0.15-0.5c0.14-0.46-0.13-0.95-0.59-1.09-0.46-0.14-0.95 0.13-1.09 0.59l-0.15 0.5c-0.14 0.46 0.13 0.95 0.59 1.09Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22.88 14c0 0.48-0.4 0.88-0.88 0.88s-0.88-0.4-0.88-0.88v-0.5c0-0.48 0.4-0.88 0.88-0.88s0.88 0.4 0.88 0.88V14Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.36 21.78c0.5 0.1 1.1 0.1 2.12 0.1h0.1l0.33-0.01c0.4-0.03 0.79-0.16 1.13-0.37l0.28-0.2 10.11-7.48c0.42-0.31 0.71-0.53 0.93-0.8 0.19-0.26 0.33-0.54 0.42-0.84 0.1-0.34 0.1-0.7 0.1-1.22V10.2c0-1.3 0-2.32-0.07-3.14-0.07-0.82-0.2-1.48-0.5-2.07C20.8 4 20 3.2 19 2.7c-0.6-0.3-1.25-0.43-2.07-0.5-0.82-0.06-1.83-0.06-3.14-0.06h-3.6c-1.3 0-2.32 0-3.14 0.06C6.24 2.26 5.58 2.4 5 2.7 4 3.2 3.2 4 2.7 5 2.4 5.6 2.27 6.24 2.2 7.06 2.13 7.88 2.13 8.9 2.13 10.2v5.32c0 1.03 0 1.62 0.1 2.12 0.4 2.09 2.04 3.72 4.13 4.14Zm5.3-3.4l-0.79 0.73c0-0.8 0-1.4 0.04-1.88 0.03-0.5 0.1-0.83 0.2-1.11 0.36-0.93 1.08-1.65 2-2.01 0.29-0.1 0.62-0.17 1.12-0.2 0.5-0.03 1.14-0.04 2.02-0.04l2-0.11-1 0.55-0.04 0.04-5.5 4c-0.02 0-0.03 0.02-0.05 0.03ZM8.5 20.12H8.14c-0.73 0-1.04 0-1.29-0.03-1.52-0.23-2.7-1.42-2.94-2.94-0.03-0.25-0.03-0.56-0.03-1.29V9.78c0-1.13 0-1.95 0.05-2.6 0.05-0.63 0.15-1.05 0.33-1.4 0.33-0.65 0.87-1.19 1.53-1.52 0.34-0.18 0.76-0.28 1.4-0.33 0.64-0.05 1.46-0.06 2.59-0.06h4.45c1.12 0 1.94 0 2.58 0.06 0.64 0.05 1.06 0.15 1.4 0.33 0.66 0.33 1.2 0.87 1.53 1.53 0.18 0.34 0.28 0.76 0.33 1.4 0.05 0.64 0.05 1.46 0.05 2.58v0.86l-0.01 0.78c-0.01 0.17-0.04 0.25-0.07 0.3-0.07 0.15-0.18 0.26-0.32 0.33-0.06 0.03-0.14 0.06-0.31 0.07l-0.79 0.02h-2.76c-1.38 0-2.18 0-2.84 0.17-1.82 0.49-3.23 1.9-3.72 3.72-0.18 0.66-0.18 1.46-0.18 2.84v0.28 0.35c-0.05 0.32-0.3 0.58-0.63 0.63Z"/>
|
||||
</vector>
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
/**
|
||||
* Treating an Enum as a circular list, returns the "next"
|
||||
@@ -6,8 +6,11 @@
|
||||
package org.signal.mediasend
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import org.signal.core.models.media.Media
|
||||
import org.signal.core.models.media.MediaFolder
|
||||
|
||||
@@ -108,22 +111,38 @@ data class MediaSendState(
|
||||
/**
|
||||
* The [MediaFolder] list available on the system
|
||||
*/
|
||||
val mediaFolders: List<MediaFolder> = emptyList(),
|
||||
val mediaFolders: @WriteWith<TransientMediaFolderListParceler> List<MediaFolder> = emptyList(),
|
||||
|
||||
/**
|
||||
* The selected [MediaFolder] for which to display content in the Select screen
|
||||
*/
|
||||
val selectedMediaFolder: MediaFolder? = null,
|
||||
val selectedMediaFolder: @WriteWith<TransientMediaFolderParceler> MediaFolder? = null,
|
||||
|
||||
/**
|
||||
* The media content for a given selected [MediaFolder]
|
||||
*/
|
||||
val selectedMediaFolderItems: List<Media> = emptyList()
|
||||
val selectedMediaFolderItems: @WriteWith<TransientMediaListParceler> List<Media> = emptyList()
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* View-once toggle state.
|
||||
* No-op parcelers for fields that are re-loaded on init and should not
|
||||
* contribute to the saved-state bundle size.
|
||||
*/
|
||||
private object TransientMediaFolderListParceler : Parceler<List<MediaFolder>> {
|
||||
override fun create(parcel: Parcel): List<MediaFolder> = emptyList()
|
||||
override fun List<MediaFolder>.write(parcel: Parcel, flags: Int) = Unit
|
||||
}
|
||||
|
||||
private object TransientMediaFolderParceler : Parceler<MediaFolder?> {
|
||||
override fun create(parcel: Parcel): MediaFolder? = null
|
||||
override fun MediaFolder?.write(parcel: Parcel, flags: Int) = Unit
|
||||
}
|
||||
|
||||
private object TransientMediaListParceler : Parceler<List<Media>> {
|
||||
override fun create(parcel: Parcel): List<Media> = emptyList()
|
||||
override fun List<Media>.write(parcel: Parcel, flags: Int) = Unit
|
||||
}
|
||||
|
||||
enum class ViewOnceToggleState(val code: Int) {
|
||||
OFF(0),
|
||||
ONCE(1);
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend.edit
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.annotation.RememberInComposition
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import org.signal.imageeditor.core.model.EditorModel
|
||||
|
||||
@Stable
|
||||
sealed interface EditorController {
|
||||
|
||||
@Stable
|
||||
class Container @RememberInComposition constructor() {
|
||||
val controllers = SnapshotStateMap<Uri, EditorController>()
|
||||
|
||||
fun getOrCreateImageController(uri: Uri, editorModel: EditorModel): Image {
|
||||
return controllers.getOrPut(uri) { Image(editorModel) } as Image
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Image @RememberInComposition constructor(val editorModel: EditorModel) : EditorController {
|
||||
|
||||
override val isUserInEdit: Boolean
|
||||
get() = mode != Mode.NONE
|
||||
|
||||
var mode: Mode by mutableStateOf(Mode.NONE)
|
||||
|
||||
var isCropLocked: Boolean by mutableStateOf(editorModel.isCropAspectLocked)
|
||||
private set
|
||||
|
||||
val isUserDrawing: Boolean
|
||||
get() = mode == Mode.DRAW || mode == Mode.HIGHLIGHT
|
||||
|
||||
val isUserBlurring: Boolean
|
||||
get() = mode == Mode.BLUR
|
||||
|
||||
val isUserEnteringText: Boolean
|
||||
get() = mode == Mode.TEXT
|
||||
|
||||
val isUserInsertingSticker: Boolean
|
||||
get() = mode == Mode.INSERT_STICKER
|
||||
|
||||
fun beginDrawEdit() {
|
||||
enterDrawMode()
|
||||
}
|
||||
|
||||
fun beginCropAndRotateEdit() {
|
||||
enterCropMode()
|
||||
}
|
||||
|
||||
fun cancelEdit() {
|
||||
mode = Mode.NONE
|
||||
}
|
||||
|
||||
fun commitEdit() {
|
||||
mode = Mode.NONE
|
||||
}
|
||||
|
||||
fun enterDrawMode() {
|
||||
mode = Mode.DRAW
|
||||
}
|
||||
|
||||
fun enterHighlightMode() {
|
||||
mode = Mode.HIGHLIGHT
|
||||
}
|
||||
|
||||
fun enterBlurMode() {
|
||||
mode = Mode.BLUR
|
||||
}
|
||||
|
||||
fun enterCropMode() {
|
||||
mode = Mode.CROP
|
||||
}
|
||||
|
||||
fun enterTextMode() {
|
||||
mode = Mode.TEXT
|
||||
}
|
||||
|
||||
fun enterStickerMode() {
|
||||
mode = Mode.INSERT_STICKER
|
||||
}
|
||||
|
||||
fun lockCrop() {
|
||||
editorModel.setCropAspectLock(true)
|
||||
isCropLocked = true
|
||||
}
|
||||
|
||||
fun unlockCrop() {
|
||||
editorModel.setCropAspectLock(false)
|
||||
isCropLocked = false
|
||||
}
|
||||
|
||||
fun flip() {
|
||||
editorModel.flipHorizontal()
|
||||
}
|
||||
|
||||
fun rotate() {
|
||||
editorModel.rotate90anticlockwise()
|
||||
}
|
||||
|
||||
fun toggleImageQuality() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fun saveToDisk() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fun addMedia() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
NONE,
|
||||
CROP,
|
||||
TEXT,
|
||||
DRAW,
|
||||
HIGHLIGHT,
|
||||
BLUR,
|
||||
MOVE_STICKER,
|
||||
MOVE_TEXT,
|
||||
DELETE,
|
||||
INSERT_STICKER
|
||||
}
|
||||
}
|
||||
|
||||
object VideoTrim : EditorController {
|
||||
override val isUserInEdit: Boolean = false
|
||||
}
|
||||
|
||||
val isUserInEdit: Boolean
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import org.signal.imageeditor.core.ImageEditorView
|
||||
|
||||
@Composable
|
||||
fun ImageEditor(
|
||||
controller: ImageEditorController,
|
||||
controller: EditorController.Image,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AndroidView(
|
||||
@@ -27,17 +27,17 @@ fun ImageEditor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapMode(mode: ImageEditorController.Mode): ImageEditorView.Mode {
|
||||
private fun mapMode(mode: EditorController.Image.Mode): ImageEditorView.Mode {
|
||||
return when (mode) {
|
||||
ImageEditorController.Mode.NONE -> ImageEditorView.Mode.MoveAndResize
|
||||
ImageEditorController.Mode.CROP -> ImageEditorView.Mode.MoveAndResize
|
||||
ImageEditorController.Mode.TEXT -> ImageEditorView.Mode.MoveAndResize
|
||||
ImageEditorController.Mode.DRAW -> ImageEditorView.Mode.Draw
|
||||
ImageEditorController.Mode.HIGHLIGHT -> ImageEditorView.Mode.Draw
|
||||
ImageEditorController.Mode.BLUR -> ImageEditorView.Mode.Blur
|
||||
ImageEditorController.Mode.MOVE_STICKER -> ImageEditorView.Mode.MoveAndResize
|
||||
ImageEditorController.Mode.MOVE_TEXT -> ImageEditorView.Mode.MoveAndResize
|
||||
ImageEditorController.Mode.DELETE -> ImageEditorView.Mode.MoveAndResize
|
||||
ImageEditorController.Mode.INSERT_STICKER -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.NONE -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.CROP -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.TEXT -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.DRAW -> ImageEditorView.Mode.Draw
|
||||
EditorController.Image.Mode.HIGHLIGHT -> ImageEditorView.Mode.Draw
|
||||
EditorController.Image.Mode.BLUR -> ImageEditorView.Mode.Blur
|
||||
EditorController.Image.Mode.MOVE_STICKER -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.MOVE_TEXT -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.DELETE -> ImageEditorView.Mode.MoveAndResize
|
||||
EditorController.Image.Mode.INSERT_STICKER -> ImageEditorView.Mode.MoveAndResize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend.edit
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.annotation.RememberInComposition
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import org.signal.imageeditor.core.model.EditorModel
|
||||
|
||||
@Stable
|
||||
class ImageEditorController @RememberInComposition constructor(
|
||||
val editorModel: EditorModel
|
||||
) {
|
||||
|
||||
var mode: Mode by mutableStateOf(Mode.NONE)
|
||||
|
||||
enum class Mode {
|
||||
NONE,
|
||||
CROP,
|
||||
TEXT,
|
||||
DRAW,
|
||||
HIGHLIGHT,
|
||||
BLUR,
|
||||
MOVE_STICKER,
|
||||
MOVE_TEXT,
|
||||
DELETE,
|
||||
INSERT_STICKER
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend.edit
|
||||
|
||||
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
|
||||
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
|
||||
import org.signal.core.ui.compose.IconButtons.iconToggleButtonColors
|
||||
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 java.util.EnumMap
|
||||
|
||||
@Composable
|
||||
fun ImageEditorToolbar(
|
||||
imageEditorController: EditorController.Image,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
when (imageEditorController.mode) {
|
||||
EditorController.Image.Mode.NONE -> {
|
||||
ImageEditorNoneStateToolbar(imageEditorController, modifier)
|
||||
}
|
||||
EditorController.Image.Mode.CROP -> {
|
||||
ImageEditorCropAndResizeToolbar(imageEditorController, modifier)
|
||||
}
|
||||
else -> {
|
||||
ImageEditorDrawStateToolbar(imageEditorController, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows user to perform actions while viewing an editable image.
|
||||
*/
|
||||
@Composable
|
||||
private fun ImageEditorNoneStateToolbar(
|
||||
imageEditorController: EditorController.Image,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
OrientedImageEditorToolbar(modifier) {
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.BrushPen.imageVector,
|
||||
onClick = imageEditorController::beginDrawEdit
|
||||
)
|
||||
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.CropRotate.imageVector,
|
||||
onClick = imageEditorController::beginCropAndRotateEdit
|
||||
)
|
||||
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.QualityHigh.imageVector,
|
||||
onClick = imageEditorController::toggleImageQuality
|
||||
)
|
||||
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.Save.imageVector,
|
||||
onClick = imageEditorController::saveToDisk
|
||||
)
|
||||
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.Plus.imageVector, // TODO [alex] - wrong art asset
|
||||
onClick = imageEditorController::addMedia
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageEditorDrawStateToolbar(
|
||||
imageEditorController: EditorController.Image,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
OrientedImageEditorToolbar(
|
||||
modifier = modifier,
|
||||
leading = {
|
||||
CommitButton(imageEditorController)
|
||||
},
|
||||
trailing = {
|
||||
DiscardButton(imageEditorController)
|
||||
}
|
||||
) {
|
||||
ImageEditorToggleButton(
|
||||
imageVector = SignalIcons.Draw.imageVector,
|
||||
checked = imageEditorController.isUserDrawing,
|
||||
onCheckChanged = {
|
||||
if (!imageEditorController.isUserDrawing) {
|
||||
imageEditorController.enterDrawMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ImageEditorToggleButton(
|
||||
imageVector = SignalIcons.Text.imageVector,
|
||||
checked = imageEditorController.isUserEnteringText,
|
||||
onCheckChanged = {
|
||||
if (!imageEditorController.isUserEnteringText) {
|
||||
imageEditorController.enterTextMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ImageEditorToggleButton(
|
||||
imageVector = SignalIcons.Sticker.imageVector,
|
||||
checked = imageEditorController.isUserInsertingSticker,
|
||||
onCheckChanged = {
|
||||
if (!imageEditorController.isUserInsertingSticker) {
|
||||
imageEditorController.enterStickerMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ImageEditorToggleButton(
|
||||
imageVector = SignalIcons.Blur.imageVector,
|
||||
checked = imageEditorController.isUserBlurring,
|
||||
onCheckChanged = {
|
||||
if (!imageEditorController.isUserBlurring) {
|
||||
imageEditorController.enterBlurMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageEditorCropAndResizeToolbar(
|
||||
imageEditorController: EditorController.Image,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
OrientedImageEditorToolbar(
|
||||
modifier = modifier,
|
||||
leading = {
|
||||
CommitButton(imageEditorController)
|
||||
},
|
||||
trailing = {
|
||||
DiscardButton(imageEditorController)
|
||||
}
|
||||
) {
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.CropRotate.imageVector,
|
||||
onClick = imageEditorController::rotate
|
||||
)
|
||||
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.Flip.imageVector,
|
||||
onClick = imageEditorController::flip
|
||||
)
|
||||
|
||||
val cropLockImageVector = SignalIcons.CropLock.imageVector
|
||||
val cropUnlockImageVector = SignalIcons.CropUnlock.imageVector
|
||||
|
||||
IconCrossfadeToggleButton(
|
||||
target = if (imageEditorController.isCropLocked) CropLock.LOCKED else CropLock.UNLOCKED,
|
||||
setTarget = { target ->
|
||||
when (target) {
|
||||
CropLock.LOCKED -> imageEditorController.lockCrop()
|
||||
CropLock.UNLOCKED -> imageEditorController.unlockCrop()
|
||||
}
|
||||
},
|
||||
targetToImageMap = remember(cropLockImageVector, cropUnlockImageVector) {
|
||||
EnumMap<CropLock, ImageVector>(
|
||||
CropLock::class.java
|
||||
).apply {
|
||||
put(CropLock.LOCKED, cropLockImageVector)
|
||||
put(CropLock.UNLOCKED, cropUnlockImageVector)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommitButton(imageEditorController: EditorController.Image) {
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.Check.imageVector,
|
||||
onClick = imageEditorController::commitEdit,
|
||||
colors = IconButtons.iconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DiscardButton(imageEditorController: EditorController.Image) {
|
||||
ImageEditorButton(
|
||||
imageVector = SignalIcons.X.imageVector,
|
||||
onClick = imageEditorController::cancelEdit,
|
||||
colors = IconButtons.iconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun <reified E : Enum<E>> IconCrossfadeToggleButton(
|
||||
target: E,
|
||||
crossinline setTarget: (E) -> Unit,
|
||||
targetToImageMap: EnumMap<E, ImageVector>
|
||||
) {
|
||||
IconButtons.IconButton(
|
||||
onClick = { setTarget(target.next()) }
|
||||
) {
|
||||
Crossfade(target) { enumValue ->
|
||||
Icon(
|
||||
imageVector = targetToImageMap[enumValue]!!,
|
||||
contentDescription = null, // TODO
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
checked: Boolean,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
IconButtons.IconToggleButton(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckChanged,
|
||||
colors = iconToggleButtonColors(
|
||||
checkedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
checkedContainerColor = SignalTheme.colors.colorTransparentInverse2
|
||||
)
|
||||
) {
|
||||
Icon(imageVector = imageVector, contentDescription = contentDescription, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrientedImageEditorToolbar(
|
||||
modifier: Modifier = Modifier,
|
||||
leading: @Composable () -> Unit = {},
|
||||
trailing: @Composable () -> Unit = {},
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val windowBreakpoint = rememberWindowBreakpoint()
|
||||
val isRow = windowBreakpoint == 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
|
||||
@FoldablePortraitNightPreview
|
||||
@Composable
|
||||
private fun ImageEditorNoneStateToolbarPreview() {
|
||||
Previews.Preview {
|
||||
ImageEditorNoneStateToolbar(
|
||||
imageEditorController = remember {
|
||||
EditorController.Image(EditorModel.create(0))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PhonePortraitDayPreview
|
||||
@PhonePortraitNightPreview
|
||||
@FoldablePortraitDayPreview
|
||||
@FoldablePortraitNightPreview
|
||||
@Composable
|
||||
private fun ImageEditorDrawStateToolbarPreview() {
|
||||
Previews.Preview {
|
||||
ImageEditorDrawStateToolbar(
|
||||
imageEditorController = remember {
|
||||
EditorController.Image(EditorModel.create(0)).apply {
|
||||
enterDrawMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PhonePortraitDayPreview
|
||||
@PhonePortraitNightPreview
|
||||
@FoldablePortraitDayPreview
|
||||
@FoldablePortraitNightPreview
|
||||
@Composable
|
||||
private fun ImageEditorCropAndResizeToolbarPreview() {
|
||||
Previews.Preview {
|
||||
ImageEditorCropAndResizeToolbar(
|
||||
imageEditorController = remember {
|
||||
EditorController.Image(EditorModel.create(0)).apply {
|
||||
enterCropMode()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class CropLock {
|
||||
LOCKED, UNLOCKED
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend.edit
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/**
|
||||
* Allows user to perform actions while viewing an editable image.
|
||||
*/
|
||||
@Composable
|
||||
fun ImageEditorTopLevelToolbar(
|
||||
imageEditorController: ImageEditorController
|
||||
) {
|
||||
// Draw -- imageEditorController draw mode
|
||||
// Crop&Rotate -- imageEditorController crop mode
|
||||
// Quality -- callback toggle quality
|
||||
// Save -- callback save to disk
|
||||
// Add -- callback go to media select
|
||||
}
|
||||
|
||||
interface ImageEditorToolbarsCallback {
|
||||
|
||||
object Empty : ImageEditorToolbarsCallback
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -28,12 +27,13 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation3.runtime.NavBackStack
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.models.media.Media
|
||||
import org.signal.core.ui.WindowBreakpoint
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.util.ContentTypeUtil
|
||||
import org.signal.core.ui.rememberWindowBreakpoint
|
||||
import org.signal.imageeditor.core.model.EditorModel
|
||||
import org.signal.mediasend.EditorState
|
||||
import org.signal.mediasend.MediaSendNavKey
|
||||
import org.signal.mediasend.MediaSendState
|
||||
@@ -46,7 +46,6 @@ fun MediaEditScreen(
|
||||
backStack: NavBackStack<NavKey>,
|
||||
videoEditorSlot: @Composable () -> Unit = {}
|
||||
) {
|
||||
val isFocusedMediaVideo = ContentTypeUtil.isVideoType(state.focusedMedia?.contentType)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
@@ -59,15 +58,28 @@ fun MediaEditScreen(
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
val isSmallWindowBreakpoint = rememberWindowBreakpoint() == WindowBreakpoint.SMALL
|
||||
val controllers = remember { EditorController.Container() }
|
||||
|
||||
val currentController = state.focusedMedia?.let {
|
||||
when (val editorState = state.editorStateMap[it.uri]) {
|
||||
is EditorState.Image -> controllers.getOrCreateImageController(it.uri, editorState.model)
|
||||
is EditorState.VideoTrim -> EditorController.VideoTrim
|
||||
null -> error("Invalid editor state.")
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
snapPosition = SnapPosition.Center
|
||||
snapPosition = SnapPosition.Center,
|
||||
userScrollEnabled = currentController?.isUserInEdit != true
|
||||
) { index ->
|
||||
when (val editorState = state.editorStateMap[state.selectedMedia[index].uri]) {
|
||||
val uri = state.selectedMedia[index].uri
|
||||
when (val editorState = state.editorStateMap[uri]) {
|
||||
is EditorState.Image -> {
|
||||
ImageEditor(
|
||||
controller = remember { ImageEditorController(editorState.model) },
|
||||
controller = controllers.getOrCreateImageController(uri, editorState.model),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
@@ -104,10 +116,13 @@ fun MediaEditScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (isFocusedMediaVideo) {
|
||||
// Video editor hud
|
||||
} else if (!currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)) {
|
||||
// Image editor HU
|
||||
when (currentController) {
|
||||
is EditorController.Image -> {
|
||||
if (isSmallWindowBreakpoint) {
|
||||
ImageEditorToolbar(imageEditorController = currentController)
|
||||
}
|
||||
}
|
||||
is EditorController.VideoTrim, null -> Unit
|
||||
}
|
||||
|
||||
AddAMessageRow(
|
||||
@@ -120,6 +135,13 @@ fun MediaEditScreen(
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSmallWindowBreakpoint && currentController is EditorController.Image) {
|
||||
ImageEditorToolbar(
|
||||
imageEditorController = currentController,
|
||||
modifier = Modifier.align(Alignment.CenterEnd).padding(end = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +154,10 @@ private fun MediaEditScreenPreview() {
|
||||
MediaEditScreen(
|
||||
state = MediaSendState(
|
||||
selectedMedia = selectedMedia,
|
||||
focusedMedia = selectedMedia.first()
|
||||
focusedMedia = selectedMedia.first(),
|
||||
editorStateMap = mutableMapOf(
|
||||
selectedMedia.first().uri to EditorState.Image(EditorModel.create(0))
|
||||
)
|
||||
),
|
||||
callback = MediaEditScreenCallback.Empty,
|
||||
backStack = rememberNavBackStack(MediaSendNavKey.Edit),
|
||||
|
||||
Reference in New Issue
Block a user