Skip to content

Commit

Permalink
Start adding snackbar
Browse files Browse the repository at this point in the history
  • Loading branch information
jocmp committed Feb 13, 2025
1 parent a888d2e commit 2609f01
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 100 deletions.
22 changes: 17 additions & 5 deletions app/src/main/java/com/capyreader/app/common/ContextFileExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ import java.io.File
fun Context.fileURI(file: File): Uri =
FileProvider.getUriForFile(this, "${BuildConfig.APPLICATION_ID}.fileprovider", file)

fun Context.externalImageCacheFile(name: String): File {
val imageCache = File(externalCacheDir, "images").apply {
if (!exists()) {
mkdir()
}
}

return File(imageCache, name).apply {
createNewFile()
}
}

fun Context.createCacheFile(name: String): File {
val file = File(externalCacheDir, name)
if (file.exists()) {
file.delete()
return File(externalCacheDir, name).apply {
if (exists()) {
delete()
}
createNewFile()
}
file.createNewFile()
return file
}
5 changes: 5 additions & 0 deletions app/src/main/java/com/capyreader/app/ui/CapySnackbar.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.capyreader.app.ui

interface CapySnackbar {
fun show(message: String)
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/capyreader/app/ui/LocalSnackbar.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.capyreader.app.ui

import androidx.compose.runtime.compositionLocalOf

val LocalSnackbar = compositionLocalOf { SnackbarState {} }

data class SnackbarState(
val showMessage: (message: String) -> Unit,
) : CapySnackbar {
override fun show(message: String) {
showMessage(message)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
Expand All @@ -36,11 +41,14 @@ import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import coil.request.ImageRequest
import com.capyreader.app.common.Media
import com.capyreader.app.ui.LocalSnackbar
import com.capyreader.app.ui.SnackbarState
import com.capyreader.app.ui.components.LoadingView
import com.capyreader.app.ui.components.Swiper
import com.capyreader.app.ui.components.rememberSwiperState
import com.capyreader.app.ui.isCompact
import com.capyreader.app.ui.theme.CapyTheme
import com.jocmp.capy.common.launchUI
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState
Expand All @@ -67,7 +75,10 @@ fun ArticleMediaView(
onDismissRequest = onDismissRequest,
showOverlay = showOverlay,
footer = {
CaptionOverlay(caption)
CaptionOverlay(
caption = caption,
imageUrl = url
)
}
) {
ZoomableAsyncImage(
Expand Down Expand Up @@ -131,43 +142,55 @@ fun MediaScaffold(
}
)

val scope = rememberCoroutineScope()
val isOverlayVisible = showOverlay && swiperState.progress == 0f
val snackbarHostState = remember { SnackbarHostState() }
val snackbarState = SnackbarState { message ->
scope.launchUI {
snackbarHostState.showSnackbar(message)
}
}

Scaffold(
containerColor = Color.Black.copy(alpha = 1f - swiperState.progress),
modifier = Modifier
.fillMaxSize()
.fillMaxSize(),
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
Box(
Modifier.padding(paddingValues)
CompositionLocalProvider(
LocalSnackbar provides snackbarState,
) {
Swiper(
state = swiperState,
modifier = Modifier.fillMaxSize()
Box(
Modifier.padding(paddingValues)
) {
content()
}

Box(Modifier.align(Alignment.BottomStart)) {
AnimatedVisibility(
isOverlayVisible,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut(),
Swiper(
state = swiperState,
modifier = Modifier.fillMaxSize()
) {
footer()
content()
}
}

CloseButton(
onClick = { onDismissRequest() },
visible = isOverlayVisible
)
Box(Modifier.align(Alignment.BottomStart)) {
AnimatedVisibility(
isOverlayVisible,
enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut(),
) {
footer()
}
}

CloseButton(
onClick = { onDismissRequest() },
visible = isOverlayVisible
)
}
}
}
}

@Composable
private fun CaptionOverlay(text: String?) {
private fun CaptionOverlay(caption: String?, imageUrl: String) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = if (isCompact()) {
Expand All @@ -180,7 +203,7 @@ private fun CaptionOverlay(text: String?) {
.background(Color.Black.copy(alpha = 0.8f))
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
if (!text.isNullOrBlank()) {
if (!caption.isNullOrBlank()) {
Box(
Modifier
.then(
Expand All @@ -192,7 +215,7 @@ private fun CaptionOverlay(text: String?) {
)
) {
Text(
text,
caption,
color = MediaColors.textColor,
modifier = Modifier
.padding(top = 8.dp)
Expand All @@ -202,8 +225,8 @@ private fun CaptionOverlay(text: String?) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
MediaSaveButton()
MediaShareButton()
MediaSaveButton(imageUrl)
MediaShareButton(imageUrl)
}
}
}
Expand Down Expand Up @@ -233,7 +256,8 @@ private fun ArticleMediaViewPreview_Foldable() {
) {
Box(Modifier.align(Alignment.BottomStart)) {
CaptionOverlay(
"A description of the picture you're taking a look at"
"A description of the picture you're taking a look at",
"http://example.com/test.jpg"
)
}
}
Expand All @@ -251,7 +275,8 @@ private fun ArticleMediaViewPreview_Phone() {
) {
Box(Modifier.align(Alignment.BottomStart)) {
CaptionOverlay(
"A description"
"A description",
"http://example.com/test.jpg"
)
}
}
Expand All @@ -270,7 +295,8 @@ private fun ArticleMediaViewPreview_Tablet() {
) {
Box(Modifier.align(Alignment.BottomStart)) {
CaptionOverlay(
"A description of the picture you're taking a look at"
"A description of the picture you're taking a look at",
"http://example.com/test.jpg"
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.capyreader.app.ui.articles.media

import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import android.provider.MediaStore
import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import coil.executeBlocking
import coil.imageLoader
import coil.request.ImageRequest
import com.capyreader.app.common.MD5
import com.capyreader.app.common.externalImageCacheFile
import com.capyreader.app.common.fileURI
import com.jocmp.capy.logging.CapyLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.io.FileOutputStream
import java.io.IOException

object ExternalImages {
suspend fun shareImage(imageUrl: String, context: Context): Result<Uri> {
return withContext(Dispatchers.IO) {
return@withContext try {
val bitmap =
createBitmap(imageUrl, context) ?: throw IOException("Failed to generate image")

val target = context.externalImageCacheFile(fileName(imageUrl))

context.contentResolver.openFileDescriptor(target.toUri(), "w")?.use { descriptor ->
FileOutputStream(descriptor.fileDescriptor).use {
it.write(jpegStream(bitmap))
}
}

Result.success(context.fileURI(target))
} catch (e: Exception) {
CapyLog.error("share_img", error = e)
Result.failure(e)
}
}
}

suspend fun saveImage(imageUrl: String, context: Context): Result<Uri> {
return withContext(Dispatchers.IO) {
return@withContext try {
val bitmap =
createBitmap(imageUrl, context) ?: throw IOException("Failed to generate image")

val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName(imageUrl))
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/Capy"
)
}

val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
) ?: throw IOException("Failed to create MediaStore entry")

context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(jpegStream(bitmap))
} ?: throw IOException("Failed to open output stream")

Result.success(uri)
} catch (e: Exception) {
CapyLog.error("save_img", error = e)
Result.failure(e)
}
}
}

private fun jpegStream(bitmap: Bitmap): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
return byteArrayOutputStream.toByteArray()
}

private fun fileName(imageUrl: String): String {
return "${MD5.from(imageUrl)}.jpg"
}

private fun createBitmap(imageUrl: String, context: Context): Bitmap? {
val imageRequest = ImageRequest.Builder(context)
.data(imageUrl)
.build()

return context.imageLoader.executeBlocking(imageRequest).drawable?.toBitmap()
}
}

This file was deleted.

Loading

0 comments on commit 2609f01

Please sign in to comment.