Skip to content

Commit e2a70ce

Browse files
authored
Merge pull request #1251 from DimensionDev/feature/media_menu
add media menu
2 parents cae03f2 + be60262 commit e2a70ce

File tree

5 files changed

+460
-125
lines changed

5 files changed

+460
-125
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,15 @@
8989
<action android:name="android.media.browse.MediaBrowserService"/>
9090
</intent-filter>
9191
</service>
92-
92+
<provider
93+
android:name="androidx.core.content.FileProvider"
94+
android:authorities="${applicationId}.provider"
95+
android:exported="false"
96+
android:grantUriPermissions="true">
97+
<meta-data
98+
android:name="android.support.FILE_PROVIDER_PATHS"
99+
android:resource="@xml/provider_paths" />
100+
</provider>
93101
</application>
94102

95103
</manifest>

app/src/main/java/dev/dimension/flare/ui/screen/media/MediaScreen.kt

Lines changed: 157 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.dimension.flare.ui.screen.media
33
import android.Manifest
44
import android.content.ContentValues
55
import android.content.Context
6+
import android.content.Intent
67
import android.graphics.BitmapFactory
78
import android.os.Build
89
import android.os.Environment
@@ -12,35 +13,41 @@ import androidx.compose.animation.AnimatedVisibility
1213
import androidx.compose.animation.slideInVertically
1314
import androidx.compose.animation.slideOutVertically
1415
import androidx.compose.foundation.ExperimentalFoundationApi
15-
import androidx.compose.foundation.Image
1616
import androidx.compose.foundation.background
1717
import androidx.compose.foundation.clickable
1818
import androidx.compose.foundation.layout.Arrangement
1919
import androidx.compose.foundation.layout.Box
2020
import androidx.compose.foundation.layout.Row
21+
import androidx.compose.foundation.layout.Spacer
2122
import androidx.compose.foundation.layout.fillMaxSize
2223
import androidx.compose.foundation.layout.fillMaxWidth
2324
import androidx.compose.foundation.layout.padding
2425
import androidx.compose.foundation.layout.size
2526
import androidx.compose.foundation.layout.systemBarsPadding
2627
import androidx.compose.foundation.shape.CircleShape
2728
import androidx.compose.material3.ExperimentalMaterial3Api
29+
import androidx.compose.material3.ListItem
30+
import androidx.compose.material3.ListItemDefaults
2831
import androidx.compose.material3.MaterialTheme
32+
import androidx.compose.material3.ModalBottomSheet
33+
import androidx.compose.material3.Text
2934
import androidx.compose.runtime.Composable
30-
import androidx.compose.runtime.LaunchedEffect
3135
import androidx.compose.runtime.getValue
3236
import androidx.compose.runtime.mutableStateOf
3337
import androidx.compose.runtime.remember
3438
import androidx.compose.runtime.setValue
3539
import androidx.compose.ui.Alignment
3640
import androidx.compose.ui.Modifier
3741
import androidx.compose.ui.draw.alpha
42+
import androidx.compose.ui.graphics.Color
43+
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
3844
import androidx.compose.ui.layout.ContentScale
3945
import androidx.compose.ui.platform.LocalContext
46+
import androidx.compose.ui.platform.LocalHapticFeedback
4047
import androidx.compose.ui.res.stringResource
4148
import androidx.compose.ui.unit.dp
49+
import androidx.core.content.FileProvider
4250
import coil3.annotation.ExperimentalCoilApi
43-
import coil3.compose.rememberAsyncImagePainter
4451
import coil3.imageLoader
4552
import coil3.request.ImageRequest
4653
import coil3.request.crossfade
@@ -50,6 +57,7 @@ import com.google.accompanist.permissions.rememberPermissionState
5057
import compose.icons.FontAwesomeIcons
5158
import compose.icons.fontawesomeicons.Solid
5259
import compose.icons.fontawesomeicons.solid.Download
60+
import compose.icons.fontawesomeicons.solid.ShareNodes
5361
import compose.icons.fontawesomeicons.solid.Xmark
5462
import dev.chrisbanes.haze.hazeSource
5563
import dev.chrisbanes.haze.rememberHazeState
@@ -62,9 +70,9 @@ import kotlinx.coroutines.Dispatchers
6270
import kotlinx.coroutines.launch
6371
import kotlinx.coroutines.withContext
6472
import 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
6675
import me.saket.telephoto.zoomable.rememberZoomableState
67-
import me.saket.telephoto.zoomable.zoomable
6876
import moe.tlaster.precompose.molecule.producePresenter
6977
import moe.tlaster.swiper.Swiper
7078
import 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

Comments
 (0)