mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-03 10:28:51 +02:00
Fix cover image sampling not working when webview strips origin header, instead fetch image from server with CapacitorHttp
This commit is contained in:
@@ -114,7 +114,7 @@
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import { FastAverageColor } from 'fast-average-color'
|
||||
import { getAverageColorFromCoverUrl } from '@/utils/coverAverageColor'
|
||||
import WrappingMarquee from '@/assets/WrappingMarquee.js'
|
||||
import jumpLabelMixin from '@/mixins/jumpLabel'
|
||||
|
||||
@@ -412,17 +412,14 @@ export default {
|
||||
},
|
||||
async coverImageLoaded(fullCoverUrl) {
|
||||
if (!fullCoverUrl) return
|
||||
|
||||
const fac = new FastAverageColor()
|
||||
fac
|
||||
.getColorAsync(fullCoverUrl)
|
||||
.then((color) => {
|
||||
this.coverRgb = color.rgba
|
||||
this.coverBgIsLight = color.isLight
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
const avg = await getAverageColorFromCoverUrl(this, fullCoverUrl)
|
||||
if (!avg) {
|
||||
this.coverRgb = 'rgb(55, 56, 56)'
|
||||
this.coverBgIsLight = false
|
||||
} else {
|
||||
this.coverRgb = avg.rgba
|
||||
this.coverBgIsLight = avg.isLight
|
||||
}
|
||||
},
|
||||
clickTitleAndAuthor() {
|
||||
if (!this.showFullscreen) return
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" @click="showFullscreenCover = true">
|
||||
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw @imageLoaded="coverImageLoaded" />
|
||||
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw />
|
||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 z-10 box-shadow-progressbar" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +172,7 @@
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
|
||||
import { FastAverageColor } from 'fast-average-color'
|
||||
import { getAverageColorFromCoverUrl } from '@/utils/coverAverageColor'
|
||||
import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
|
||||
|
||||
export default {
|
||||
@@ -496,17 +496,10 @@ export default {
|
||||
},
|
||||
async coverImageLoaded(fullCoverUrl) {
|
||||
if (!fullCoverUrl) return
|
||||
|
||||
const fac = new FastAverageColor()
|
||||
fac
|
||||
.getColorAsync(fullCoverUrl)
|
||||
.then((color) => {
|
||||
this.coverRgb = color.rgba
|
||||
this.coverBgIsLight = color.isLight
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
const avg = await getAverageColorFromCoverUrl(this, fullCoverUrl)
|
||||
if (!avg) return
|
||||
this.coverRgb = avg.rgba
|
||||
this.coverBgIsLight = avg.isLight
|
||||
},
|
||||
moreButtonPress() {
|
||||
this.showMoreMenu = true
|
||||
|
||||
@@ -43,7 +43,8 @@ export default function ({ store, $db, $socket }, inject) {
|
||||
}
|
||||
if (res.status >= 400) {
|
||||
console.error(`[nativeHttp] ${res.status} status for url "${url}"`)
|
||||
throw new Error(res.data)
|
||||
const message = typeof res.data === 'string' ? res.data : `HTTP ${res.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
return res.data
|
||||
})
|
||||
@@ -100,7 +101,8 @@ export default function ({ store, $db, $socket }, inject) {
|
||||
|
||||
if (retryResponse.status >= 400) {
|
||||
console.error(`[nativeHttp] Retry request failed with status ${retryResponse.status}`)
|
||||
throw new Error(retryResponse.data)
|
||||
const message = typeof retryResponse.data === 'string' ? retryResponse.data : `HTTP ${retryResponse.status}`
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return retryResponse.data
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
Reference in New Issue
Block a user