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 a341fd5a8e..34ebb9abe4 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 @@ -44,6 +44,7 @@ import okio.buffer import okio.sink import okio.source import java.io.File +import java.io.IOException import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -53,6 +54,7 @@ object MediaSaverToDisk { okHttpClient: (String) -> OkHttpClient, mimeType: String?, localContext: Context, + resolveBlossom: suspend (String) -> String? = { null }, onSuccess: () -> Any?, onError: (Throwable) -> Any?, ) = withContext(Dispatchers.IO) { @@ -77,6 +79,7 @@ object MediaSaverToDisk { mimeType = mimeType, okHttpClient = okHttpClient, context = localContext, + resolveBlossom = resolveBlossom, onSuccess = onSuccess, onError = onError, ) @@ -87,6 +90,11 @@ object MediaSaverToDisk { /** * Saves the image to the gallery. May require a storage permission. * + * `blossom:` (BUD-10) URIs are resolved to a concrete `http(s)` server URL + * via [resolveBlossom] before downloading, since OkHttp only speaks + * http/https. When resolution fails the save reports an error instead of + * crashing with "expected scheme http or https but was blossom". + * * @see AMETHYST_SUBDIRECTORY */ suspend fun downloadAndSave( @@ -94,26 +102,35 @@ object MediaSaverToDisk { mimeType: String?, okHttpClient: (String) -> OkHttpClient, context: Context, + resolveBlossom: suspend (String) -> String? = { null }, onSuccess: () -> Any?, onError: (Throwable) -> Any?, ) { try { - val client = okHttpClient(url) + val downloadUrl = + if (url.startsWith(BLOSSOM_SCHEME, ignoreCase = true)) { + resolveBlossom(url) + ?: throw IOException("Could not find a Blossom server that hosts $url") + } else { + url + } + + val client = okHttpClient(downloadUrl) val request = Request .Builder() .get() - .url(url) + .url(downloadUrl) .build() client.newCall(request).executeAsync().use { response -> withContext(Dispatchers.IO) { check(response.isSuccessful) { - "Failed to download $url: HTTP ${response.code} ${response.message}" + "Failed to download $downloadUrl: HTTP ${response.code} ${response.message}" } - val trimmedUrl = trimInlineMetaData(url) + val trimmedUrl = trimInlineMetaData(downloadUrl) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val headerType = response @@ -292,4 +309,5 @@ object MediaSaverToDisk { private const val AMETHYST_SUBDIRECTORY = "Amethyst" private const val PDF_MIME_TYPE = "application/pdf" + private const val BLOSSOM_SCHEME = "blossom:" } 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 56b3440198..defca41a12 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 @@ -83,6 +83,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -532,6 +533,11 @@ internal suspend fun saveMediaToGallery( } }, localContext, + resolveBlossom = { + Amethyst.instance.blossomResolver + .findServers(it) + ?.serverUrl + }, onSuccess = { showToastOnMain(localContext, success) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index c4efef0e6b..279cd83ceb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -2254,6 +2254,11 @@ class AccountViewModel( okHttpClient = httpClientBuilder::okHttpClientForVideo, mimeType = mimeType, localContext = localContext, + resolveBlossom = { + Amethyst.instance.blossomResolver + .findServers(it) + ?.serverUrl + }, onSuccess = { Handler(Looper.getMainLooper()).post { Toast diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt index 724c252da0..4c834b2e8c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt @@ -60,7 +60,6 @@ object Hex { * emoji in `p` tags) instead of throwing. ~47ns in debug on the Emulator; * use [isHex64] when the length is known to be 64. */ - // 47ns in debug on the Emulator fun isHex(hex: String?): Boolean { if (hex == null) return false if (hex.length and 1 != 0) return false @@ -93,7 +92,6 @@ object Hex { * the length is fixed and the checks are unrolled. Assumes [hex] is at least * 64 chars long; it does not verify the total length. */ - // 30% faster than isHex fun isHex64(hex: String): Boolean = try { hexToByte[hex[0].code] >= 0 &&