@@ -24,10 +24,17 @@ import androidx.compose.ui.layout.ContentScale
2424import androidx.compose.ui.platform.LocalContext
2525import androidx.compose.ui.res.stringResource
2626import androidx.compose.ui.unit.dp
27+ import androidx.compose.ui.viewinterop.AndroidView
2728import androidx.lifecycle.compose.collectAsStateWithLifecycle
29+ import androidx.media3.common.Player
30+ import androidx.media3.exoplayer.ExoPlayer
31+ import androidx.media3.ui.PlayerView
2832import com.darkrockstudios.app.securecamera.ConfirmDeletePhotoDialog
2933import com.darkrockstudios.app.securecamera.R
34+ import com.darkrockstudios.app.securecamera.camera.MediaItem
35+ import com.darkrockstudios.app.securecamera.camera.MediaType
3036import com.darkrockstudios.app.securecamera.camera.PhotoDef
37+ import com.darkrockstudios.app.securecamera.camera.VideoDef
3138import com.darkrockstudios.app.securecamera.navigation.NavController
3239import com.darkrockstudios.app.securecamera.navigation.ObfuscatePhoto
3340import com.darkrockstudios.app.securecamera.ui.HandleUiEvents
@@ -36,25 +43,26 @@ import net.engawapg.lib.zoomable.rememberZoomState
3643import net.engawapg.lib.zoomable.zoomableWithScroll
3744import org.koin.androidx.compose.koinViewModel
3845import org.koin.core.parameter.parametersOf
46+ import androidx.media3.common.MediaItem as ExoMediaItem
3947
4048@SuppressLint(" UnusedBoxWithConstraintsScope" )
4149@OptIn(ExperimentalZoomableApi ::class )
4250@Composable
4351fun ViewPhotoContent (
44- initialPhoto : PhotoDef ,
52+ initialMedia : MediaItem ,
4553 navController : NavController ,
4654 modifier : Modifier = Modifier ,
4755 snackbarHostState : SnackbarHostState ,
4856 paddingValues : PaddingValues
4957) {
5058 val viewModel: ViewPhotoViewModel =
51- koinViewModel(key = initialPhoto.photoName ) { parametersOf(initialPhoto.photoName ) }
59+ koinViewModel(key = initialMedia.mediaName ) { parametersOf(initialMedia.mediaName ) }
5260 val context = LocalContext .current
5361
5462 val uiState by viewModel.uiState.collectAsStateWithLifecycle()
5563
56- LaunchedEffect (uiState.photoDeleted ) {
57- if (uiState.photoDeleted ) {
64+ LaunchedEffect (uiState.mediaDeleted ) {
65+ if (uiState.mediaDeleted ) {
5866 navController.navigateUp()
5967 }
6068 }
@@ -66,6 +74,7 @@ fun ViewPhotoContent(
6674 ) {
6775 ViewPhotoTopBar (
6876 navController = navController,
77+ mediaType = uiState.currentMediaType,
6978 onDeleteClick = {
7079 viewModel.showDeleteConfirmation()
7180 },
@@ -81,7 +90,7 @@ fun ViewPhotoContent(
8190 onShareClick = {
8291 viewModel.sharePhoto(context)
8392 },
84- showDecoyButton = uiState.hasPoisonPill,
93+ showDecoyButton = uiState.hasPoisonPill && uiState.currentMediaType == MediaType . PHOTO ,
8594 isDecoy = uiState.isDecoy,
8695 isDecoyLoading = uiState.isDecoyLoading,
8796 onDecoyClick = {
@@ -93,7 +102,7 @@ fun ViewPhotoContent(
93102 ConfirmDeletePhotoDialog (
94103 selectedCount = 1 ,
95104 onConfirm = {
96- viewModel.deleteCurrentPhoto ()
105+ viewModel.deleteCurrentMedia ()
97106 viewModel.hideDeleteConfirmation()
98107 },
99108 onDismiss = {
@@ -102,7 +111,7 @@ fun ViewPhotoContent(
102111 )
103112 }
104113
105- if (uiState.photos .isNotEmpty()) {
114+ if (uiState.mediaItems .isNotEmpty()) {
106115 val listState = remember { LazyListState (firstVisibleItemIndex = uiState.currentIndex) }
107116
108117 LaunchedEffect (listState) {
@@ -111,7 +120,7 @@ fun ViewPhotoContent(
111120 listState.firstVisibleItemScrollOffset
112121 }.collect { (idx, off) ->
113122 if (listState.firstVisibleItemIndex != uiState.currentIndex) {
114- viewModel.setCurrentPhotoIndex (listState.firstVisibleItemIndex)
123+ viewModel.setCurrentMediaIndex (listState.firstVisibleItemIndex)
115124 }
116125 }
117126 }
@@ -123,16 +132,26 @@ fun ViewPhotoContent(
123132 verticalAlignment = Alignment .CenterVertically ,
124133 horizontalArrangement = Arrangement .spacedBy(32 .dp),
125134 ) {
126- items(count = uiState.photos.size, key = { uiState.photos[it].photoName }) { index ->
127- val photo = uiState.photos[index]
128-
129- ViewPhoto (
130- modifier = Modifier
131- .fillParentMaxSize()
132- .padding(bottom = paddingValues.calculateBottomPadding()),
133- photo = photo,
134- viewModel = viewModel,
135- )
135+ items(count = uiState.mediaItems.size, key = { uiState.mediaItems[it].mediaName }) { index ->
136+ val mediaItem = uiState.mediaItems[index]
137+
138+ when (mediaItem) {
139+ is PhotoDef -> ViewPhoto (
140+ modifier = Modifier
141+ .fillParentMaxSize()
142+ .padding(bottom = paddingValues.calculateBottomPadding()),
143+ photo = mediaItem,
144+ viewModel = viewModel,
145+ )
146+
147+ is VideoDef -> ViewVideo (
148+ modifier = Modifier
149+ .fillParentMaxSize()
150+ .padding(bottom = paddingValues.calculateBottomPadding()),
151+ video = mediaItem,
152+ isCurrentItem = index == uiState.currentIndex,
153+ )
154+ }
136155 }
137156 }
138157 }
@@ -210,3 +229,57 @@ private fun ViewPhoto(
210229 }
211230 }
212231}
232+
233+ @androidx.annotation.OptIn (androidx.media3.common.util.UnstableApi ::class )
234+ @Composable
235+ private fun ViewVideo (
236+ modifier : Modifier ,
237+ video : VideoDef ,
238+ isCurrentItem : Boolean ,
239+ ) {
240+ val context = LocalContext .current
241+
242+ val exoPlayer = remember(video.videoName) {
243+ ExoPlayer .Builder (context).build().apply {
244+ val mediaItem = ExoMediaItem .fromUri(video.videoFile.toURI().toString())
245+ setMediaItem(mediaItem)
246+ prepare()
247+ repeatMode = Player .REPEAT_MODE_OFF
248+ }
249+ }
250+
251+ // Pause when not the current item
252+ LaunchedEffect (isCurrentItem) {
253+ if (! isCurrentItem) {
254+ exoPlayer.pause()
255+ }
256+ }
257+
258+ DisposableEffect (video.videoName) {
259+ onDispose {
260+ exoPlayer.release()
261+ }
262+ }
263+
264+ Box (
265+ modifier = modifier.clipToBounds(),
266+ contentAlignment = Alignment .Center
267+ ) {
268+ if (video.videoFile.exists()) {
269+ AndroidView (
270+ factory = { ctx ->
271+ PlayerView (ctx).apply {
272+ player = exoPlayer
273+ useController = true
274+ }
275+ },
276+ modifier = Modifier .fillMaxSize()
277+ )
278+ } else {
279+ Text (
280+ modifier = Modifier .align(alignment = Alignment .Center ),
281+ text = stringResource(id = R .string.video_not_found),
282+ )
283+ }
284+ }
285+ }
0 commit comments