Reverting the spotlight on the Save button

This commit is contained in:
Vitor Pamplona
2024-06-10 19:51:52 -04:00
parent 6ed1f9d369
commit c4ecf85618
5 changed files with 237 additions and 139 deletions

View File

@@ -29,6 +29,8 @@ import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClientManager import com.vitorpamplona.amethyst.service.HttpClientManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -45,6 +47,33 @@ import java.io.File
import java.util.UUID import java.util.UUID
object ImageSaver { object ImageSaver {
fun saveImage(
videoUri: String?,
mimeType: String?,
localContext: Context,
onSuccess: () -> Any?,
onError: (Throwable) -> Any?,
) {
if (videoUri != null) {
if (!videoUri.startsWith("file")) {
saveImage(
context = localContext,
url = videoUri,
onSuccess = onSuccess,
onError = onError,
)
} else {
saveImage(
context = localContext,
localFile = videoUri.toUri().toFile(),
mimeType = mimeType,
onSuccess = onSuccess,
onError = onError,
)
}
}
}
/** /**
* Saves the image to the gallery. May require a storage permission. * Saves the image to the gallery. May require a storage permission.
* *

View File

@@ -20,10 +20,12 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.Manifest
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -44,6 +46,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -89,12 +92,15 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController import androidx.media3.session.MediaController
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.linc.audiowaveform.infiniteLinearGradient import com.linc.audiowaveform.infiniteLinearGradient
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.compose.produceCachedState
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.service.playback.PlaybackClientController import com.vitorpamplona.amethyst.service.playback.PlaybackClientController
import com.vitorpamplona.amethyst.ui.actions.ImageSaver
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
import com.vitorpamplona.amethyst.ui.note.LyricsIcon import com.vitorpamplona.amethyst.ui.note.LyricsIcon
import com.vitorpamplona.amethyst.ui.note.LyricsOffIcon import com.vitorpamplona.amethyst.ui.note.LyricsOffIcon
@@ -103,6 +109,7 @@ import com.vitorpamplona.amethyst.ui.note.MutedIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.Size110dp import com.vitorpamplona.amethyst.ui.theme.Size110dp
import com.vitorpamplona.amethyst.ui.theme.Size165dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
@@ -789,14 +796,13 @@ private fun RenderVideoPlayer(
keepPlaying.value = newKeepPlaying keepPlaying.value = newKeepPlaying
} }
AnimatedSaveAndShareButton( AnimatedSaveButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size110dp)) { context ->
videoUri = videoUri, saveImage(videoUri, mimeType, context, accountViewModel)
mimeType = mimeType, }
nostrUriCallback = nostrUriCallback,
controllerVisible = controllerVisible, AnimatedShareButton(controllerVisible, Modifier.align(Alignment.TopEnd).padding(end = Size165dp)) { popupExpanded, toggle ->
modifier = Modifier.align(Alignment.TopEnd).padding(end = Size110dp), ShareImageAction(popupExpanded, videoUri, nostrUriCallback, toggle)
accountViewModel = accountViewModel, }
)
} }
} }
@@ -1055,31 +1061,23 @@ private fun KeepPlayingButton(
} }
@Composable @Composable
fun AnimatedSaveAndShareButton( fun AnimatedSaveButton(
videoUri: String,
mimeType: String?,
nostrUriCallback: String?,
controllerVisible: State<Boolean>, controllerVisible: State<Boolean>,
modifier: Modifier, modifier: Modifier,
accountViewModel: AccountViewModel, onSaveClick: (localContext: Context) -> Unit,
) { ) {
AnimatedSaveAndShareButton(controllerVisible, modifier) { popupExpanded, toggle -> AnimatedVisibility(
ShareImageAction(popupExpanded, videoUri, nostrUriCallback, mimeType, toggle, accountViewModel) visible = controllerVisible.value,
modifier = modifier,
enter = remember { fadeIn() },
exit = remember { fadeOut() },
) {
SaveButton(onSaveClick)
} }
} }
@Composable @Composable
fun SaveAndShareButton( fun AnimatedShareButton(
content: BaseMediaContent,
accountViewModel: AccountViewModel,
) {
SaveAndShareButton { popupExpanded, toggle ->
ShareImageAction(popupExpanded, content, toggle, accountViewModel)
}
}
@Composable
fun AnimatedSaveAndShareButton(
controllerVisible: State<Boolean>, controllerVisible: State<Boolean>,
modifier: Modifier, modifier: Modifier,
innerAction: @Composable (MutableState<Boolean>, () -> Unit) -> Unit, innerAction: @Composable (MutableState<Boolean>, () -> Unit) -> Unit,
@@ -1090,12 +1088,12 @@ fun AnimatedSaveAndShareButton(
enter = remember { fadeIn() }, enter = remember { fadeIn() },
exit = remember { fadeOut() }, exit = remember { fadeOut() },
) { ) {
SaveAndShareButton(innerAction) ShareButton(innerAction)
} }
} }
@Composable @Composable
fun SaveAndShareButton(innerAction: @Composable (MutableState<Boolean>, () -> Unit) -> Unit) { fun ShareButton(innerAction: @Composable (MutableState<Boolean>, () -> Unit) -> Unit) {
Box(modifier = PinBottomIconSize) { Box(modifier = PinBottomIconSize) {
Box( Box(
Modifier.clip(CircleShape) Modifier.clip(CircleShape)
@@ -1122,3 +1120,64 @@ fun SaveAndShareButton(innerAction: @Composable (MutableState<Boolean>, () -> Un
} }
} }
} }
@kotlin.OptIn(ExperimentalPermissionsApi::class)
@Composable
fun SaveButton(onSaveClick: (localContext: Context) -> Unit) {
Box(modifier = PinBottomIconSize) {
Box(
Modifier.clip(CircleShape)
.fillMaxSize(0.6f)
.align(Alignment.Center)
.background(MaterialTheme.colorScheme.background),
)
val localContext = LocalContext.current
val writeStoragePermissionState =
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
if (isGranted) {
onSaveClick(localContext)
}
}
IconButton(
onClick = {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
writeStoragePermissionState.status.isGranted
) {
onSaveClick(localContext)
} else {
writeStoragePermissionState.launchPermissionRequest()
}
},
modifier = Size50Modifier,
) {
Icon(
imageVector = Icons.Default.Download,
modifier = Size20Modifier,
contentDescription = stringResource(R.string.save_to_gallery),
)
}
}
}
private fun saveImage(
videoUri: String?,
mimeType: String?,
localContext: Context,
accountViewModel: AccountViewModel,
) {
ImageSaver.saveImage(
videoUri = videoUri,
mimeType = mimeType,
localContext = localContext,
onSuccess = {
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
},
onError = {
accountViewModel.toast(R.string.failed_to_save_the_image, null, it)
},
)
}

View File

@@ -20,6 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.Manifest
import android.content.Context
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
@@ -29,11 +31,13 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -43,6 +47,7 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -60,6 +65,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@@ -67,14 +73,21 @@ import androidx.compose.ui.window.DialogProperties
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
import com.vitorpamplona.amethyst.commons.richtext.MediaPreloadedContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.ui.actions.ImageSaver
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size15dp
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.Size5dp
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -156,7 +169,7 @@ fun ZoomableImageDialog(
} }
@Composable @Composable
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalPermissionsApi::class)
private fun DialogContent( private fun DialogContent(
allImages: ImmutableList<BaseMediaContent>, allImages: ImmutableList<BaseMediaContent>,
imageUrl: BaseMediaContent, imageUrl: BaseMediaContent,
@@ -221,7 +234,7 @@ private fun DialogContent(
exit = remember { fadeOut() }, exit = remember { fadeOut() },
) { ) {
Row( Row(
modifier = Modifier.padding(Size10dp).statusBarsPadding().systemBarsPadding().fillMaxWidth(), modifier = Modifier.padding(horizontal = Size15dp, vertical = Size10dp).statusBarsPadding().systemBarsPadding().fillMaxWidth(),
horizontalArrangement = spacedBy(Size10dp), horizontalArrangement = spacedBy(Size10dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@@ -236,27 +249,92 @@ private fun DialogContent(
) )
} }
val popupExpanded = remember { mutableStateOf(false) } Spacer(modifier = Modifier.weight(1f))
OutlinedButton( allImages.getOrNull(pagerState.currentPage)?.let { myContent ->
onClick = { popupExpanded.value = true }, val popupExpanded = remember { mutableStateOf(false) }
contentPadding = PaddingValues(horizontal = Size5dp),
colors = ButtonDefaults.outlinedButtonColors().copy(containerColor = MaterialTheme.colorScheme.background),
) {
Icon(
imageVector = Icons.Default.Share,
modifier = Size20Modifier,
contentDescription = stringResource(R.string.share_or_save),
)
allImages.getOrNull(pagerState.currentPage)?.let { myContent -> OutlinedButton(
ShareImageAction(popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false }, accountViewModel = accountViewModel) onClick = { popupExpanded.value = true },
contentPadding = PaddingValues(horizontal = Size5dp),
colors = ButtonDefaults.outlinedButtonColors().copy(containerColor = MaterialTheme.colorScheme.background),
) {
Icon(
imageVector = Icons.Default.Share,
modifier = Size20Modifier,
contentDescription = stringResource(R.string.quick_action_share),
)
ShareImageAction(popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false })
}
val localContext = LocalContext.current
val writeStoragePermissionState =
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
if (isGranted) {
saveImage(myContent, localContext, accountViewModel)
}
}
OutlinedButton(
onClick = {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
writeStoragePermissionState.status.isGranted
) {
saveImage(myContent, localContext, accountViewModel)
} else {
writeStoragePermissionState.launchPermissionRequest()
}
},
contentPadding = PaddingValues(horizontal = Size5dp),
colors = ButtonDefaults.outlinedButtonColors().copy(containerColor = MaterialTheme.colorScheme.background),
) {
Icon(
imageVector = Icons.Default.Download,
modifier = Size20Modifier,
contentDescription = stringResource(R.string.save_to_gallery),
)
} }
} }
} }
} }
} }
private fun saveImage(
content: BaseMediaContent,
localContext: Context,
accountViewModel: AccountViewModel,
) {
if (content is MediaUrlContent) {
ImageSaver.saveImage(
content.url,
localContext,
onSuccess = {
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
},
onError = {
accountViewModel.toast(R.string.failed_to_save_the_image, null, it)
},
)
} else if (content is MediaPreloadedContent) {
content.localFile?.let {
ImageSaver.saveImage(
it,
content.mimeType,
localContext,
onSuccess = {
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
},
onError = {
accountViewModel.toast(R.string.failed_to_save_the_image, null, it)
},
)
}
}
}
@Composable @Composable
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
fun InlineCarrousel( fun InlineCarrousel(

View File

@@ -20,16 +20,15 @@
*/ */
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.os.Build
import android.util.Log import android.util.Log
import android.view.Window import android.view.Window
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
@@ -73,8 +72,6 @@ import coil.compose.AsyncImagePainter
import coil.compose.SubcomposeAsyncImage import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent import coil.compose.SubcomposeAsyncImageContent
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
@@ -85,7 +82,6 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.service.BlurHashRequester import com.vitorpamplona.amethyst.service.BlurHashRequester
import com.vitorpamplona.amethyst.ui.actions.ImageSaver
import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.actions.InformationDialog
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.BlankNote
@@ -142,12 +138,12 @@ fun ZoomableContentView(
mainImageModifier = mainImageModifier.clickable { dialogOpen = true } mainImageModifier = mainImageModifier.clickable { dialogOpen = true }
} }
val controllerVisible = remember { mutableStateOf(true) }
when (content) { when (content) {
is MediaUrlImage -> is MediaUrlImage ->
SensitivityWarning(content.contentWarning != null, accountViewModel) { SensitivityWarning(content.contentWarning != null, accountViewModel) {
UrlImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) TwoSecondController(content) { controllerVisible ->
UrlImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
}
} }
is MediaUrlVideo -> is MediaUrlVideo ->
SensitivityWarning(content.contentWarning != null, accountViewModel) { SensitivityWarning(content.contentWarning != null, accountViewModel) {
@@ -169,7 +165,9 @@ fun ZoomableContentView(
} }
} }
is MediaLocalImage -> is MediaLocalImage ->
LocalImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel) TwoSecondController(content) { controllerVisible ->
LocalImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
}
is MediaLocalVideo -> is MediaLocalVideo ->
content.localFile?.let { content.localFile?.let {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
@@ -194,6 +192,25 @@ fun ZoomableContentView(
} }
} }
@Composable
fun TwoSecondController(
content: BaseMediaContent,
inner: @Composable (controllerVisible: MutableState<Boolean>) -> Unit,
) {
val controllerVisible = remember { mutableStateOf(true) }
LaunchedEffect(content) {
launch(Dispatchers.Default) {
delay(2000)
withContext(Dispatchers.Main) {
controllerVisible.value = false
}
}
}
inner(controllerVisible)
}
@Composable @Composable
fun LocalImageView( fun LocalImageView(
content: MediaLocalImage, content: MediaLocalImage,
@@ -600,25 +617,20 @@ fun ShareImageAction(
popupExpanded: MutableState<Boolean>, popupExpanded: MutableState<Boolean>,
content: BaseMediaContent, content: BaseMediaContent,
onDismiss: () -> Unit, onDismiss: () -> Unit,
accountViewModel: AccountViewModel,
) { ) {
if (content is MediaUrlContent) { if (content is MediaUrlContent) {
ShareImageAction( ShareImageAction(
popupExpanded = popupExpanded, popupExpanded = popupExpanded,
videoUri = content.url, videoUri = content.url,
postNostrUri = content.uri, postNostrUri = content.uri,
mimeType = content.mimeType,
onDismiss = onDismiss, onDismiss = onDismiss,
accountViewModel = accountViewModel,
) )
} else if (content is MediaPreloadedContent) { } else if (content is MediaPreloadedContent) {
ShareImageAction( ShareImageAction(
popupExpanded = popupExpanded, popupExpanded = popupExpanded,
videoUri = content.localFile?.toUri().toString(), videoUri = content.localFile?.toUri().toString(),
postNostrUri = content.uri, postNostrUri = content.uri,
mimeType = content.mimeType,
onDismiss = onDismiss, onDismiss = onDismiss,
accountViewModel = accountViewModel,
) )
} }
} }
@@ -629,9 +641,7 @@ fun ShareImageAction(
popupExpanded: MutableState<Boolean>, popupExpanded: MutableState<Boolean>,
videoUri: String?, videoUri: String?,
postNostrUri: String?, postNostrUri: String?,
mimeType: String?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
accountViewModel: AccountViewModel,
) { ) {
DropdownMenu( DropdownMenu(
expanded = popupExpanded.value, expanded = popupExpanded.value,
@@ -658,85 +668,6 @@ fun ShareImageAction(
}, },
) )
} }
if (videoUri != null) {
if (!videoUri.startsWith("file")) {
val localContext = LocalContext.current
fun saveImage() {
ImageSaver.saveImage(
context = localContext,
url = videoUri,
onSuccess = {
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
},
onError = {
accountViewModel.toast(R.string.failed_to_save_the_image, null, it)
},
)
}
val writeStoragePermissionState =
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
if (isGranted) {
saveImage()
}
}
DropdownMenuItem(
text = { Text(stringResource(R.string.save_to_gallery)) },
onClick = {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
writeStoragePermissionState.status.isGranted
) {
saveImage()
} else {
writeStoragePermissionState.launchPermissionRequest()
}
onDismiss()
},
)
} else {
val localContext = LocalContext.current
fun saveImage() {
ImageSaver.saveImage(
context = localContext,
localFile = videoUri.toUri().toFile(),
mimeType = mimeType,
onSuccess = {
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
},
onError = {
accountViewModel.toast(R.string.failed_to_save_the_image, null, it)
},
)
}
val writeStoragePermissionState =
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
if (isGranted) {
saveImage()
}
}
DropdownMenuItem(
text = { Text(stringResource(R.string.save_to_gallery)) },
onClick = {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
writeStoragePermissionState.status.isGranted
) {
saveImage()
} else {
writeStoragePermissionState.launchPermissionRequest()
}
onDismiss()
},
)
}
}
} }
} }

View File

@@ -97,6 +97,7 @@ val Size40dp = 40.dp
val Size55dp = 55.dp val Size55dp = 55.dp
val Size75dp = 75.dp val Size75dp = 75.dp
val Size110dp = 110.dp val Size110dp = 110.dp
val Size165dp = 165.dp
val HalfEndPadding = Modifier.padding(end = 5.dp) val HalfEndPadding = Modifier.padding(end = 5.dp)
val HalfStartPadding = Modifier.padding(start = 5.dp) val HalfStartPadding = Modifier.padding(start = 5.dp)