diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt
index 0d618a0c0..9837d17e2 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt
@@ -31,7 +31,7 @@ import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.net.toFile
import androidx.core.net.toUri
-import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.PICTURES_SUBDIRECTORY
+import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.AMETHYST_SUBDIRECTORY
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -87,7 +87,7 @@ object MediaSaverToDisk {
/**
* Saves the image to the gallery. May require a storage permission.
*
- * @see PICTURES_SUBDIRECTORY
+ * @see AMETHYST_SUBDIRECTORY
*/
suspend fun downloadAndSave(
url: String,
@@ -120,9 +120,9 @@ object MediaSaverToDisk {
?.trim()
val realType =
- headerType?.takeIf(::isMediaMimeType)
- ?: mimeType?.takeIf(::isMediaMimeType)
- ?: getMimeTypeFromExtension(trimmedUrl).takeIf(::isMediaMimeType)
+ headerType?.takeIf(::isSaveableMimeType)
+ ?: mimeType?.takeIf(::isSaveableMimeType)
+ ?: getMimeTypeFromExtension(trimmedUrl).takeIf(::isSaveableMimeType)
?: ""
check(realType.isNotBlank()) { "Can't find out the content type" }
@@ -154,12 +154,13 @@ object MediaSaverToDisk {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it).orEmpty()
}
- private fun isMediaMimeType(type: String): Boolean =
+ private fun isSaveableMimeType(type: String): Boolean =
type.isNotBlank() &&
(
type.startsWith("image/", ignoreCase = true) ||
type.startsWith("video/", ignoreCase = true) ||
- type.startsWith("audio/", ignoreCase = true)
+ type.startsWith("audio/", ignoreCase = true) ||
+ type.equals(PDF_MIME_TYPE, ignoreCase = true)
)
@OptIn(ExperimentalUuidApi::class)
@@ -205,22 +206,32 @@ object MediaSaverToDisk {
contentResolver: ContentResolver,
) {
val cleanMimeType = contentType.substringBefore(";").trim()
+
+ val (masterUri, baseDir) =
+ when {
+ cleanMimeType.startsWith("image/", ignoreCase = true) -> {
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI to Environment.DIRECTORY_PICTURES
+ }
+
+ cleanMimeType.equals(PDF_MIME_TYPE, ignoreCase = true) -> {
+ MediaStore.Downloads.EXTERNAL_CONTENT_URI to Environment.DIRECTORY_DOWNLOADS
+ }
+
+ else -> {
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI to Environment.DIRECTORY_PICTURES
+ }
+ }
+
val contentValues =
ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.MIME_TYPE, cleanMimeType)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
- Environment.DIRECTORY_PICTURES + File.separatorChar + PICTURES_SUBDIRECTORY,
+ baseDir + File.separatorChar + AMETHYST_SUBDIRECTORY,
)
}
- val masterUri =
- when {
- contentType.startsWith("image") -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
- else -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
- }
-
val uri = contentResolver.insert(masterUri, contentValues)
checkNotNull(uri) { "Can't insert the new content" }
@@ -244,7 +255,7 @@ object MediaSaverToDisk {
val subdirectory =
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
- PICTURES_SUBDIRECTORY,
+ AMETHYST_SUBDIRECTORY,
).apply {
if (!exists()) mkdirs()
}
@@ -260,5 +271,6 @@ object MediaSaverToDisk {
private fun trimInlineMetaData(url: String): String = url.substringBefore("#")
- private const val PICTURES_SUBDIRECTORY = "Amethyst"
+ private const val AMETHYST_SUBDIRECTORY = "Amethyst"
+ private const val PDF_MIME_TYPE = "application/pdf"
}
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt
index aa0f69ef9..332bc16d3 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt
@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
import com.vitorpamplona.amethyst.commons.richtext.MediaPreloadedContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
+import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
@@ -377,7 +378,9 @@ private fun DialogContent(
ShareMediaAction(accountViewModel = accountViewModel, popupExpanded = sharePopupExpanded, myContent, onDismiss = { sharePopupExpanded.value = false })
}
+ }
+ if (myContent is MediaUrlImage || myContent is MediaLocalImage || myContent is MediaUrlPdf) {
if (myContent !is MediaUrlContent || !isLiveStreaming(myContent.url)) {
val localContext = LocalContext.current
@@ -447,22 +450,33 @@ private fun showToastOnMain(
}
}
-private suspend fun saveMediaToGallery(
+internal suspend fun saveMediaToGallery(
content: BaseMediaContent,
localContext: Context,
accountViewModel: AccountViewModel,
) {
val isImage = content is MediaUrlImage || content is MediaLocalImage
+ val isPdf = content is MediaUrlPdf
- val success = if (isImage) R.string.image_saved_to_the_gallery else R.string.video_saved_to_the_gallery
- val failure = if (isImage) R.string.failed_to_save_the_image else R.string.failed_to_save_the_video
+ val success =
+ when {
+ isImage -> R.string.image_saved_to_the_gallery
+ isPdf -> R.string.pdf_saved_to_the_gallery
+ else -> R.string.video_saved_to_the_gallery
+ }
+ val failure =
+ when {
+ isImage -> R.string.failed_to_save_the_image
+ isPdf -> R.string.failed_to_save_the_pdf
+ else -> R.string.failed_to_save_the_video
+ }
if (content is MediaUrlContent) {
MediaSaverToDisk.downloadAndSave(
content.url,
mimeType = content.mimeType,
okHttpClient = {
- if (isImage) {
+ if (isImage || isPdf) {
accountViewModel.httpClientBuilder.okHttpClientForImage(it)
} else {
accountViewModel.httpClientBuilder.okHttpClientForVideo(it)
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt
index dcfd488d3..1244e6a62 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt
@@ -914,6 +914,21 @@ fun ShareMediaAction(
onDismiss()
}
}
+ if (content is MediaUrlPdf && videoUri != null && !videoUri.startsWith("file")) {
+ val appContext = LocalContext.current.applicationContext
+ M3ActionRow(icon = MaterialSymbols.Download, text = stringRes(R.string.download_to_phone)) {
+ accountViewModel.viewModelScope.launch(Dispatchers.IO) {
+ saveMediaToGallery(content, appContext, accountViewModel)
+ }
+ Toast
+ .makeText(
+ appContext,
+ stringRes(appContext, R.string.media_download_has_started_toast),
+ Toast.LENGTH_SHORT,
+ ).show()
+ onDismiss()
+ }
+ }
postNostrUri?.let {
M3ActionRow(icon = MaterialSymbols.ContentCopy, text = stringRes(R.string.copy_the_note_id_to_the_clipboard)) {
scope.launch {
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index 959849ab3..0502a8da3 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -176,6 +176,8 @@
Failed to save the image
Video saved to the phone\'s video gallery
Failed to save the video
+ PDF saved to Downloads/Amethyst
+ Failed to save the PDF
Upload Image
Upload File
Take a picture