mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-04 20:42:48 +02:00
Reverting the spotlight on the Save button
This commit is contained in:
@@ -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.
|
||||||
*
|
*
|
||||||
|
@@ -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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -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(
|
||||||
|
@@ -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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user