Fix cover image sampling not working when webview strips origin header, instead fetch image from server with CapacitorHttp

This commit is contained in:
advplyr
2026-05-10 15:15:13 -05:00
parent b8ac073247
commit 853546f3d2
5 changed files with 118 additions and 27 deletions
+59
View File
@@ -0,0 +1,59 @@
import { Capacitor } from '@capacitor/core'
import { FastAverageColor } from 'fast-average-color'
import { imageHttpDataToBlob } from '@/utils/imageHttpBlob'
/**
* True when the cover URL is http(s) and not same-origin with the WebView (or browser tab).
* Same-origin URLs (e.g. Capacitor file bridge on localhost) can use FastAverageColor directly.
*/
export function shouldFetchCoverViaNativeHttp(coverUrl) {
if (!coverUrl || typeof coverUrl !== 'string') return false
if (!coverUrl.startsWith('http://') && !coverUrl.startsWith('https://')) return false
try {
return new URL(coverUrl).origin !== window.location.origin
} catch {
return false
}
}
/**
* Average color for a cover image. On native, cross-origin http(s) covers are loaded with
* CapacitorHttp (no WebView CORS), then sampled via a same-origin blob URL.
* @param {*} vm - component instance with $nativeHttp (this)
* @param {string} fullCoverUrl
* @returns {Promise<{ rgba: string, isLight: boolean }|null>}
*/
export async function getAverageColorFromCoverUrl(vm, fullCoverUrl) {
if (!fullCoverUrl) return null
const fac = new FastAverageColor()
let objectUrl = null
try {
let resource = fullCoverUrl
if (Capacitor.isNativePlatform() && shouldFetchCoverViaNativeHttp(fullCoverUrl)) {
const raw = await vm.$nativeHttp.get(fullCoverUrl, {
responseType: 'blob',
connectTimeout: 15000,
readTimeout: 30000
})
const blob = imageHttpDataToBlob(raw)
if (!blob) {
throw new Error('Cover image response could not be converted to a blob')
}
objectUrl = URL.createObjectURL(blob)
resource = objectUrl
}
const color = await fac.getColorAsync(resource)
return { rgba: color.rgba, isLight: color.isLight }
} catch (e) {
console.error('[coverAverageColor]', e)
return null
} finally {
if (objectUrl) {
URL.revokeObjectURL(objectUrl)
}
fac.destroy()
}
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Normalize CapacitorHttp binary responses (Blob, base64 string, data URL, ArrayBuffer) to a Blob.
* @param {unknown} data - CapacitorHttp response `data` when responseType is blob/arraybuffer
* @param {string} [mimeType='image/jpeg'] - fallback Content-Type
* @returns {Blob|null}
*/
export function imageHttpDataToBlob(data, mimeType = 'image/jpeg') {
if (data == null) return null
if (typeof Blob !== 'undefined' && data instanceof Blob) return data
if (data instanceof ArrayBuffer) return new Blob([data], { type: mimeType })
if (ArrayBuffer.isView(data)) return new Blob([data], { type: mimeType })
if (typeof data === 'object' && typeof data.base64 === 'string') {
return base64ToBlob(data.base64, mimeType)
}
if (typeof data === 'string') {
if (data.startsWith('data:')) {
const match = /^data:([^;]+);base64,([\s\S]+)$/.exec(data)
if (match) {
const type = match[1] || mimeType
return base64ToBlob(match[2], type)
}
}
try {
return base64ToBlob(data, mimeType)
} catch {
return null
}
}
return null
}
function base64ToBlob(base64, type) {
const binaryString = atob(base64)
const len = binaryString.length
const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return new Blob([bytes], { type })
}