Skip to content

Commit

Permalink
Show snackbar on save success, failure
Browse files Browse the repository at this point in the history
  • Loading branch information
jocmp committed Feb 15, 2025
1 parent eccc61d commit 9216f5f
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 56 deletions.
4 changes: 2 additions & 2 deletions app/src/main/java/com/capyreader/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class MainActivity : ComponentActivity() {
enableStrictModeOnDebug()
enableEdgeToEdge()
super.onCreate(savedInstanceState)
NotificationHelper.handleResult(intent, appPreferences = appPreferences)
NotificationHelper.openArticle(intent, appPreferences = appPreferences)

val theme = appPreferences.theme

Expand All @@ -39,7 +39,7 @@ class MainActivity : ComponentActivity() {

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
NotificationHelper.handleResult(intent, appPreferences = appPreferences)
NotificationHelper.openArticle(intent, appPreferences = appPreferences)
}

private fun startDestination(): Route {
Expand Down
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class NotificationHelper(
}
}

fun handleResult(intent: Intent, appPreferences: AppPreferences) {
fun openArticle(intent: Intent, appPreferences: AppPreferences) {
val articleID = intent.getStringExtra(ARTICLE_ID_KEY) ?: return
val feedID = intent.getStringExtra(FEED_ID_KEY) ?: return
intent.replaceExtras(Bundle())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@ import androidx.compose.foundation.layout.fillMaxHeight
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.widthIn
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
Expand All @@ -40,6 +47,7 @@ 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.settings.LocalSnackbarHost
import com.capyreader.app.ui.theme.CapyTheme
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
Expand Down Expand Up @@ -67,7 +75,10 @@ fun ArticleMediaView(
onDismissRequest = onDismissRequest,
showOverlay = showOverlay,
footer = {
CaptionOverlay(caption)
CaptionOverlay(
caption = caption,
imageUrl = url
)
}
) {
ZoomableAsyncImage(
Expand Down Expand Up @@ -132,42 +143,64 @@ fun MediaScaffold(
)

val isOverlayVisible = showOverlay && swiperState.progress == 0f
val snackbarHostState = remember { SnackbarHostState() }

Scaffold(
containerColor = Color.Black.copy(alpha = 1f - swiperState.progress),
modifier = Modifier
.fillMaxSize()
.fillMaxSize(),
snackbarHost = {
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.statusBarsPadding()
.fillMaxSize(),
) {
SnackbarHost(snackbarHostState) { data ->
val darkColors = darkColorScheme()
Snackbar(
data,
containerColor = darkColors.inverseSurface,
contentColor = darkColors.inverseOnSurface,
)
}
}
}
) { paddingValues ->
Box(
Modifier.padding(paddingValues)
CompositionLocalProvider(
LocalSnackbarHost provides snackbarHostState,
) {
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 +213,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 +225,7 @@ private fun CaptionOverlay(text: String?) {
)
) {
Text(
text,
caption,
color = MediaColors.textColor,
modifier = Modifier
.padding(top = 8.dp)
Expand All @@ -202,8 +235,8 @@ private fun CaptionOverlay(text: String?) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
MediaSaveButton()
MediaShareButton()
MediaSaveButton(imageUrl)
MediaShareButton(imageUrl)
}
}
}
Expand Down Expand Up @@ -233,7 +266,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 +285,8 @@ private fun ArticleMediaViewPreview_Phone() {
) {
Box(Modifier.align(Alignment.BottomStart)) {
CaptionOverlay(
"A description"
"A description",
"http://example.com/test.jpg"
)
}
}
Expand All @@ -270,7 +305,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
Expand Up @@ -7,35 +7,33 @@ 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 org.koin.core.component.KoinComponent
import java.io.ByteArrayOutputStream
import java.io.FileOutputStream
import java.io.IOException

class ImageSaver(
private val context: Context,
) : KoinComponent {
suspend fun saveImage(imageUrl: String, filename: String): Result<Uri> {

object ImageSaver {
suspend fun saveImage(imageUrl: String, context: Context): Result<Uri> {
return withContext(Dispatchers.IO) {
try {
val imageRequest = ImageRequest.Builder(context)
.data(imageUrl)
.build()
return@withContext try {
val bitmap =
context.imageLoader.executeBlocking(imageRequest).drawable?.toBitmap()
?: return@withContext Result.failure(Error())
createBitmap(imageUrl, context) ?: throw IOException("Failed to generate image")

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

Expand All @@ -48,9 +46,32 @@ class ImageSaver(
outputStream.write(jpegStream(bitmap))
} ?: throw IOException("Failed to open output stream")

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

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(jpegFileName(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)
}
}
}
Expand All @@ -60,4 +81,16 @@ class ImageSaver(
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
return byteArrayOutputStream.toByteArray()
}

private fun jpegFileName(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()
}
}
Loading

0 comments on commit 9216f5f

Please sign in to comment.