Allow PDF downloads, route them to Downloads/Amethyst
expose Save-to-Disk button for PDFs add Save-to-Phone row for PDFs
This commit is contained in:
@@ -31,7 +31,7 @@ import android.webkit.MimeTypeMap
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
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 com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -87,7 +87,7 @@ object MediaSaverToDisk {
|
|||||||
/**
|
/**
|
||||||
* Saves the image to the gallery. May require a storage permission.
|
* Saves the image to the gallery. May require a storage permission.
|
||||||
*
|
*
|
||||||
* @see PICTURES_SUBDIRECTORY
|
* @see AMETHYST_SUBDIRECTORY
|
||||||
*/
|
*/
|
||||||
suspend fun downloadAndSave(
|
suspend fun downloadAndSave(
|
||||||
url: String,
|
url: String,
|
||||||
@@ -120,9 +120,9 @@ object MediaSaverToDisk {
|
|||||||
?.trim()
|
?.trim()
|
||||||
|
|
||||||
val realType =
|
val realType =
|
||||||
headerType?.takeIf(::isMediaMimeType)
|
headerType?.takeIf(::isSaveableMimeType)
|
||||||
?: mimeType?.takeIf(::isMediaMimeType)
|
?: mimeType?.takeIf(::isSaveableMimeType)
|
||||||
?: getMimeTypeFromExtension(trimmedUrl).takeIf(::isMediaMimeType)
|
?: getMimeTypeFromExtension(trimmedUrl).takeIf(::isSaveableMimeType)
|
||||||
?: ""
|
?: ""
|
||||||
check(realType.isNotBlank()) { "Can't find out the content type" }
|
check(realType.isNotBlank()) { "Can't find out the content type" }
|
||||||
|
|
||||||
@@ -154,12 +154,13 @@ object MediaSaverToDisk {
|
|||||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it).orEmpty()
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(it).orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isMediaMimeType(type: String): Boolean =
|
private fun isSaveableMimeType(type: String): Boolean =
|
||||||
type.isNotBlank() &&
|
type.isNotBlank() &&
|
||||||
(
|
(
|
||||||
type.startsWith("image/", ignoreCase = true) ||
|
type.startsWith("image/", ignoreCase = true) ||
|
||||||
type.startsWith("video/", 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)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
@@ -205,22 +206,32 @@ object MediaSaverToDisk {
|
|||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
) {
|
) {
|
||||||
val cleanMimeType = contentType.substringBefore(";").trim()
|
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 =
|
val contentValues =
|
||||||
ContentValues().apply {
|
ContentValues().apply {
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, cleanMimeType)
|
put(MediaStore.MediaColumns.MIME_TYPE, cleanMimeType)
|
||||||
put(
|
put(
|
||||||
MediaStore.MediaColumns.RELATIVE_PATH,
|
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)
|
val uri = contentResolver.insert(masterUri, contentValues)
|
||||||
checkNotNull(uri) { "Can't insert the new content" }
|
checkNotNull(uri) { "Can't insert the new content" }
|
||||||
|
|
||||||
@@ -244,7 +255,7 @@ object MediaSaverToDisk {
|
|||||||
val subdirectory =
|
val subdirectory =
|
||||||
File(
|
File(
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||||
PICTURES_SUBDIRECTORY,
|
AMETHYST_SUBDIRECTORY,
|
||||||
).apply {
|
).apply {
|
||||||
if (!exists()) mkdirs()
|
if (!exists()) mkdirs()
|
||||||
}
|
}
|
||||||
@@ -260,5 +271,6 @@ object MediaSaverToDisk {
|
|||||||
|
|
||||||
private fun trimInlineMetaData(url: String): String = url.substringBefore("#")
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-4
@@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaLocalVideo
|
|||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaPreloadedContent
|
import com.vitorpamplona.amethyst.commons.richtext.MediaPreloadedContent
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
|
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlContent
|
||||||
import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage
|
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.commons.richtext.MediaUrlVideo
|
||||||
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
import com.vitorpamplona.amethyst.model.MediaAspectRatioCache
|
||||||
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
|
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 })
|
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)) {
|
if (myContent !is MediaUrlContent || !isLiveStreaming(myContent.url)) {
|
||||||
val localContext = LocalContext.current
|
val localContext = LocalContext.current
|
||||||
|
|
||||||
@@ -447,22 +450,33 @@ private fun showToastOnMain(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveMediaToGallery(
|
internal suspend fun saveMediaToGallery(
|
||||||
content: BaseMediaContent,
|
content: BaseMediaContent,
|
||||||
localContext: Context,
|
localContext: Context,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val isImage = content is MediaUrlImage || content is MediaLocalImage
|
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 success =
|
||||||
val failure = if (isImage) R.string.failed_to_save_the_image else R.string.failed_to_save_the_video
|
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) {
|
if (content is MediaUrlContent) {
|
||||||
MediaSaverToDisk.downloadAndSave(
|
MediaSaverToDisk.downloadAndSave(
|
||||||
content.url,
|
content.url,
|
||||||
mimeType = content.mimeType,
|
mimeType = content.mimeType,
|
||||||
okHttpClient = {
|
okHttpClient = {
|
||||||
if (isImage) {
|
if (isImage || isPdf) {
|
||||||
accountViewModel.httpClientBuilder.okHttpClientForImage(it)
|
accountViewModel.httpClientBuilder.okHttpClientForImage(it)
|
||||||
} else {
|
} else {
|
||||||
accountViewModel.httpClientBuilder.okHttpClientForVideo(it)
|
accountViewModel.httpClientBuilder.okHttpClientForVideo(it)
|
||||||
|
|||||||
+15
@@ -914,6 +914,21 @@ fun ShareMediaAction(
|
|||||||
onDismiss()
|
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 {
|
postNostrUri?.let {
|
||||||
M3ActionRow(icon = MaterialSymbols.ContentCopy, text = stringRes(R.string.copy_the_note_id_to_the_clipboard)) {
|
M3ActionRow(icon = MaterialSymbols.ContentCopy, text = stringRes(R.string.copy_the_note_id_to_the_clipboard)) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -176,6 +176,8 @@
|
|||||||
<string name="failed_to_save_the_image">Failed to save the image</string>
|
<string name="failed_to_save_the_image">Failed to save the image</string>
|
||||||
<string name="video_saved_to_the_gallery">Video saved to the phone\'s video gallery</string>
|
<string name="video_saved_to_the_gallery">Video saved to the phone\'s video gallery</string>
|
||||||
<string name="failed_to_save_the_video">Failed to save the video</string>
|
<string name="failed_to_save_the_video">Failed to save the video</string>
|
||||||
|
<string name="pdf_saved_to_the_gallery">PDF saved to Downloads/Amethyst</string>
|
||||||
|
<string name="failed_to_save_the_pdf">Failed to save the PDF</string>
|
||||||
<string name="upload_image">Upload Image</string>
|
<string name="upload_image">Upload Image</string>
|
||||||
<string name="upload_file">Upload File</string>
|
<string name="upload_file">Upload File</string>
|
||||||
<string name="take_a_picture">Take a picture</string>
|
<string name="take_a_picture">Take a picture</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user