mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 22:55:48 +00:00
android, desktop: blur for media (#4508)
* android, desktop: blur for media * change * new option and applied blur to other elements * new line * added to migration * long click handling * hover on desktop * changes * change * showDownloadButton function * file rename * don't blur when menu is visible * rename
This commit is contained in:
committed by
GitHub
parent
a53333be20
commit
032c5d3a5b
@@ -28,3 +28,5 @@ actual fun Modifier.desktopOnExternalDrag(
|
||||
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
|
||||
|
||||
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this
|
||||
|
||||
actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier
|
||||
|
||||
@@ -116,6 +116,7 @@ class AppPreferences {
|
||||
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
|
||||
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
||||
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
|
||||
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||
@@ -328,6 +329,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
|
||||
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
||||
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
|
||||
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
|
||||
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
||||
@@ -5998,6 +6000,7 @@ data class AppSettings(
|
||||
var privacyShowChatPreviews: Boolean? = null,
|
||||
var privacySaveLastDraft: Boolean? = null,
|
||||
var privacyProtectScreen: Boolean? = null,
|
||||
var privacyMediaBlurRadius: Int? = null,
|
||||
var notificationMode: AppSettingsNotificationMode? = null,
|
||||
var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null,
|
||||
var webrtcPolicyRelay: Boolean? = null,
|
||||
@@ -6027,6 +6030,7 @@ data class AppSettings(
|
||||
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||
if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen }
|
||||
if (privacyMediaBlurRadius != def.privacyMediaBlurRadius) { empty.privacyMediaBlurRadius = privacyMediaBlurRadius }
|
||||
if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode }
|
||||
if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode }
|
||||
if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay }
|
||||
@@ -6064,6 +6068,7 @@ data class AppSettings(
|
||||
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
||||
privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) }
|
||||
privacyProtectScreen?.let { def.privacyProtectScreen.set(it) }
|
||||
privacyMediaBlurRadius?.let { def.privacyMediaBlurRadius.set(it) }
|
||||
notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) }
|
||||
notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) }
|
||||
webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) }
|
||||
@@ -6094,6 +6099,7 @@ data class AppSettings(
|
||||
privacyShowChatPreviews = true,
|
||||
privacySaveLastDraft = true,
|
||||
privacyProtectScreen = false,
|
||||
privacyMediaBlurRadius = 0,
|
||||
notificationMode = AppSettingsNotificationMode.INSTANT,
|
||||
notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE,
|
||||
webrtcPolicyRelay = true,
|
||||
@@ -6125,6 +6131,7 @@ data class AppSettings(
|
||||
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
||||
privacySaveLastDraft = def.privacySaveLastDraft.get(),
|
||||
privacyProtectScreen = def.privacyProtectScreen.get(),
|
||||
privacyMediaBlurRadius = def.privacyMediaBlurRadius.get(),
|
||||
notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()),
|
||||
notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)),
|
||||
webrtcPolicyRelay = def.webrtcPolicyRelay.get(),
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.views.helpers.KeyChangeEffect
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.io.File
|
||||
|
||||
expect fun Modifier.navigationBarsWithImePadding(): Modifier
|
||||
@@ -25,3 +34,68 @@ expect fun Modifier.desktopOnExternalDrag(
|
||||
expect fun Modifier.onRightClick(action: () -> Unit): Modifier
|
||||
|
||||
expect fun Modifier.desktopPointerHoverIconHand(): Modifier
|
||||
|
||||
expect fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier
|
||||
|
||||
@Composable
|
||||
fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState<Boolean>, showMenu: State<Boolean>,): Modifier {
|
||||
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
|
||||
if (appPlatform.isDesktop) {
|
||||
KeyChangeEffect(blurRadius.value) {
|
||||
blurred.value = enabled && blurRadius.value > 0
|
||||
}
|
||||
}
|
||||
return if (appPlatform.isDesktop && enabled && blurRadius.value > 0 && !showMenu.value) {
|
||||
var job: Job = remember { Job() }
|
||||
LaunchedEffect(Unit) {
|
||||
// The approach here is to allow menu to show up and to not blur the view. When menu is shown and mouse is hovering,
|
||||
// unhovered action is still received, but we don't need to handle it until menu closes. When it closes, it takes one frame to catch a
|
||||
// hover action again and if:
|
||||
// 1. mouse is still on the view, the hover action will cancel this coroutine and the view will stay unblurred
|
||||
// 2. mouse is not on the view, the view will become blurred after 100 ms
|
||||
job = launch {
|
||||
delay(100)
|
||||
blurred.value = true
|
||||
}
|
||||
}
|
||||
this then Modifier.desktopOnHovered { hovered ->
|
||||
job.cancel()
|
||||
blurred.value = !hovered && !showMenu.value
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.privacyBlur(
|
||||
enabled: Boolean,
|
||||
blurred: MutableState<Boolean> = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) },
|
||||
scrollState: State<Boolean>,
|
||||
onLongClick: () -> Unit = {}
|
||||
): Modifier {
|
||||
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
|
||||
return if (enabled && blurred.value) {
|
||||
this then Modifier.blur(
|
||||
radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
|
||||
radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
|
||||
edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp))
|
||||
)
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClick,
|
||||
onClick = {
|
||||
blurred.value = false
|
||||
}
|
||||
)
|
||||
} else if (enabled && blurRadius.value > 0 && appPlatform.isAndroid) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { scrollState.value }
|
||||
.filter { it }
|
||||
.filter { !blurred.value }
|
||||
.collect { blurred.value = true }
|
||||
}
|
||||
this
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,6 +1113,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.isScrollInProgress }
|
||||
.collect {
|
||||
chatViewScrollState.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -1326,6 +1332,8 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
val chatViewScrollState = MutableStateFlow(false)
|
||||
|
||||
private fun bottomEndFloatingButton(
|
||||
unreadCount: Int,
|
||||
showButtonWithCounter: Boolean,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.HoverInteraction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -17,8 +19,10 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
||||
import chat.simplex.common.views.chat.chatViewScrollState
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -32,6 +36,7 @@ fun CIImageView(
|
||||
smallView: Boolean,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
@Composable
|
||||
fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
@@ -105,7 +110,8 @@ fun CIImageView(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
.onRightClick { showMenu.value = true }
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
@@ -128,7 +134,8 @@ fun CIImageView(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
.onRightClick { showMenu.value = true }
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
|
||||
)
|
||||
} else {
|
||||
@@ -138,7 +145,8 @@ fun CIImageView(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = {}
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
.onRightClick { showMenu.value = true }
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
@@ -174,7 +182,8 @@ fun CIImageView(
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
|
||||
.desktopModifyBlurredState(!smallView, blurred, showMenu),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember {
|
||||
@@ -256,9 +265,10 @@ fun CIImageView(
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!smallView) {
|
||||
// Do not show download icon when the view is blurred
|
||||
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
|
||||
loadingIndicator()
|
||||
} else if (file?.showStatusIconInSmallView == true) {
|
||||
} else if (smallView && file?.showStatusIconInSmallView == true) {
|
||||
Box(Modifier.align(Alignment.Center)) {
|
||||
loadingIndicator()
|
||||
}
|
||||
@@ -266,6 +276,9 @@ fun CIImageView(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDownloadButton(status: CIFileStatus?): Boolean =
|
||||
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
|
||||
|
||||
@Composable
|
||||
expect fun SimpleAndAnimatedImageView(
|
||||
data: ByteArray,
|
||||
|
||||
@@ -19,7 +19,9 @@ import chat.simplex.res.MR
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.chatViewScrollState
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
@@ -34,8 +36,10 @@ fun CIVideoView(
|
||||
smallView: Boolean = false,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
|
||||
.desktopModifyBlurredState(!smallView, blurred, showMenu),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val preview = remember(image) { base64ToBitmap(image) }
|
||||
@@ -68,15 +72,15 @@ fun CIVideoView(
|
||||
if (decrypted != null && smallView) {
|
||||
SmallVideoView(decrypted, file, preview, duration * 1000L, autoPlay, sizeMultiplier, openFullscreen = openFullscreen)
|
||||
} else if (decrypted != null) {
|
||||
VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen)
|
||||
VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen)
|
||||
} else if (smallView) {
|
||||
SmallVideoViewEncrypted(uriDecrypted, file, preview, autoPlay, showMenu, sizeMultiplier, openFullscreen = openFullscreen)
|
||||
} else {
|
||||
VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen)
|
||||
VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen)
|
||||
}
|
||||
} else {
|
||||
Box {
|
||||
VideoPreviewImageView(preview, onClick = {
|
||||
VideoPreviewImageView(preview, blurred = blurred, onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
@@ -109,14 +113,15 @@ fun CIVideoView(
|
||||
if (file != null && !smallView) {
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) {
|
||||
if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) {
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!smallView) {
|
||||
// Do not show download icon when the view is blurred
|
||||
if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) {
|
||||
fileStatusIcon(file, false)
|
||||
} else if (file?.showStatusIconInSmallView == true) {
|
||||
} else if (smallView && file?.showStatusIconInSmallView == true) {
|
||||
Box(Modifier.align(Alignment.Center)) {
|
||||
fileStatusIcon(file, true)
|
||||
}
|
||||
@@ -132,15 +137,16 @@ private fun VideoViewEncrypted(
|
||||
defaultDuration: Long,
|
||||
autoPlay: MutableState<Boolean>,
|
||||
showMenu: MutableState<Boolean>,
|
||||
blurred: MutableState<Boolean>,
|
||||
openFullscreen: () -> Unit,
|
||||
) {
|
||||
var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) }
|
||||
val onLongClick = { showMenu.value = true }
|
||||
Box {
|
||||
VideoPreviewImageView(defaultPreview, smallView = false, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
|
||||
VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
|
||||
if (decryptionInProgress) {
|
||||
VideoDecryptionProgress(1f, onLongClick = onLongClick)
|
||||
} else {
|
||||
} else if (!blurred.value) {
|
||||
PlayButton(false, 1f, onLongClick = onLongClick) {
|
||||
decryptionInProgress = true
|
||||
withBGApi {
|
||||
@@ -170,7 +176,7 @@ private fun SmallVideoViewEncrypted(
|
||||
var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) }
|
||||
val onLongClick = { showMenu.value = true }
|
||||
Box {
|
||||
VideoPreviewImageView(defaultPreview, smallView = true, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
|
||||
VideoPreviewImageView(defaultPreview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = if (decryptionInProgress) {{}} else openFullscreen, onLongClick = onLongClick)
|
||||
if (decryptionInProgress) {
|
||||
VideoDecryptionProgress(sizeMultiplier, onLongClick = onLongClick)
|
||||
} else if (!file.showStatusIconInSmallView) {
|
||||
@@ -190,7 +196,15 @@ private fun SmallVideoViewEncrypted(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmallVideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState<Boolean>, sizeMultiplier: Float, openFullscreen: () -> Unit) {
|
||||
private fun SmallVideoView(
|
||||
uri: URI,
|
||||
file: CIFile,
|
||||
defaultPreview: ImageBitmap,
|
||||
defaultDuration: Long,
|
||||
autoPlay: MutableState<Boolean>,
|
||||
sizeMultiplier: Float,
|
||||
openFullscreen: () -> Unit
|
||||
) {
|
||||
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, defaultDuration, true) }
|
||||
val preview by remember { player.preview }
|
||||
// val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled }
|
||||
@@ -205,7 +219,7 @@ private fun SmallVideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap,
|
||||
onLongClick = {},
|
||||
{}
|
||||
)
|
||||
VideoPreviewImageView(preview, smallView = true, openFullscreen, onLongClick = {})
|
||||
VideoPreviewImageView(preview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = openFullscreen, onLongClick = {})
|
||||
if (!file.showStatusIconInSmallView) {
|
||||
PlayButton(brokenVideo, sizeMultiplier, onLongClick = {}, onClick = openFullscreen)
|
||||
}
|
||||
@@ -216,7 +230,16 @@ private fun SmallVideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap,
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState<Boolean>, showMenu: MutableState<Boolean>, openFullscreen: () -> Unit) {
|
||||
private fun VideoView(
|
||||
uri: URI,
|
||||
file: CIFile,
|
||||
defaultPreview: ImageBitmap,
|
||||
defaultDuration: Long,
|
||||
autoPlay: MutableState<Boolean>,
|
||||
showMenu: MutableState<Boolean>,
|
||||
blurred: MutableState<Boolean>,
|
||||
openFullscreen: () -> Unit
|
||||
) {
|
||||
val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) }
|
||||
val videoPlaying = remember(uri.path) { player.videoPlaying }
|
||||
val progress = remember(uri.path) { player.progress }
|
||||
@@ -257,8 +280,8 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
||||
stop
|
||||
)
|
||||
if (showPreview.value) {
|
||||
VideoPreviewImageView(preview, smallView = false, openFullscreen, onLongClick)
|
||||
if (!autoPlay.value) {
|
||||
VideoPreviewImageView(preview, smallView = false, blurred = blurred, openFullscreen, onLongClick)
|
||||
if (!autoPlay.value && !blurred.value) {
|
||||
PlayButton(brokenVideo, onLongClick = onLongClick, onClick = play)
|
||||
}
|
||||
}
|
||||
@@ -365,7 +388,13 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoPreviewImageView(preview: ImageBitmap, smallView: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
|
||||
fun VideoPreviewImageView(
|
||||
preview: ImageBitmap,
|
||||
smallView: Boolean,
|
||||
blurred: MutableState<Boolean>,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||
Image(
|
||||
@@ -377,7 +406,8 @@ fun VideoPreviewImageView(preview: ImageBitmap, smallView: Boolean, onClick: ()
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick
|
||||
)
|
||||
.onRightClick(onLongClick),
|
||||
.onRightClick(onLongClick)
|
||||
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick),
|
||||
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
@@ -522,6 +552,9 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDownloadButton(status: CIFileStatus?): Boolean =
|
||||
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
|
||||
|
||||
private fun fileSizeValid(file: CIFile?): Boolean {
|
||||
if (file != null) {
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
@@ -267,7 +267,7 @@ fun FramedItemView(
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview)
|
||||
ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true })
|
||||
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -15,9 +14,11 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.LinkPreview
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.chatViewScrollState
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -121,12 +122,16 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemLinkView(linkPreview: LinkPreview) {
|
||||
fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State<Boolean>, onLongClick: () -> Unit) {
|
||||
Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
|
||||
val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }
|
||||
Image(
|
||||
base64ToBitmap(linkPreview.image),
|
||||
stringResource(MR.strings.image_descr_link_preview),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.desktopModifyBlurredState(true, blurred, showMenu)
|
||||
.privacyBlur(true, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
Column(Modifier.padding(top = 6.dp).padding(horizontal = 12.dp)) {
|
||||
@@ -179,7 +184,7 @@ private fun normalizeImageUri(u: URL, imageUri: String) = when {
|
||||
@Composable
|
||||
fun PreviewChatItemLinkView() {
|
||||
SimpleXTheme {
|
||||
ChatItemLinkView(LinkPreview.sampleData)
|
||||
ChatItemLinkView(LinkPreview.sampleData, remember { mutableStateOf(false) }) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.ProfileNameField
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -32,6 +33,8 @@ import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
import chat.simplex.common.views.onboarding.ReadableText
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class LAMode {
|
||||
SYSTEM,
|
||||
@@ -95,6 +98,9 @@ fun PrivacySettingsView(
|
||||
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
||||
})
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) {
|
||||
appPrefs.privacyMediaBlurRadius.set(it)
|
||||
}
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
|
||||
}
|
||||
SectionCustomFooter {
|
||||
@@ -217,6 +223,30 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlurRadiusOptions(state: State<Int>, onSelected: (Int) -> Unit) {
|
||||
val choices = listOf(0, 12, 24, 48)
|
||||
val pickerValues = choices + if (choices.contains(state.value)) emptyList() else listOf(state.value)
|
||||
val values = remember {
|
||||
pickerValues.map {
|
||||
when (it) {
|
||||
0 -> it to generalGetString(MR.strings.privacy_media_blur_radius_off)
|
||||
12 -> it to generalGetString(MR.strings.privacy_media_blur_radius_soft)
|
||||
24 -> it to generalGetString(MR.strings.privacy_media_blur_radius_medium)
|
||||
48 -> it to generalGetString(MR.strings.privacy_media_blur_radius_strong)
|
||||
else -> it to "$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.privacy_media_blur_radius),
|
||||
values,
|
||||
state,
|
||||
icon = painterResource(MR.images.ic_blur_on),
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun PrivacyDeviceSection(
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
|
||||
@@ -1102,6 +1102,11 @@
|
||||
<string name="receipts_groups_disable_keep_overrides">Disable (keep group overrides)</string>
|
||||
<string name="receipts_groups_enable_for_all">Enable for all groups</string>
|
||||
<string name="receipts_groups_disable_for_all">Disable for all groups</string>
|
||||
<string name="privacy_media_blur_radius">Blur media</string>
|
||||
<string name="privacy_media_blur_radius_off">Off</string>
|
||||
<string name="privacy_media_blur_radius_soft">Soft</string>
|
||||
<string name="privacy_media_blur_radius_medium">Medium</string>
|
||||
<string name="privacy_media_blur_radius_strong">Strong</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">YOU</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M124-382q-9.5 0-15.25-6T103-403q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-155q-9.5 0-15.25-6T103-558q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm117.96 332.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-161q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-154.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q225.68-595 241.59-595t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q257.91-520 241.96-520Zm0-161q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q225.68-756 241.59-756t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q257.91-681 241.96-681Zm160.91 331q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44Q456-425.12 456-403.06q0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm0-154.5q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44Q456-579.62 456-557.56q0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm.09 300q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77 10.59-10.89 26.5-10.89t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-476.5q-15.96 0-26.71-10.74-10.75-10.73-10.75-26.6 0-15.87 10.59-26.77Q386.68-756 402.59-756t26.91 10.74q11 10.73 11 26.6 0 15.87-10.79 26.77Q418.91-681 402.96-681Zm.04 578q-9.5 0-15.25-6T382-124q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-712.5q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6ZM557.37-350q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44 15.75 15.44 15.75 37.5 0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm0-154.5q-21.87 0-37.37-15.44-15.5-15.44-15.5-37.5 0-22.06 15.42-37.56 15.43-15.5 37.46-15.5 21.62 0 37.37 15.44 15.75 15.44 15.75 37.5 0 22.06-15.63 37.56-15.63 15.5-37.5 15.5Zm.09 300q-15.96 0-26.71-10.74Q520-225.97 520-241.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T584-268.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-476.5q-15.96 0-26.71-10.74Q520-702.47 520-718.34q0-15.87 10.59-26.77Q541.18-756 557.09-756T584-745.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q573.41-681 557.46-681ZM564-103q-9.5 0-15.25-6T543-124q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm-6-712.5q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm160.46 611q-15.96 0-26.71-10.74Q681-225.97 681-241.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T745-268.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-161q-15.96 0-26.71-10.74Q681-386.97 681-402.84q0-15.87 10.59-26.77 10.59-10.89 26.5-10.89T745-429.76q11 10.73 11 26.6 0 15.87-10.79 26.77-10.8 10.89-26.75 10.89Zm0-154.5q-15.96 0-26.71-10.74Q681-541.47 681-557.34q0-15.87 10.59-26.77Q702.18-595 718.09-595T745-584.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q734.41-520 718.46-520Zm0-161q-15.96 0-26.71-10.74Q681-702.47 681-718.34q0-15.87 10.59-26.77Q702.18-756 718.09-756T745-745.26q11 10.73 11 26.6 0 15.87-10.79 26.77Q734.41-681 718.46-681ZM836.5-382q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Zm0-155q-9.5 0-15.25-6t-5.75-15q0-9 5.75-15t15.25-6q9 0 15 6t6 15q0 9-6 15t-15 6Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -4,8 +4,7 @@ import androidx.compose.foundation.contextMenuOpenDetector
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
@@ -40,3 +39,8 @@ onExternalDrag(enabled) {
|
||||
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() }
|
||||
|
||||
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = Modifier.pointerHoverIcon(PointerIcon.Hand)
|
||||
|
||||
actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier =
|
||||
this then Modifier
|
||||
.onPointerEvent(PointerEventType.Enter) { action(true) }
|
||||
.onPointerEvent(PointerEventType.Exit) { action(false) }
|
||||
|
||||
Reference in New Issue
Block a user