@@ -3,6 +3,7 @@ package dev.dimension.flare.ui.screen.media
33import android.Manifest
44import android.content.ContentValues
55import android.content.Context
6+ import android.content.Intent
67import android.graphics.BitmapFactory
78import android.os.Build
89import android.os.Environment
@@ -12,35 +13,41 @@ import androidx.compose.animation.AnimatedVisibility
1213import androidx.compose.animation.slideInVertically
1314import androidx.compose.animation.slideOutVertically
1415import androidx.compose.foundation.ExperimentalFoundationApi
15- import androidx.compose.foundation.Image
1616import androidx.compose.foundation.background
1717import androidx.compose.foundation.clickable
1818import androidx.compose.foundation.layout.Arrangement
1919import androidx.compose.foundation.layout.Box
2020import androidx.compose.foundation.layout.Row
21+ import androidx.compose.foundation.layout.Spacer
2122import androidx.compose.foundation.layout.fillMaxSize
2223import androidx.compose.foundation.layout.fillMaxWidth
2324import androidx.compose.foundation.layout.padding
2425import androidx.compose.foundation.layout.size
2526import androidx.compose.foundation.layout.systemBarsPadding
2627import androidx.compose.foundation.shape.CircleShape
2728import androidx.compose.material3.ExperimentalMaterial3Api
29+ import androidx.compose.material3.ListItem
30+ import androidx.compose.material3.ListItemDefaults
2831import androidx.compose.material3.MaterialTheme
32+ import androidx.compose.material3.ModalBottomSheet
33+ import androidx.compose.material3.Text
2934import androidx.compose.runtime.Composable
30- import androidx.compose.runtime.LaunchedEffect
3135import androidx.compose.runtime.getValue
3236import androidx.compose.runtime.mutableStateOf
3337import androidx.compose.runtime.remember
3438import androidx.compose.runtime.setValue
3539import androidx.compose.ui.Alignment
3640import androidx.compose.ui.Modifier
3741import androidx.compose.ui.draw.alpha
42+ import androidx.compose.ui.graphics.Color
43+ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
3844import androidx.compose.ui.layout.ContentScale
3945import androidx.compose.ui.platform.LocalContext
46+ import androidx.compose.ui.platform.LocalHapticFeedback
4047import androidx.compose.ui.res.stringResource
4148import androidx.compose.ui.unit.dp
49+ import androidx.core.content.FileProvider
4250import coil3.annotation.ExperimentalCoilApi
43- import coil3.compose.rememberAsyncImagePainter
4451import coil3.imageLoader
4552import coil3.request.ImageRequest
4653import coil3.request.crossfade
@@ -50,6 +57,7 @@ import com.google.accompanist.permissions.rememberPermissionState
5057import compose.icons.FontAwesomeIcons
5158import compose.icons.fontawesomeicons.Solid
5259import compose.icons.fontawesomeicons.solid.Download
60+ import compose.icons.fontawesomeicons.solid.ShareNodes
5361import compose.icons.fontawesomeicons.solid.Xmark
5462import dev.chrisbanes.haze.hazeSource
5563import dev.chrisbanes.haze.rememberHazeState
@@ -62,9 +70,9 @@ import kotlinx.coroutines.Dispatchers
6270import kotlinx.coroutines.launch
6371import kotlinx.coroutines.withContext
6472import me.saket.telephoto.zoomable.ZoomSpec
65- import me.saket.telephoto.zoomable.ZoomableContentLocation
73+ import me.saket.telephoto.zoomable.coil3.ZoomableAsyncImage
74+ import me.saket.telephoto.zoomable.rememberZoomableImageState
6675import me.saket.telephoto.zoomable.rememberZoomableState
67- import me.saket.telephoto.zoomable.zoomable
6876import moe.tlaster.precompose.molecule.producePresenter
6977import moe.tlaster.swiper.Swiper
7078import moe.tlaster.swiper.rememberSwiperState
@@ -84,6 +92,7 @@ internal fun MediaScreen(
8492 previewUrl : String? ,
8593 onDismiss : () -> Unit ,
8694) {
95+ val hapticFeedback = LocalHapticFeedback .current
8796 val hazeState = rememberHazeState()
8897 val context = LocalContext .current
8998 val permissionState =
@@ -115,36 +124,31 @@ internal fun MediaScreen(
115124 .hazeSource(state = hazeState),
116125 ) {
117126 val zoomableState =
118- rememberZoomableState(zoomSpec = ZoomSpec (maxZoomFactor = 10f ))
119- val painter =
120- rememberAsyncImagePainter(
121- model =
122- ImageRequest
123- .Builder (LocalContext .current)
124- .data(uri)
125- .placeholderMemoryCacheKey(previewUrl)
126- .crossfade(1_000 )
127- .build(),
128- )
129- LaunchedEffect (painter.intrinsicSize) {
130- zoomableState.setContentLocation(
131- ZoomableContentLocation .scaledInsideAndCenterAligned(
132- painter.intrinsicSize,
133- ),
134- )
135- }
136- Image (
137- painter = painter,
127+ rememberZoomableImageState(rememberZoomableState(zoomSpec = ZoomSpec (maxZoomFactor = 10f )))
128+ ZoomableAsyncImage (
129+ model =
130+ ImageRequest
131+ .Builder (LocalContext .current)
132+ .data(uri)
133+ .placeholderMemoryCacheKey(previewUrl)
134+ .crossfade(1_000 )
135+ .build(),
138136 contentDescription = null ,
139- contentScale = ContentScale .Inside ,
137+ contentScale = ContentScale .Fit ,
140138 alignment = Alignment .Center ,
139+ state = zoomableState,
140+ onClick = {
141+ state.setShowUi(! state.showUi)
142+ },
143+ onLongClick = {
144+ hapticFeedback.performHapticFeedback(
145+ HapticFeedbackType .LongPress ,
146+ )
147+ state.setShowSheet(true )
148+ },
141149 modifier =
142150 Modifier
143- .fillMaxSize()
144- .zoomable(zoomableState)
145- .clickable {
146- state.setShowUi(! state.showUi)
147- },
151+ .fillMaxSize(),
148152 )
149153 }
150154 AnimatedVisibility (
@@ -161,7 +165,7 @@ internal fun MediaScreen(
161165 Modifier
162166 .systemBarsPadding()
163167 .padding(horizontal = 4 .dp, vertical = 8 .dp),
164- horizontalArrangement = Arrangement .SpaceBetween ,
168+ horizontalArrangement = Arrangement .spacedBy( 8 .dp) ,
165169 ) {
166170 Glassify (
167171 onClick = {
@@ -177,6 +181,7 @@ internal fun MediaScreen(
177181 contentDescription = stringResource(id = R .string.navigate_back),
178182 )
179183 }
184+ Spacer (Modifier .weight(1f ))
180185 Glassify (
181186 onClick = {
182187 if (Build .VERSION .SDK_INT < Build .VERSION_CODES .Q ) {
@@ -199,9 +204,84 @@ internal fun MediaScreen(
199204 contentDescription = stringResource(id = R .string.media_menu_save),
200205 )
201206 }
207+ Glassify (
208+ onClick = {
209+ state.share()
210+ },
211+ hazeState = hazeState,
212+ color = MaterialTheme .colorScheme.secondaryContainer,
213+ modifier = Modifier .size(40 .dp),
214+ shape = CircleShape ,
215+ ) {
216+ FAIcon (
217+ FontAwesomeIcons .Solid .ShareNodes ,
218+ contentDescription = stringResource(id = R .string.media_menu_share_image),
219+ )
220+ }
202221 }
203222 }
204223 }
224+
225+ if (state.showSheet) {
226+ ModalBottomSheet (
227+ onDismissRequest = {
228+ state.setShowSheet(false )
229+ },
230+ ) {
231+ ListItem (
232+ headlineContent = {
233+ Text (stringResource(id = R .string.media_menu_save))
234+ },
235+ leadingContent = {
236+ FAIcon (
237+ FontAwesomeIcons .Solid .Download ,
238+ contentDescription = null ,
239+ modifier = Modifier .size(24 .dp),
240+ )
241+ },
242+ modifier =
243+ Modifier
244+ .clickable {
245+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .Q ) {
246+ if (! permissionState.status.isGranted) {
247+ permissionState.launchPermissionRequest()
248+ } else {
249+ state.save()
250+ }
251+ } else {
252+ state.save()
253+ }
254+ state.setShowSheet(false )
255+ },
256+ colors =
257+ ListItemDefaults .colors(
258+ containerColor = Color .Transparent ,
259+ ),
260+ )
261+ ListItem (
262+ headlineContent = {
263+ Text (stringResource(id = R .string.media_menu_share_image))
264+ },
265+ leadingContent = {
266+ FAIcon (
267+ FontAwesomeIcons .Solid .ShareNodes ,
268+ contentDescription = null ,
269+ modifier = Modifier .size(24 .dp),
270+ )
271+ },
272+ modifier =
273+ Modifier
274+ .clickable {
275+ state.share()
276+ state.setShowSheet(false )
277+ },
278+ colors =
279+ ListItemDefaults .colors(
280+ containerColor = Color .Transparent ,
281+ ),
282+ )
283+ }
284+ }
205285 }
206286}
207287
@@ -215,9 +295,18 @@ private fun mediaPresenter(
215295 var showUi by remember {
216296 mutableStateOf(true )
217297 }
298+ var showSheet by remember {
299+ mutableStateOf(false )
300+ }
218301 object {
219302 val showUi = showUi
220303
304+ val showSheet = showSheet
305+
306+ fun setShowSheet (value : Boolean ) {
307+ showSheet = value
308+ }
309+
221310 fun setShowUi (value : Boolean ) {
222311 showUi = value
223312 }
@@ -239,6 +328,42 @@ private fun mediaPresenter(
239328 }
240329 }
241330 }
331+
332+ fun share () {
333+ scope.launch {
334+ context.imageLoader.diskCache?.openSnapshot(uri)?.use {
335+ val originFile = it.data.toFile()
336+ val targetFile =
337+ File (
338+ context.cacheDir,
339+ uri.substringAfterLast(" /" ),
340+ )
341+ originFile.copyTo(targetFile, overwrite = true )
342+ val uri =
343+ FileProvider .getUriForFile(
344+ context,
345+ context.packageName + " .provider" ,
346+ targetFile,
347+ )
348+ val intent =
349+ Intent ().apply {
350+ action = Intent .ACTION_SEND
351+ putExtra(Intent .EXTRA_STREAM , uri)
352+ setDataAndType(
353+ uri,
354+ " image/*" ,
355+ )
356+ addFlags(Intent .FLAG_GRANT_READ_URI_PERMISSION )
357+ }
358+ context.startActivity(
359+ Intent .createChooser(
360+ intent,
361+ context.getString(R .string.media_menu_share_image),
362+ ),
363+ )
364+ }
365+ }
366+ }
242367 }
243368}
244369
0 commit comments