restructures video and image views to avoid buttons getting on top of one another.
This commit is contained in:
@@ -304,6 +304,7 @@ fun EditPostView(
|
|||||||
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
|
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
|
||||||
VideoView(
|
VideoView(
|
||||||
myUrlPreview,
|
myUrlPreview,
|
||||||
|
mimeType = null,
|
||||||
roundedCorner = true,
|
roundedCorner = true,
|
||||||
isFiniteHeight = false,
|
isFiniteHeight = false,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ fun ImageVideoPost(
|
|||||||
postViewModel.galleryUri?.let {
|
postViewModel.galleryUri?.let {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = it.toString(),
|
videoUri = it.toString(),
|
||||||
|
mimeType = postViewModel.mediaType,
|
||||||
roundedCorner = false,
|
roundedCorner = false,
|
||||||
isFiniteHeight = false,
|
isFiniteHeight = false,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ fun NewPostView(
|
|||||||
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
|
} else if (RichTextParser.isVideoUrl(myUrlPreview)) {
|
||||||
VideoView(
|
VideoView(
|
||||||
myUrlPreview,
|
myUrlPreview,
|
||||||
|
mimeType = null,
|
||||||
roundedCorner = true,
|
roundedCorner = true,
|
||||||
isFiniteHeight = false,
|
isFiniteHeight = false,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
@@ -1737,7 +1738,7 @@ fun ImageVideoDescription(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
VideoView(uri.toString(), roundedCorner = true, isFiniteHeight = false, accountViewModel = accountViewModel)
|
VideoView(uri.toString(), roundedCorner = true, isFiniteHeight = false, mimeType = mediaType, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,16 +43,21 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
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.filled.Share
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableFloatState
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -72,7 +77,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -85,8 +90,10 @@ 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.linc.audiowaveform.infiniteLinearGradient
|
import com.linc.audiowaveform.infiniteLinearGradient
|
||||||
|
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.note.DownloadForOfflineIcon
|
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.LyricsIcon
|
import com.vitorpamplona.amethyst.ui.note.LyricsIcon
|
||||||
@@ -95,8 +102,11 @@ import com.vitorpamplona.amethyst.ui.note.MuteIcon
|
|||||||
import com.vitorpamplona.amethyst.ui.note.MutedIcon
|
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.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
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
@@ -117,6 +127,7 @@ public val DEFAULT_MUTED_SETTING = mutableStateOf(true)
|
|||||||
@Composable
|
@Composable
|
||||||
fun LoadThumbAndThenVideoView(
|
fun LoadThumbAndThenVideoView(
|
||||||
videoUri: String,
|
videoUri: String,
|
||||||
|
mimeType: String?,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumbUri: String,
|
thumbUri: String,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
@@ -134,11 +145,12 @@ fun LoadThumbAndThenVideoView(
|
|||||||
context,
|
context,
|
||||||
thumbUri,
|
thumbUri,
|
||||||
onReady = {
|
onReady = {
|
||||||
if (it != null) {
|
loadingFinished =
|
||||||
loadingFinished = Pair(true, it)
|
if (it != null) {
|
||||||
} else {
|
Pair(true, it)
|
||||||
loadingFinished = Pair(true, null)
|
} else {
|
||||||
}
|
Pair(true, null)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError = { loadingFinished = Pair(true, null) },
|
onError = { loadingFinished = Pair(true, null) },
|
||||||
)
|
)
|
||||||
@@ -148,6 +160,7 @@ fun LoadThumbAndThenVideoView(
|
|||||||
if (loadingFinished.second != null) {
|
if (loadingFinished.second != null) {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
|
mimeType = mimeType,
|
||||||
title = title,
|
title = title,
|
||||||
thumb = VideoThumb(loadingFinished.second),
|
thumb = VideoThumb(loadingFinished.second),
|
||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
@@ -161,6 +174,7 @@ fun LoadThumbAndThenVideoView(
|
|||||||
} else {
|
} else {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
|
mimeType = mimeType,
|
||||||
title = title,
|
title = title,
|
||||||
thumb = null,
|
thumb = null,
|
||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
@@ -178,11 +192,11 @@ fun LoadThumbAndThenVideoView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun VideoView(
|
fun VideoView(
|
||||||
videoUri: String,
|
videoUri: String,
|
||||||
|
mimeType: String?,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
roundedCorner: Boolean,
|
roundedCorner: Boolean,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
|
||||||
waveform: ImmutableList<Int>? = null,
|
waveform: ImmutableList<Int>? = null,
|
||||||
artworkUri: String? = null,
|
artworkUri: String? = null,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
@@ -218,21 +232,20 @@ fun VideoView(
|
|||||||
} else {
|
} else {
|
||||||
VideoViewInner(
|
VideoViewInner(
|
||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
|
mimeType = mimeType,
|
||||||
defaultToStart = defaultToStart,
|
defaultToStart = defaultToStart,
|
||||||
title = title,
|
title = title,
|
||||||
thumb = thumb,
|
thumb = thumb,
|
||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
isFiniteHeight = isFiniteHeight,
|
isFiniteHeight = isFiniteHeight,
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
artworkUri = artworkUri,
|
artworkUri = artworkUri,
|
||||||
authorName = authorName,
|
authorName = authorName,
|
||||||
dimensions = dimensions,
|
|
||||||
blurhash = blurhash,
|
|
||||||
nostrUriCallback = nostrUriCallback,
|
nostrUriCallback = nostrUriCallback,
|
||||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||||
onDialog = onDialog,
|
onDialog = onDialog,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,21 +285,20 @@ fun VideoView(
|
|||||||
} else {
|
} else {
|
||||||
VideoViewInner(
|
VideoViewInner(
|
||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
|
mimeType = mimeType,
|
||||||
defaultToStart = defaultToStart,
|
defaultToStart = defaultToStart,
|
||||||
title = title,
|
title = title,
|
||||||
thumb = thumb,
|
thumb = thumb,
|
||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
isFiniteHeight = isFiniteHeight,
|
isFiniteHeight = isFiniteHeight,
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
artworkUri = artworkUri,
|
artworkUri = artworkUri,
|
||||||
authorName = authorName,
|
authorName = authorName,
|
||||||
dimensions = dimensions,
|
|
||||||
blurhash = blurhash,
|
|
||||||
nostrUriCallback = nostrUriCallback,
|
nostrUriCallback = nostrUriCallback,
|
||||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||||
onDialog = onDialog,
|
onDialog = onDialog,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,21 +309,20 @@ fun VideoView(
|
|||||||
@OptIn(androidx.media3.common.util.UnstableApi::class)
|
@OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
fun VideoViewInner(
|
fun VideoViewInner(
|
||||||
videoUri: String,
|
videoUri: String,
|
||||||
|
mimeType: String?,
|
||||||
defaultToStart: Boolean = false,
|
defaultToStart: Boolean = false,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
roundedCorner: Boolean,
|
roundedCorner: Boolean,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
|
||||||
waveform: ImmutableList<Int>? = null,
|
waveform: ImmutableList<Int>? = null,
|
||||||
artworkUri: String? = null,
|
artworkUri: String? = null,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
dimensions: String? = null,
|
|
||||||
blurhash: String? = null,
|
|
||||||
nostrUriCallback: String? = null,
|
nostrUriCallback: String? = null,
|
||||||
automaticallyStartPlayback: State<Boolean>,
|
automaticallyStartPlayback: State<Boolean>,
|
||||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||||
onDialog: ((Boolean) -> Unit)? = null,
|
onDialog: ((Boolean) -> Unit)? = null,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
VideoPlayerActiveMutex(videoUri) { modifier, activeOnScreen ->
|
VideoPlayerActiveMutex(videoUri) { modifier, activeOnScreen ->
|
||||||
GetMediaItem(videoUri, title, artworkUri, authorName) { mediaItem ->
|
GetMediaItem(videoUri, title, artworkUri, authorName) { mediaItem ->
|
||||||
@@ -322,13 +333,13 @@ fun VideoViewInner(
|
|||||||
nostrUriCallback = nostrUriCallback,
|
nostrUriCallback = nostrUriCallback,
|
||||||
) { controller, keepPlaying ->
|
) { controller, keepPlaying ->
|
||||||
RenderVideoPlayer(
|
RenderVideoPlayer(
|
||||||
|
videoUri = videoUri,
|
||||||
|
mimeType = mimeType,
|
||||||
controller = controller,
|
controller = controller,
|
||||||
thumbData = thumb,
|
thumbData = thumb,
|
||||||
roundedCorner = roundedCorner,
|
roundedCorner = roundedCorner,
|
||||||
isFiniteHeight = isFiniteHeight,
|
isFiniteHeight = isFiniteHeight,
|
||||||
dimensions = dimensions,
|
nostrUriCallback = nostrUriCallback,
|
||||||
blurhash = blurhash,
|
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
keepPlaying = keepPlaying,
|
keepPlaying = keepPlaying,
|
||||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||||
@@ -336,6 +347,7 @@ fun VideoViewInner(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||||
onDialog = onDialog,
|
onDialog = onDialog,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,18 +365,18 @@ data class MediaItemData(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class MediaItemCache() : GenericBaseCache<MediaItemData, MediaItem>(20) {
|
class MediaItemCache() : GenericBaseCache<MediaItemData, MediaItem>(20) {
|
||||||
override suspend fun compute(data: MediaItemData): MediaItem? {
|
override suspend fun compute(key: MediaItemData): MediaItem {
|
||||||
return MediaItem.Builder()
|
return MediaItem.Builder()
|
||||||
.setMediaId(data.videoUri)
|
.setMediaId(key.videoUri)
|
||||||
.setUri(data.videoUri)
|
.setUri(key.videoUri)
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setArtist(data.authorName?.ifBlank { null })
|
.setArtist(key.authorName?.ifBlank { null })
|
||||||
.setTitle(data.title?.ifBlank { null } ?: data.videoUri)
|
.setTitle(key.title?.ifBlank { null } ?: key.videoUri)
|
||||||
.setArtworkUri(
|
.setArtworkUri(
|
||||||
try {
|
try {
|
||||||
if (data.artworkUri != null) {
|
if (key.artworkUri != null) {
|
||||||
Uri.parse(data.artworkUri)
|
Uri.parse(key.artworkUri)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -647,13 +659,13 @@ data class VideoThumb(
|
|||||||
@Composable
|
@Composable
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private fun RenderVideoPlayer(
|
private fun RenderVideoPlayer(
|
||||||
|
videoUri: String,
|
||||||
|
mimeType: String?,
|
||||||
controller: MediaController,
|
controller: MediaController,
|
||||||
thumbData: VideoThumb?,
|
thumbData: VideoThumb?,
|
||||||
roundedCorner: Boolean,
|
roundedCorner: Boolean,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
dimensions: String? = null,
|
nostrUriCallback: String?,
|
||||||
blurhash: String? = null,
|
|
||||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
|
||||||
waveform: ImmutableList<Int>? = null,
|
waveform: ImmutableList<Int>? = null,
|
||||||
keepPlaying: MutableState<Boolean>,
|
keepPlaying: MutableState<Boolean>,
|
||||||
automaticallyStartPlayback: State<Boolean>,
|
automaticallyStartPlayback: State<Boolean>,
|
||||||
@@ -661,6 +673,7 @@ private fun RenderVideoPlayer(
|
|||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||||
onDialog: ((Boolean) -> Unit)?,
|
onDialog: ((Boolean) -> Unit)?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen)
|
ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen)
|
||||||
|
|
||||||
@@ -724,6 +737,7 @@ private fun RenderVideoPlayer(
|
|||||||
MuteButton(
|
MuteButton(
|
||||||
controllerVisible,
|
controllerVisible,
|
||||||
startingMuteState,
|
startingMuteState,
|
||||||
|
Modifier.align(Alignment.TopEnd),
|
||||||
) { mute: Boolean ->
|
) { mute: Boolean ->
|
||||||
// makes the new setting the default for new creations.
|
// makes the new setting the default for new creations.
|
||||||
DEFAULT_MUTED_SETTING.value = mute
|
DEFAULT_MUTED_SETTING.value = mute
|
||||||
@@ -741,7 +755,7 @@ private fun RenderVideoPlayer(
|
|||||||
KeepPlayingButton(
|
KeepPlayingButton(
|
||||||
keepPlaying,
|
keepPlaying,
|
||||||
controllerVisible,
|
controllerVisible,
|
||||||
Modifier.align(Alignment.TopEnd),
|
Modifier.align(Alignment.TopEnd).padding(end = Size55dp),
|
||||||
) { newKeepPlaying: Boolean ->
|
) { newKeepPlaying: Boolean ->
|
||||||
// If something else is playing and the user marks this video to keep playing, stops the other
|
// If something else is playing and the user marks this video to keep playing, stops the other
|
||||||
// one.
|
// one.
|
||||||
@@ -759,6 +773,15 @@ private fun RenderVideoPlayer(
|
|||||||
|
|
||||||
keepPlaying.value = newKeepPlaying
|
keepPlaying.value = newKeepPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedSaveAndShareButton(
|
||||||
|
videoUri = videoUri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
nostrUriCallback = nostrUriCallback,
|
||||||
|
controllerVisible = controllerVisible,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd).padding(end = Size110dp),
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,7 +800,7 @@ fun Waveform(
|
|||||||
controller: MediaController,
|
controller: MediaController,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val waveformProgress = remember { mutableStateOf(0F) }
|
val waveformProgress = remember { mutableFloatStateOf(0F) }
|
||||||
|
|
||||||
DrawWaveform(waveform, waveformProgress, modifier)
|
DrawWaveform(waveform, waveformProgress, modifier)
|
||||||
|
|
||||||
@@ -791,7 +814,7 @@ fun Waveform(
|
|||||||
// doesn't consider the mutex because the screen can turn off if the video
|
// doesn't consider the mutex because the screen can turn off if the video
|
||||||
// being played in the mutex is not visible.
|
// being played in the mutex is not visible.
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
restartFlow.value += 1
|
restartFlow.intValue += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,28 +823,28 @@ fun Waveform(
|
|||||||
onDispose { controller.removeListener(listener) }
|
onDispose { controller.removeListener(listener) }
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = restartFlow.value) {
|
LaunchedEffect(key1 = restartFlow.intValue) {
|
||||||
pollCurrentDuration(controller).collect { value -> waveformProgress.value = value }
|
pollCurrentDuration(controller).collect { value -> waveformProgress.floatValue = value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawWaveform(
|
fun DrawWaveform(
|
||||||
waveform: ImmutableList<Int>,
|
waveform: ImmutableList<Int>,
|
||||||
waveformProgress: MutableState<Float>,
|
waveformProgress: MutableFloatState,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
AudioWaveformReadOnly(
|
AudioWaveformReadOnly(
|
||||||
modifier = modifier.padding(start = 10.dp, end = 10.dp),
|
modifier = modifier.padding(start = 10.dp, end = 10.dp),
|
||||||
amplitudes = waveform,
|
amplitudes = waveform,
|
||||||
progress = waveformProgress.value,
|
progress = waveformProgress.floatValue,
|
||||||
progressBrush =
|
progressBrush =
|
||||||
Brush.infiniteLinearGradient(
|
Brush.infiniteLinearGradient(
|
||||||
colors = listOf(Color(0xff2598cf), Color(0xff652d80)),
|
colors = listOf(Color(0xff2598cf), Color(0xff652d80)),
|
||||||
animation = tween(durationMillis = 6000, easing = LinearEasing),
|
animation = tween(durationMillis = 6000, easing = LinearEasing),
|
||||||
width = 128F,
|
width = 128F,
|
||||||
),
|
),
|
||||||
onProgressChange = { waveformProgress.value = it },
|
onProgressChange = { waveformProgress.floatValue = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,6 +949,7 @@ fun LayoutCoordinates.getDistanceToVertCenterIfVisible(view: View): Float? {
|
|||||||
private fun MuteButton(
|
private fun MuteButton(
|
||||||
controllerVisible: MutableState<Boolean>,
|
controllerVisible: MutableState<Boolean>,
|
||||||
startingMuteState: Boolean,
|
startingMuteState: Boolean,
|
||||||
|
modifier: Modifier,
|
||||||
toggle: (Boolean) -> Unit,
|
toggle: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val holdOn =
|
val holdOn =
|
||||||
@@ -946,6 +970,7 @@ private fun MuteButton(
|
|||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = holdOn.value || controllerVisible.value,
|
visible = holdOn.value || controllerVisible.value,
|
||||||
|
modifier = modifier,
|
||||||
enter = remember { fadeIn() },
|
enter = remember { fadeIn() },
|
||||||
exit = remember { fadeOut() },
|
exit = remember { fadeOut() },
|
||||||
) {
|
) {
|
||||||
@@ -978,14 +1003,14 @@ private fun MuteButton(
|
|||||||
private fun KeepPlayingButton(
|
private fun KeepPlayingButton(
|
||||||
keepPlayingStart: MutableState<Boolean>,
|
keepPlayingStart: MutableState<Boolean>,
|
||||||
controllerVisible: MutableState<Boolean>,
|
controllerVisible: MutableState<Boolean>,
|
||||||
alignment: Modifier,
|
modifier: Modifier,
|
||||||
toggle: (Boolean) -> Unit,
|
toggle: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val keepPlaying = remember(keepPlayingStart.value) { mutableStateOf(keepPlayingStart.value) }
|
val keepPlaying = remember(keepPlayingStart.value) { mutableStateOf(keepPlayingStart.value) }
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = controllerVisible.value,
|
visible = controllerVisible.value,
|
||||||
modifier = alignment,
|
modifier = modifier,
|
||||||
enter = remember { fadeIn() },
|
enter = remember { fadeIn() },
|
||||||
exit = remember { fadeOut() },
|
exit = remember { fadeOut() },
|
||||||
) {
|
) {
|
||||||
@@ -1013,3 +1038,72 @@ private fun KeepPlayingButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedSaveAndShareButton(
|
||||||
|
videoUri: String,
|
||||||
|
mimeType: String?,
|
||||||
|
nostrUriCallback: String?,
|
||||||
|
controllerVisible: State<Boolean>,
|
||||||
|
modifier: Modifier,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
AnimatedSaveAndShareButton(controllerVisible, modifier) { popupExpanded, toggle ->
|
||||||
|
ShareImageAction(popupExpanded, videoUri, nostrUriCallback, mimeType, toggle, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SaveAndShareButton(
|
||||||
|
content: BaseMediaContent,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
SaveAndShareButton { popupExpanded, toggle ->
|
||||||
|
ShareImageAction(popupExpanded, content, toggle, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedSaveAndShareButton(
|
||||||
|
controllerVisible: State<Boolean>,
|
||||||
|
modifier: Modifier,
|
||||||
|
innerAction: @Composable (MutableState<Boolean>, () -> Unit) -> Unit,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = controllerVisible.value,
|
||||||
|
modifier = modifier,
|
||||||
|
enter = remember { fadeIn() },
|
||||||
|
exit = remember { fadeOut() },
|
||||||
|
) {
|
||||||
|
SaveAndShareButton(innerAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SaveAndShareButton(innerAction: @Composable (MutableState<Boolean>, () -> Unit) -> Unit) {
|
||||||
|
Box(modifier = PinBottomIconSize) {
|
||||||
|
Box(
|
||||||
|
Modifier.clip(CircleShape)
|
||||||
|
.fillMaxSize(0.6f)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
)
|
||||||
|
|
||||||
|
val popupExpanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
popupExpanded.value = true
|
||||||
|
},
|
||||||
|
modifier = Size50Modifier,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Share,
|
||||||
|
modifier = Size20Modifier,
|
||||||
|
contentDescription = stringResource(R.string.share_or_save),
|
||||||
|
)
|
||||||
|
|
||||||
|
innerAction(popupExpanded) { popupExpanded.value = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.BaseMediaContent
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalImage
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
||||||
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.engawapg.lib.zoomable.rememberZoomState
|
||||||
|
import net.engawapg.lib.zoomable.zoomable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ZoomableImageDialog(
|
||||||
|
imageUrl: BaseMediaContent,
|
||||||
|
allImages: ImmutableList<BaseMediaContent> = listOf(imageUrl).toImmutableList(),
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val orientation = LocalConfiguration.current.orientation
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties =
|
||||||
|
DialogProperties(
|
||||||
|
usePlatformDefaultWidth = true,
|
||||||
|
decorFitsSystemWindows = false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val insets = ViewCompat.getRootWindowInsets(view)
|
||||||
|
|
||||||
|
val orientation = LocalConfiguration.current.orientation
|
||||||
|
println("This Log only exists to force orientation listener $orientation")
|
||||||
|
|
||||||
|
val activityWindow = getActivityWindow()
|
||||||
|
val dialogWindow = getDialogWindow()
|
||||||
|
val parentView = LocalView.current.parent as View
|
||||||
|
SideEffect {
|
||||||
|
if (activityWindow != null && dialogWindow != null) {
|
||||||
|
val attributes = WindowManager.LayoutParams()
|
||||||
|
attributes.copyFrom(activityWindow.attributes)
|
||||||
|
attributes.type = dialogWindow.attributes.type
|
||||||
|
dialogWindow.attributes = attributes
|
||||||
|
parentView.layoutParams =
|
||||||
|
FrameLayout.LayoutParams(
|
||||||
|
activityWindow.decorView.width,
|
||||||
|
activityWindow.decorView.height,
|
||||||
|
)
|
||||||
|
view.layoutParams =
|
||||||
|
FrameLayout.LayoutParams(
|
||||||
|
activityWindow.decorView.width,
|
||||||
|
activityWindow.decorView.height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(key1 = Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
view.windowInsetsController?.hide(
|
||||||
|
WindowInsets.Type.systemBars(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
view.windowInsetsController?.show(
|
||||||
|
WindowInsets.Type.systemBars(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||||
|
DialogContent(allImages, imageUrl, onDismiss, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private fun DialogContent(
|
||||||
|
allImages: ImmutableList<BaseMediaContent>,
|
||||||
|
imageUrl: BaseMediaContent,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val pagerState: PagerState = rememberPagerState { allImages.size }
|
||||||
|
val controllerVisible = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
|
||||||
|
launch {
|
||||||
|
val page = allImages.indexOf(imageUrl)
|
||||||
|
if (page > -1) {
|
||||||
|
pagerState.scrollToPage(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
delay(2000)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
controllerVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allImages.size > 1) {
|
||||||
|
SlidingCarousel(
|
||||||
|
pagerState = pagerState,
|
||||||
|
) { index ->
|
||||||
|
allImages.getOrNull(index)?.let {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
RenderImageOrVideo(
|
||||||
|
content = it,
|
||||||
|
roundedCorner = false,
|
||||||
|
isFiniteHeight = true,
|
||||||
|
controllerVisible = controllerVisible,
|
||||||
|
onControllerVisibilityChanged = { controllerVisible.value = it },
|
||||||
|
onToggleControllerVisibility = {
|
||||||
|
controllerVisible.value = !controllerVisible.value
|
||||||
|
},
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
RenderImageOrVideo(
|
||||||
|
content = imageUrl,
|
||||||
|
roundedCorner = false,
|
||||||
|
isFiniteHeight = true,
|
||||||
|
controllerVisible = controllerVisible,
|
||||||
|
onControllerVisibilityChanged = { controllerVisible.value = it },
|
||||||
|
onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value },
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = controllerVisible.value,
|
||||||
|
enter = remember { fadeIn() },
|
||||||
|
exit = remember { fadeOut() },
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(Size10dp).statusBarsPadding().systemBarsPadding().fillMaxWidth(),
|
||||||
|
horizontalArrangement = spacedBy(Size10dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
contentPadding = PaddingValues(horizontal = Size5dp),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors().copy(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
|
) {
|
||||||
|
ArrowBackIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
val popupExpanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
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.share_or_save),
|
||||||
|
)
|
||||||
|
|
||||||
|
allImages.getOrNull(pagerState.currentPage)?.let { myContent ->
|
||||||
|
ShareImageAction(popupExpanded = popupExpanded, myContent, onDismiss = { popupExpanded.value = false }, accountViewModel = accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
fun InlineCarrousel(
|
||||||
|
allImages: ImmutableList<String>,
|
||||||
|
imageUrl: String,
|
||||||
|
) {
|
||||||
|
val pagerState: PagerState = rememberPagerState { allImages.size }
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
|
||||||
|
launch {
|
||||||
|
val page = allImages.indexOf(imageUrl)
|
||||||
|
if (page > -1) {
|
||||||
|
pagerState.scrollToPage(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allImages.size > 1) {
|
||||||
|
SlidingCarousel(
|
||||||
|
pagerState = pagerState,
|
||||||
|
) { index ->
|
||||||
|
AsyncImage(
|
||||||
|
model = allImages[index],
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderImageOrVideo(
|
||||||
|
content: BaseMediaContent,
|
||||||
|
roundedCorner: Boolean,
|
||||||
|
isFiniteHeight: Boolean,
|
||||||
|
controllerVisible: MutableState<Boolean>,
|
||||||
|
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
||||||
|
onToggleControllerVisibility: (() -> Unit)? = null,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val automaticallyStartPlayback = remember { mutableStateOf<Boolean>(true) }
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
if (content is MediaUrlImage) {
|
||||||
|
val mainModifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.zoomable(
|
||||||
|
rememberZoomState(),
|
||||||
|
onTap = {
|
||||||
|
if (onToggleControllerVisibility != null) {
|
||||||
|
onToggleControllerVisibility()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
UrlImageView(
|
||||||
|
content = content,
|
||||||
|
mainImageModifier = mainModifier,
|
||||||
|
isFiniteHeight = isFiniteHeight,
|
||||||
|
controllerVisible = controllerVisible,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
alwayShowImage = true,
|
||||||
|
)
|
||||||
|
} else if (content is MediaUrlVideo) {
|
||||||
|
VideoViewInner(
|
||||||
|
videoUri = content.url,
|
||||||
|
mimeType = content.mimeType,
|
||||||
|
title = content.description,
|
||||||
|
artworkUri = content.artworkUri,
|
||||||
|
authorName = content.authorName,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
|
isFiniteHeight = isFiniteHeight,
|
||||||
|
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||||
|
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
} else if (content is MediaLocalImage) {
|
||||||
|
val mainModifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.zoomable(
|
||||||
|
rememberZoomState(),
|
||||||
|
onTap = {
|
||||||
|
if (onToggleControllerVisibility != null) {
|
||||||
|
onToggleControllerVisibility()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
LocalImageView(
|
||||||
|
content = content,
|
||||||
|
mainImageModifier = mainModifier,
|
||||||
|
isFiniteHeight = isFiniteHeight,
|
||||||
|
controllerVisible = controllerVisible,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
alwayShowImage = true,
|
||||||
|
)
|
||||||
|
} else if (content is MediaLocalVideo) {
|
||||||
|
content.localFile?.let {
|
||||||
|
VideoViewInner(
|
||||||
|
videoUri = it.toUri().toString(),
|
||||||
|
mimeType = content.mimeType,
|
||||||
|
title = content.description,
|
||||||
|
artworkUri = content.artworkUri,
|
||||||
|
authorName = content.authorName,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
|
isFiniteHeight = isFiniteHeight,
|
||||||
|
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||||
|
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
-369
@@ -20,53 +20,32 @@
|
|||||||
*/
|
*/
|
||||||
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.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
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.ExperimentalFoundationApi
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.pager.PagerState
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -76,7 +55,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
@@ -87,19 +65,18 @@ import androidx.compose.ui.text.PlaceholderVerticalAlign
|
|||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.isSpecified
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import coil.annotation.ExperimentalCoilApi
|
import coil.annotation.ExperimentalCoilApi
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.compose.AsyncImagePainter
|
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.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
|
||||||
@@ -110,24 +87,19 @@ 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.CloseButton
|
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.actions.SaveToGallery
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
import com.vitorpamplona.amethyst.ui.theme.Size75dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
import com.vitorpamplona.amethyst.ui.theme.hashVerifierMark
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
@@ -139,11 +111,8 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.engawapg.lib.zoomable.rememberZoomState
|
|
||||||
import net.engawapg.lib.zoomable.zoomable
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
fun ZoomableContentView(
|
fun ZoomableContentView(
|
||||||
content: BaseMediaContent,
|
content: BaseMediaContent,
|
||||||
images: ImmutableList<BaseMediaContent> = remember(content) { listOf(content).toImmutableList() },
|
images: ImmutableList<BaseMediaContent> = remember(content) { listOf(content).toImmutableList() },
|
||||||
@@ -154,13 +123,6 @@ fun ZoomableContentView(
|
|||||||
// store the dialog open or close state
|
// store the dialog open or close state
|
||||||
var dialogOpen by remember(content) { mutableStateOf(false) }
|
var dialogOpen by remember(content) { mutableStateOf(false) }
|
||||||
|
|
||||||
// store the dialog open or close state
|
|
||||||
val shareOpen = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (shareOpen.value) {
|
|
||||||
ShareImageAction(shareOpen, content) { shareOpen.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
var mainImageModifier =
|
var mainImageModifier =
|
||||||
if (roundedCorner) {
|
if (roundedCorner) {
|
||||||
MaterialTheme.colorScheme.imageModifier
|
MaterialTheme.colorScheme.imageModifier
|
||||||
@@ -170,30 +132,31 @@ fun ZoomableContentView(
|
|||||||
|
|
||||||
if (content is MediaUrlContent) {
|
if (content is MediaUrlContent) {
|
||||||
mainImageModifier =
|
mainImageModifier =
|
||||||
mainImageModifier.combinedClickable(
|
mainImageModifier.clickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { shareOpen.value = true },
|
|
||||||
)
|
)
|
||||||
} else if (content is MediaPreloadedContent) {
|
} else if (content is MediaPreloadedContent) {
|
||||||
mainImageModifier =
|
mainImageModifier =
|
||||||
mainImageModifier.combinedClickable(
|
mainImageModifier.clickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { shareOpen.value = true },
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
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, accountViewModel = accountViewModel)
|
UrlImageView(content, mainImageModifier, isFiniteHeight, controllerVisible, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
is MediaUrlVideo ->
|
is MediaUrlVideo ->
|
||||||
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
SensitivityWarning(content.contentWarning != null, accountViewModel) {
|
||||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = content.url,
|
videoUri = content.url,
|
||||||
|
mimeType = content.mimeType,
|
||||||
title = content.description,
|
title = content.description,
|
||||||
artworkUri = content.artworkUri,
|
artworkUri = content.artworkUri,
|
||||||
authorName = content.authorName,
|
authorName = content.authorName,
|
||||||
@@ -208,12 +171,13 @@ fun ZoomableContentView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MediaLocalImage ->
|
is MediaLocalImage ->
|
||||||
LocalImageView(content, mainImageModifier, isFiniteHeight, accountViewModel = accountViewModel)
|
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) {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = it.toUri().toString(),
|
videoUri = it.toUri().toString(),
|
||||||
|
mimeType = content.mimeType,
|
||||||
title = content.description,
|
title = content.description,
|
||||||
artworkUri = content.artworkUri,
|
artworkUri = content.artworkUri,
|
||||||
authorName = content.authorName,
|
authorName = content.authorName,
|
||||||
@@ -233,11 +197,11 @@ fun ZoomableContentView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LocalImageView(
|
fun LocalImageView(
|
||||||
content: MediaLocalImage,
|
content: MediaLocalImage,
|
||||||
mainImageModifier: Modifier,
|
mainImageModifier: Modifier,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
controllerVisible: MutableState<Boolean>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
alwayShowImage: Boolean = false,
|
alwayShowImage: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -294,15 +258,15 @@ private fun LocalImageView(
|
|||||||
SubcomposeAsyncImageContent()
|
SubcomposeAsyncImageContent()
|
||||||
|
|
||||||
content.isVerified?.let {
|
content.isVerified?.let {
|
||||||
val verifierModifier =
|
AnimatedVisibility(
|
||||||
if (topPaddingForControllers.isSpecified) {
|
visible = controllerVisible.value,
|
||||||
Modifier.align(Alignment.TopEnd).padding(top = topPaddingForControllers)
|
modifier = Modifier,
|
||||||
} else {
|
enter = remember { fadeIn() },
|
||||||
Modifier.align(Alignment.TopEnd)
|
exit = remember { fadeOut() },
|
||||||
|
) {
|
||||||
|
Box(Modifier.align(Alignment.TopEnd), contentAlignment = Alignment.TopEnd) {
|
||||||
|
HashVerificationSymbol(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(verifierModifier, contentAlignment = Alignment.TopEnd) {
|
|
||||||
HashVerificationSymbol(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,11 +300,11 @@ private fun LocalImageView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun UrlImageView(
|
fun UrlImageView(
|
||||||
content: MediaUrlImage,
|
content: MediaUrlImage,
|
||||||
mainImageModifier: Modifier,
|
mainImageModifier: Modifier,
|
||||||
isFiniteHeight: Boolean,
|
isFiniteHeight: Boolean,
|
||||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
controllerVisible: MutableState<Boolean>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
alwayShowImage: Boolean = false,
|
alwayShowImage: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -361,7 +325,7 @@ private fun UrlImageView(
|
|||||||
model = content.url,
|
model = content.url,
|
||||||
contentDescription = content.description,
|
contentDescription = content.description,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
modifier = mainImageModifier,
|
modifier = mainImageModifier.border(10.dp, Color.Red),
|
||||||
) {
|
) {
|
||||||
when (painter.state) {
|
when (painter.state) {
|
||||||
is AsyncImagePainter.State.Loading,
|
is AsyncImagePainter.State.Loading,
|
||||||
@@ -392,15 +356,15 @@ private fun UrlImageView(
|
|||||||
is AsyncImagePainter.State.Success -> {
|
is AsyncImagePainter.State.Success -> {
|
||||||
SubcomposeAsyncImageContent()
|
SubcomposeAsyncImageContent()
|
||||||
|
|
||||||
val verifierModifier =
|
AnimatedVisibility(
|
||||||
if (topPaddingForControllers.isSpecified) {
|
visible = controllerVisible.value,
|
||||||
Modifier.align(Alignment.TopEnd).padding(top = topPaddingForControllers)
|
modifier = Modifier.align(Alignment.TopEnd),
|
||||||
} else {
|
enter = remember { fadeIn() },
|
||||||
Modifier.align(Alignment.TopEnd)
|
exit = remember { fadeOut() },
|
||||||
|
) {
|
||||||
|
Box(Modifier.align(Alignment.TopEnd), contentAlignment = Alignment.TopEnd) {
|
||||||
|
ShowHash(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(verifierModifier, contentAlignment = Alignment.TopEnd) {
|
|
||||||
ShowHash(content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -634,217 +598,42 @@ fun DisplayBlurHash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ZoomableImageDialog(
|
fun ShareImageAction(
|
||||||
imageUrl: BaseMediaContent,
|
|
||||||
allImages: ImmutableList<BaseMediaContent> = listOf(imageUrl).toImmutableList(),
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
val orientation = LocalConfiguration.current.orientation
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties =
|
|
||||||
DialogProperties(
|
|
||||||
usePlatformDefaultWidth = true,
|
|
||||||
decorFitsSystemWindows = false,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
val view = LocalView.current
|
|
||||||
val insets = ViewCompat.getRootWindowInsets(view)
|
|
||||||
|
|
||||||
val orientation = LocalConfiguration.current.orientation
|
|
||||||
println("This Log only exists to force orientation listener $orientation")
|
|
||||||
|
|
||||||
val activityWindow = getActivityWindow()
|
|
||||||
val dialogWindow = getDialogWindow()
|
|
||||||
val parentView = LocalView.current.parent as View
|
|
||||||
SideEffect {
|
|
||||||
if (activityWindow != null && dialogWindow != null) {
|
|
||||||
val attributes = WindowManager.LayoutParams()
|
|
||||||
attributes.copyFrom(activityWindow.attributes)
|
|
||||||
attributes.type = dialogWindow.attributes.type
|
|
||||||
dialogWindow.attributes = attributes
|
|
||||||
parentView.layoutParams =
|
|
||||||
FrameLayout.LayoutParams(activityWindow.decorView.width, activityWindow.decorView.height)
|
|
||||||
view.layoutParams =
|
|
||||||
FrameLayout.LayoutParams(activityWindow.decorView.width, activityWindow.decorView.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DisposableEffect(key1 = Unit) {
|
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
|
||||||
view.windowInsetsController?.hide(
|
|
||||||
android.view.WindowInsets.Type.systemBars(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onDispose {
|
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
|
||||||
view.windowInsetsController?.show(
|
|
||||||
android.view.WindowInsets.Type.systemBars(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
|
||||||
DialogContent(allImages, imageUrl, onDismiss, accountViewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
private fun DialogContent(
|
|
||||||
allImages: ImmutableList<BaseMediaContent>,
|
|
||||||
imageUrl: BaseMediaContent,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
val pagerState: PagerState = rememberPagerState { allImages.size }
|
|
||||||
val controllerVisible = remember { mutableStateOf(false) }
|
|
||||||
val holdOn = remember { mutableStateOf<Boolean>(true) }
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
|
|
||||||
launch {
|
|
||||||
val page = allImages.indexOf(imageUrl)
|
|
||||||
if (page > -1) {
|
|
||||||
pagerState.scrollToPage(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch(Dispatchers.Default) {
|
|
||||||
delay(2000)
|
|
||||||
holdOn.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allImages.size > 1) {
|
|
||||||
SlidingCarousel(
|
|
||||||
pagerState = pagerState,
|
|
||||||
) { index ->
|
|
||||||
allImages.getOrNull(index)?.let {
|
|
||||||
RenderImageOrVideo(
|
|
||||||
content = it,
|
|
||||||
roundedCorner = false,
|
|
||||||
isFiniteHeight = true,
|
|
||||||
topPaddingForControllers = Size55dp,
|
|
||||||
onControllerVisibilityChanged = { controllerVisible.value = it },
|
|
||||||
onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value },
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RenderImageOrVideo(
|
|
||||||
content = imageUrl,
|
|
||||||
roundedCorner = false,
|
|
||||||
isFiniteHeight = true,
|
|
||||||
topPaddingForControllers = Size55dp,
|
|
||||||
onControllerVisibilityChanged = { controllerVisible.value = it },
|
|
||||||
onToggleControllerVisibility = { controllerVisible.value = !controllerVisible.value },
|
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = holdOn.value || controllerVisible.value,
|
|
||||||
enter = remember { fadeIn() },
|
|
||||||
exit = remember { fadeOut() },
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(10.dp)
|
|
||||||
.statusBarsPadding()
|
|
||||||
.systemBarsPadding()
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
CloseButton(onPress = onDismiss)
|
|
||||||
|
|
||||||
allImages.getOrNull(pagerState.currentPage)?.let { myContent ->
|
|
||||||
if (myContent is MediaUrlContent) {
|
|
||||||
Row {
|
|
||||||
CopyToClipboard(content = myContent)
|
|
||||||
Spacer(modifier = StdHorzSpacer)
|
|
||||||
SaveToGallery(url = myContent.url)
|
|
||||||
}
|
|
||||||
} else if (myContent is MediaLocalImage && myContent.localFileExists()) {
|
|
||||||
SaveToGallery(
|
|
||||||
localFile = myContent.localFile!!,
|
|
||||||
mimeType = myContent.mimeType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
fun InlineCarrousel(
|
|
||||||
allImages: ImmutableList<String>,
|
|
||||||
imageUrl: String,
|
|
||||||
) {
|
|
||||||
val pagerState: PagerState = rememberPagerState { allImages.size }
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
|
|
||||||
launch {
|
|
||||||
val page = allImages.indexOf(imageUrl)
|
|
||||||
if (page > -1) {
|
|
||||||
pagerState.scrollToPage(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allImages.size > 1) {
|
|
||||||
SlidingCarousel(
|
|
||||||
pagerState = pagerState,
|
|
||||||
) { index ->
|
|
||||||
AsyncImage(
|
|
||||||
model = allImages[index],
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AsyncImage(
|
|
||||||
model = imageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.FillWidth,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CopyToClipboard(content: BaseMediaContent) {
|
|
||||||
val popupExpanded = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
OutlinedButton(
|
|
||||||
modifier = Modifier.padding(horizontal = Size5dp),
|
|
||||||
onClick = { popupExpanded.value = true },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Share,
|
|
||||||
modifier = Size20Modifier,
|
|
||||||
contentDescription = stringResource(R.string.copy_url_to_clipboard),
|
|
||||||
)
|
|
||||||
|
|
||||||
ShareImageAction(popupExpanded, content) { popupExpanded.value = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ShareImageAction(
|
|
||||||
popupExpanded: MutableState<Boolean>,
|
popupExpanded: MutableState<Boolean>,
|
||||||
content: BaseMediaContent,
|
content: BaseMediaContent,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
if (content is MediaUrlContent) {
|
||||||
|
ShareImageAction(
|
||||||
|
popupExpanded = popupExpanded,
|
||||||
|
videoUri = content.url,
|
||||||
|
postNostrUri = content.uri,
|
||||||
|
mimeType = content.mimeType,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
} else if (content is MediaPreloadedContent) {
|
||||||
|
ShareImageAction(
|
||||||
|
popupExpanded = popupExpanded,
|
||||||
|
videoUri = content.localFile?.toUri().toString(),
|
||||||
|
postNostrUri = content.uri,
|
||||||
|
mimeType = content.mimeType,
|
||||||
|
onDismiss = onDismiss,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ShareImageAction(
|
||||||
|
popupExpanded: MutableState<Boolean>,
|
||||||
|
videoUri: String?,
|
||||||
|
postNostrUri: String?,
|
||||||
|
mimeType: String?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = popupExpanded.value,
|
expanded = popupExpanded.value,
|
||||||
@@ -852,118 +641,101 @@ private fun ShareImageAction(
|
|||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
if (content is MediaUrlContent) {
|
if (videoUri != null && !videoUri.startsWith("file")) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.copy_url_to_clipboard)) },
|
text = { Text(stringResource(R.string.copy_url_to_clipboard)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.setText(AnnotatedString(content.url))
|
clipboardManager.setText(AnnotatedString(videoUri))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
content.uri?.let {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(R.string.copy_the_note_id_to_the_clipboard)) },
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.setText(AnnotatedString(it))
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content is MediaPreloadedContent) {
|
postNostrUri?.let {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.copy_the_note_id_to_the_clipboard)) },
|
text = { Text(stringResource(R.string.copy_the_note_id_to_the_clipboard)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
clipboardManager.setText(AnnotatedString(content.uri))
|
clipboardManager.setText(AnnotatedString(it))
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
if (videoUri != null) {
|
||||||
private fun RenderImageOrVideo(
|
if (!videoUri.startsWith("file")) {
|
||||||
content: BaseMediaContent,
|
val localContext = LocalContext.current
|
||||||
roundedCorner: Boolean,
|
|
||||||
isFiniteHeight: Boolean,
|
|
||||||
topPaddingForControllers: Dp = Dp.Unspecified,
|
|
||||||
onControllerVisibilityChanged: ((Boolean) -> Unit)? = null,
|
|
||||||
onToggleControllerVisibility: (() -> Unit)? = null,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
|
||||||
val automaticallyStartPlayback = remember { mutableStateOf<Boolean>(true) }
|
|
||||||
|
|
||||||
if (content is MediaUrlImage) {
|
fun saveImage() {
|
||||||
val mainModifier =
|
ImageSaver.saveImage(
|
||||||
Modifier
|
context = localContext,
|
||||||
.fillMaxSize()
|
url = videoUri,
|
||||||
.zoomable(
|
onSuccess = {
|
||||||
rememberZoomState(),
|
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
|
||||||
onTap = {
|
},
|
||||||
if (onToggleControllerVisibility != null) {
|
onError = {
|
||||||
onToggleControllerVisibility()
|
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
|
||||||
|
|
||||||
UrlImageView(
|
fun saveImage() {
|
||||||
content = content,
|
ImageSaver.saveImage(
|
||||||
mainImageModifier = mainModifier,
|
context = localContext,
|
||||||
isFiniteHeight = isFiniteHeight,
|
localFile = videoUri.toUri().toFile(),
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
mimeType = mimeType,
|
||||||
accountViewModel = accountViewModel,
|
onSuccess = {
|
||||||
alwayShowImage = true,
|
accountViewModel.toast(R.string.image_saved_to_the_gallery, R.string.image_saved_to_the_gallery)
|
||||||
)
|
},
|
||||||
} else if (content is MediaUrlVideo) {
|
onError = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize(1f)) {
|
accountViewModel.toast(R.string.failed_to_save_the_image, null, it)
|
||||||
VideoViewInner(
|
},
|
||||||
videoUri = content.url,
|
)
|
||||||
title = content.description,
|
}
|
||||||
artworkUri = content.artworkUri,
|
|
||||||
authorName = content.authorName,
|
val writeStoragePermissionState =
|
||||||
roundedCorner = roundedCorner,
|
rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) { isGranted ->
|
||||||
isFiniteHeight = isFiniteHeight,
|
if (isGranted) {
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
saveImage()
|
||||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
|
||||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (content is MediaLocalImage) {
|
|
||||||
val mainModifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.zoomable(
|
|
||||||
rememberZoomState(),
|
|
||||||
onTap = {
|
|
||||||
if (onToggleControllerVisibility != null) {
|
|
||||||
onToggleControllerVisibility()
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
LocalImageView(
|
DropdownMenuItem(
|
||||||
content = content,
|
text = { Text(stringResource(R.string.save_to_gallery)) },
|
||||||
mainImageModifier = mainModifier,
|
onClick = {
|
||||||
isFiniteHeight = isFiniteHeight,
|
if (
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ||
|
||||||
accountViewModel = accountViewModel,
|
writeStoragePermissionState.status.isGranted
|
||||||
alwayShowImage = true,
|
) {
|
||||||
)
|
saveImage()
|
||||||
} else if (content is MediaLocalVideo) {
|
} else {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize(1f)) {
|
writeStoragePermissionState.launchPermissionRequest()
|
||||||
content.localFile?.let {
|
}
|
||||||
VideoViewInner(
|
onDismiss()
|
||||||
videoUri = it.toUri().toString(),
|
},
|
||||||
title = content.description,
|
|
||||||
artworkUri = content.artworkUri,
|
|
||||||
authorName = content.authorName,
|
|
||||||
roundedCorner = roundedCorner,
|
|
||||||
isFiniteHeight = isFiniteHeight,
|
|
||||||
topPaddingForControllers = topPaddingForControllers,
|
|
||||||
automaticallyStartPlayback = automaticallyStartPlayback,
|
|
||||||
onControllerVisibilityChanged = onControllerVisibilityChanged,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ fun AudioTrackHeader(
|
|||||||
cover?.let { cover ->
|
cover?.let { cover ->
|
||||||
LoadThumbAndThenVideoView(
|
LoadThumbAndThenVideoView(
|
||||||
videoUri = media,
|
videoUri = media,
|
||||||
|
mimeType = null,
|
||||||
title = noteEvent.subject(),
|
title = noteEvent.subject(),
|
||||||
thumbUri = cover,
|
thumbUri = cover,
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
@@ -153,6 +154,7 @@ fun AudioTrackHeader(
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = media,
|
videoUri = media,
|
||||||
|
mimeType = null,
|
||||||
title = noteEvent.subject(),
|
title = noteEvent.subject(),
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
roundedCorner = true,
|
roundedCorner = true,
|
||||||
@@ -202,6 +204,7 @@ fun AudioHeader(
|
|||||||
) {
|
) {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = media,
|
videoUri = media,
|
||||||
|
mimeType = null,
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
title = noteEvent.subject(),
|
title = noteEvent.subject(),
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ fun FileHeaderDisplay(
|
|||||||
val description = event.content.ifEmpty { null } ?: event.alt()
|
val description = event.content.ifEmpty { null } ?: event.alt()
|
||||||
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
||||||
val uri = note.toNostrUri()
|
val uri = note.toNostrUri()
|
||||||
|
val mimeType = event.mimeType()
|
||||||
|
|
||||||
mutableStateOf<BaseMediaContent>(
|
mutableStateOf<BaseMediaContent>(
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@@ -62,6 +63,7 @@ fun FileHeaderDisplay(
|
|||||||
blurhash = blurHash,
|
blurhash = blurHash,
|
||||||
dim = dimensions,
|
dim = dimensions,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MediaUrlVideo(
|
MediaUrlVideo(
|
||||||
@@ -72,6 +74,7 @@ fun FileHeaderDisplay(
|
|||||||
dim = dimensions,
|
dim = dimensions,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ fun RenderLiveActivityEventInner(
|
|||||||
) {
|
) {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = media,
|
videoUri = media,
|
||||||
|
mimeType = null,
|
||||||
title = subject,
|
title = subject,
|
||||||
artworkUri = cover,
|
artworkUri = cover,
|
||||||
authorName = baseNote.author?.toBestDisplayName(),
|
authorName = baseNote.author?.toBestDisplayName(),
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ fun VideoDisplay(
|
|||||||
val description = event.content.ifBlank { null } ?: event.alt()
|
val description = event.content.ifBlank { null } ?: event.alt()
|
||||||
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
||||||
val uri = note.toNostrUri()
|
val uri = note.toNostrUri()
|
||||||
|
val mimeType = event.mimeType()
|
||||||
|
|
||||||
mutableStateOf<BaseMediaContent>(
|
mutableStateOf<BaseMediaContent>(
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@@ -97,6 +98,7 @@ fun VideoDisplay(
|
|||||||
blurhash = blurHash,
|
blurhash = blurHash,
|
||||||
dim = dimensions,
|
dim = dimensions,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MediaUrlVideo(
|
MediaUrlVideo(
|
||||||
@@ -107,6 +109,7 @@ fun VideoDisplay(
|
|||||||
uri = uri,
|
uri = uri,
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
artworkUri = event.thumb() ?: event.image(),
|
artworkUri = event.thumb() ?: event.image(),
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ fun JustVideoDisplay(
|
|||||||
val description = event.content.ifEmpty { null } ?: event.alt()
|
val description = event.content.ifEmpty { null } ?: event.alt()
|
||||||
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
val isImage = event.mimeType()?.startsWith("image/") == true || RichTextParser.isImageUrl(fullUrl)
|
||||||
val uri = note.toNostrUri()
|
val uri = note.toNostrUri()
|
||||||
|
val mimeType = event.mimeType()
|
||||||
|
|
||||||
mutableStateOf<BaseMediaContent>(
|
mutableStateOf<BaseMediaContent>(
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@@ -62,6 +63,7 @@ fun JustVideoDisplay(
|
|||||||
blurhash = blurHash,
|
blurhash = blurHash,
|
||||||
dim = dimensions,
|
dim = dimensions,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MediaUrlVideo(
|
MediaUrlVideo(
|
||||||
@@ -72,6 +74,7 @@ fun JustVideoDisplay(
|
|||||||
dim = dimensions,
|
dim = dimensions,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
|
mimeType = mimeType,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ import kotlin.time.measureTimedValue
|
|||||||
val params: Array<out String>? = null,
|
val params: Array<out String>? = null,
|
||||||
) : ToastMsg()
|
) : ToastMsg()
|
||||||
|
|
||||||
|
@Immutable class ThrowableToastMsg(val titleResId: Int, val msg: String? = null, val throwable: Throwable) : ToastMsg()
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao {
|
class AccountViewModel(val account: Account, val settings: SettingsState) : ViewModel(), Dao {
|
||||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||||
@@ -163,6 +165,14 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||||||
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId)) }
|
viewModelScope.launch { toasts.emit(ResourceToastMsg(titleResId, resourceId)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toast(
|
||||||
|
titleResId: Int,
|
||||||
|
message: String?,
|
||||||
|
throwable: Throwable,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch { toasts.emit(ThrowableToastMsg(titleResId, message, throwable)) }
|
||||||
|
}
|
||||||
|
|
||||||
fun toast(
|
fun toast(
|
||||||
titleResId: Int,
|
titleResId: Int,
|
||||||
resourceId: Int,
|
resourceId: Int,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ val Size35dp = 35.dp
|
|||||||
val Size40dp = 40.dp
|
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 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)
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
<string name="website_url">Website URL</string>
|
<string name="website_url">Website URL</string>
|
||||||
<string name="ln_address">LN Address</string>
|
<string name="ln_address">LN Address</string>
|
||||||
<string name="ln_url_outdated">LN URL (outdated)</string>
|
<string name="ln_url_outdated">LN URL (outdated)</string>
|
||||||
|
<string name="save_to_gallery">Save to Gallery</string>
|
||||||
<string name="image_saved_to_the_gallery">Image saved to the gallery</string>
|
<string name="image_saved_to_the_gallery">Image saved to the gallery</string>
|
||||||
<string name="failed_to_save_the_image">Failed to save the image</string>
|
<string name="failed_to_save_the_image">Failed to save the image</string>
|
||||||
<string name="upload_image">Upload Image</string>
|
<string name="upload_image">Upload Image</string>
|
||||||
@@ -580,6 +581,7 @@
|
|||||||
|
|
||||||
<string name="copy_to_clipboard">Copy to clipboard</string>
|
<string name="copy_to_clipboard">Copy to clipboard</string>
|
||||||
<string name="copy_npub_to_clipboard">Copy npub to clipboard</string>
|
<string name="copy_npub_to_clipboard">Copy npub to clipboard</string>
|
||||||
|
<string name="share_or_save">Share or Save</string>
|
||||||
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
|
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
|
||||||
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
|
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -38,6 +38,7 @@ abstract class MediaUrlContent(
|
|||||||
dim: String? = null,
|
dim: String? = null,
|
||||||
blurhash: String? = null,
|
blurhash: String? = null,
|
||||||
val uri: String? = null,
|
val uri: String? = null,
|
||||||
|
val mimeType: String? = null,
|
||||||
) : BaseMediaContent(description, dim, blurhash)
|
) : BaseMediaContent(description, dim, blurhash)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -49,7 +50,8 @@ class MediaUrlImage(
|
|||||||
dim: String? = null,
|
dim: String? = null,
|
||||||
uri: String? = null,
|
uri: String? = null,
|
||||||
val contentWarning: String? = null,
|
val contentWarning: String? = null,
|
||||||
) : MediaUrlContent(url, description, hash, dim, blurhash, uri)
|
mimeType: String? = null,
|
||||||
|
) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
class MediaUrlVideo(
|
class MediaUrlVideo(
|
||||||
@@ -62,7 +64,8 @@ class MediaUrlVideo(
|
|||||||
val authorName: String? = null,
|
val authorName: String? = null,
|
||||||
blurhash: String? = null,
|
blurhash: String? = null,
|
||||||
val contentWarning: String? = null,
|
val contentWarning: String? = null,
|
||||||
) : MediaUrlContent(url, description, hash, dim, blurhash, uri)
|
mimeType: String? = null,
|
||||||
|
) : MediaUrlContent(url, description, hash, dim, blurhash, uri, mimeType)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
abstract class MediaPreloadedContent(
|
abstract class MediaPreloadedContent(
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class RichTextParser() {
|
|||||||
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
||||||
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
||||||
uri = callbackUri,
|
uri = callbackUri,
|
||||||
|
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
|
||||||
)
|
)
|
||||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
val frags = Nip54InlineMetadata().parse(fullUrl)
|
val frags = Nip54InlineMetadata().parse(fullUrl)
|
||||||
@@ -73,6 +74,7 @@ class RichTextParser() {
|
|||||||
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
dim = frags[FileHeaderEvent.DIMENSION] ?: tags[FileHeaderEvent.DIMENSION],
|
||||||
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
contentWarning = frags["content-warning"] ?: tags["content-warning"],
|
||||||
uri = callbackUri,
|
uri = callbackUri,
|
||||||
|
mimeType = frags[FileHeaderEvent.MIME_TYPE] ?: tags[FileHeaderEvent.MIME_TYPE],
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
Reference in New Issue
Block a user