mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): stop reprobing image format support on every build (#1976)
This commit is contained in:
1 parent
66791d3ca2
commit
7a33b3198a
1 file changed
+5
-99
@@ -1,16 +1,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Config from '@app/features/app/config/Config';
|
||||
import {isSvgMimeType, normalizeImageMimeType} from '@app/features/expressions/utils/ImageUploadFileUtils';
|
||||
import AppStorage from '@app/features/platform/state/PersistentStorage';
|
||||
import {useEffect, useState, useSyncExternalStore} from 'react';
|
||||
|
||||
export type MediaFormat = 'avif' | 'webp' | 'apng' | 'jxl';
|
||||
|
||||
export interface CapabilityResult {
|
||||
interface CapabilityResult {
|
||||
avif: boolean;
|
||||
webp: boolean;
|
||||
apng: boolean;
|
||||
jxl: boolean;
|
||||
probedAt: number;
|
||||
uaKey: string;
|
||||
@@ -19,16 +14,12 @@ export interface CapabilityResult {
|
||||
const AVIF_PROBE =
|
||||
'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgIYAQQUDAJEBQYJlAQUAAAAB1FmDuOmIs=';
|
||||
const WEBP_PROBE = 'data:image/webp;base64,UklGRhwAAABXRUJQVlA4TBAAAAAvAAAAAAfQ//73v/+BiOh/AAA=';
|
||||
const APNG_PROBE =
|
||||
'data:image/apng;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAALOZWzgAAAANSURBVAjXY/j//z8DAAj8Av6IXwbgAAAAAElFTkSuQmCC';
|
||||
const JXL_PROBE = 'data:image/jxl;base64,/wr6PwH4TWFvLnVMkM4=';
|
||||
const STORAGE_KEY = 'fluxer:media_caps:v1';
|
||||
const PROBE_TIMEOUT_MS = 1500;
|
||||
|
||||
function buildUaKey(): string {
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : 'ssr';
|
||||
const version = Config.PUBLIC_BUILD_VERSION ?? 'dev';
|
||||
return `${version}:${ua}`;
|
||||
return typeof navigator !== 'undefined' ? navigator.userAgent : 'ssr';
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -67,7 +58,6 @@ function readCached(): CapabilityResult | null {
|
||||
return {
|
||||
avif: parsed.avif === true,
|
||||
webp: parsed.webp === true,
|
||||
apng: parsed.apng === true,
|
||||
jxl: parsed.jxl === true,
|
||||
probedAt: typeof parsed.probedAt === 'number' ? parsed.probedAt : 0,
|
||||
uaKey: parsed.uaKey,
|
||||
@@ -86,28 +76,16 @@ function writeCached(result: CapabilityResult): void {
|
||||
let cached: CapabilityResult | null = null;
|
||||
let inflight: Promise<CapabilityResult> | null = null;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function notify(): void {
|
||||
for (const fn of listeners) fn();
|
||||
}
|
||||
|
||||
async function runProbes(): Promise<CapabilityResult> {
|
||||
const [avif, webp, apng, jxl] = await Promise.all([
|
||||
probeOne(AVIF_PROBE),
|
||||
probeOne(WEBP_PROBE),
|
||||
probeOne(APNG_PROBE),
|
||||
probeOne(JXL_PROBE),
|
||||
]);
|
||||
return {avif, webp, apng, jxl, probedAt: Date.now(), uaKey: buildUaKey()};
|
||||
const [avif, webp, jxl] = await Promise.all([probeOne(AVIF_PROBE), probeOne(WEBP_PROBE), probeOne(JXL_PROBE)]);
|
||||
return {avif, webp, jxl, probedAt: Date.now(), uaKey: buildUaKey()};
|
||||
}
|
||||
|
||||
export async function probeMediaCapabilities(): Promise<CapabilityResult> {
|
||||
async function probeMediaCapabilities(): Promise<CapabilityResult> {
|
||||
if (cached) return cached;
|
||||
const fromStorage = readCached();
|
||||
if (fromStorage) {
|
||||
cached = fromStorage;
|
||||
notify();
|
||||
return fromStorage;
|
||||
}
|
||||
if (inflight) return inflight;
|
||||
@@ -115,65 +93,12 @@ export async function probeMediaCapabilities(): Promise<CapabilityResult> {
|
||||
const result = await runProbes();
|
||||
cached = result;
|
||||
writeCached(result);
|
||||
notify();
|
||||
inflight = null;
|
||||
return result;
|
||||
})();
|
||||
return inflight;
|
||||
}
|
||||
|
||||
export function getMediaCapabilitiesSync(): CapabilityResult | null {
|
||||
if (cached) return cached;
|
||||
cached = readCached();
|
||||
return cached;
|
||||
}
|
||||
|
||||
type CroppableAssetKind = 'avatar' | 'guild_icon' | 'banner' | 'splash' | 'embed_splash' | 'emoji' | 'sticker';
|
||||
|
||||
function pickAnimated(kind: CroppableAssetKind, caps: CapabilityResult): MediaFormat | null {
|
||||
switch (kind) {
|
||||
case 'emoji':
|
||||
case 'sticker':
|
||||
case 'avatar':
|
||||
case 'guild_icon':
|
||||
case 'banner':
|
||||
case 'splash':
|
||||
case 'embed_splash':
|
||||
if (caps.webp) return 'webp';
|
||||
if (caps.apng) return 'apng';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pickStatic(kind: CroppableAssetKind, caps: CapabilityResult): MediaFormat | null {
|
||||
switch (kind) {
|
||||
case 'avatar':
|
||||
case 'guild_icon':
|
||||
case 'banner':
|
||||
case 'splash':
|
||||
case 'embed_splash':
|
||||
if (caps.avif) return 'avif';
|
||||
if (caps.webp) return 'webp';
|
||||
return null;
|
||||
case 'emoji':
|
||||
case 'sticker':
|
||||
if (caps.webp) return 'webp';
|
||||
if (caps.avif) return 'avif';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const MediaCapabilities = {
|
||||
probe: probeMediaCapabilities,
|
||||
getSync: getMediaCapabilitiesSync,
|
||||
bestFormatFor(kind: CroppableAssetKind, animated: boolean): MediaFormat | null {
|
||||
const caps = getMediaCapabilitiesSync();
|
||||
if (!caps) return null;
|
||||
return animated ? pickAnimated(kind, caps) : pickStatic(kind, caps);
|
||||
},
|
||||
canCropFormat,
|
||||
};
|
||||
|
||||
export async function canCropFormat(mime: string): Promise<boolean> {
|
||||
const m = normalizeImageMimeType(mime);
|
||||
if (m === 'image/png' || m === 'image/apng' || m === 'image/jpeg' || m === 'image/gif') {
|
||||
@@ -203,22 +128,3 @@ async function canDecodeViaImage(mime: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function useCapabilityHint(): CapabilityResult | null {
|
||||
const subscribe = (cb: () => void): (() => void) => {
|
||||
listeners.add(cb);
|
||||
return () => {
|
||||
listeners.delete(cb);
|
||||
};
|
||||
};
|
||||
const getSnapshot = (): CapabilityResult | null => cached;
|
||||
const getServerSnapshot = (): CapabilityResult | null => null;
|
||||
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
||||
const [bootstrapped, setBootstrapped] = useState(value);
|
||||
useEffect(() => {
|
||||
if (!bootstrapped) {
|
||||
void probeMediaCapabilities().then((r) => setBootstrapped(r));
|
||||
}
|
||||
}, [bootstrapped]);
|
||||
return value ?? bootstrapped;
|
||||
}
|
||||
Reference in new issue
Block a user