polish: use indexed db to persist skin previews

This commit is contained in:
Calum H.
2025-05-24 18:50:51 +01:00
committed by Alejandro González
parent 0dd033d7f2
commit dd4f22767f
3 changed files with 161 additions and 1 deletions

View File

@@ -69,6 +69,8 @@ import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import { get_available_capes, get_available_skins } from './helpers/skins'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
const formatRelativeTime = useRelativeTime()
@@ -198,6 +200,14 @@ async function setupApp() {
get_opening_command().then(handleCommand)
checkUpdates()
fetchCredentials()
try {
const skins = (await get_available_skins()) ?? []
const capes = (await get_available_capes()) ?? []
generateSkinPreviews(skins, capes)
} catch (error) {
console.warn("Failed to generate skin previews in app setup.", error);
}
}
const stateFailed = ref(false)

View File

@@ -3,6 +3,7 @@ import type { Skin, Cape } from '../skins'
import { determineModelType } from '../skins'
import { reactive } from 'vue'
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
import { skinPreviewStorage } from '../storage/skin-preview-storage'
export interface RenderResult {
forwards: string
@@ -98,7 +99,6 @@ class BatchSkinRenderer {
this.scene.remove(this.currentModel)
}
// Use the utility function to setup the skin model
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
const group = new THREE.Group()
@@ -130,6 +130,21 @@ function getModelUrlForVariant(variant: string): string {
export const map = reactive(new Map<string, RenderResult>())
const DEBUG_MODE = false
export async function cleanupUnusedPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
const validKeys = new Set<string>()
for (const skin of skins) {
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
validKeys.add(key)
}
try {
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
} catch (error) {
console.warn('Failed to cleanup unused skin previews:', error)
}
}
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
const renderer = new BatchSkinRenderer()
const capeModelUrl = '/src/assets/models/cape.gltf'
@@ -147,6 +162,16 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
} else continue
}
try {
const cached = await skinPreviewStorage.retrieve(key)
if (cached) {
map.set(key, cached)
continue
}
} catch (error) {
console.warn('Failed to retrieve cached skin preview:', error)
}
let variant = skin.variant
if (variant === 'UNKNOWN') {
try {
@@ -167,8 +192,15 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
)
map.set(key, renderResult)
try {
await skinPreviewStorage.store(key, renderResult)
} catch (error) {
console.warn('Failed to store skin preview in persistent storage:', error)
}
}
} finally {
renderer.dispose()
await cleanupUnusedPreviews(skins, capes)
}
}

View File

@@ -0,0 +1,118 @@
import type { RenderResult } from '../rendering/batch-skin-renderer'
interface StoredPreview {
forwards: Blob
backwards: Blob
timestamp: number
}
export class SkinPreviewStorage {
private dbName = 'skin-previews'
private version = 1
private db: IDBDatabase | null = null
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains('previews')) {
db.createObjectStore('previews')
}
}
})
}
async store(key: string, result: RenderResult): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readwrite')
const store = transaction.objectStore('previews')
const forwardsBlob = await fetch(result.forwards).then(r => r.blob())
const backwardsBlob = await fetch(result.backwards).then(r => r.blob())
const storedPreview: StoredPreview = {
forwards: forwardsBlob,
backwards: backwardsBlob,
timestamp: Date.now()
}
return new Promise((resolve, reject) => {
const request = store.put(storedPreview, key)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
async retrieve(key: string): Promise<RenderResult | null> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly')
const store = transaction.objectStore('previews')
return new Promise((resolve, reject) => {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result as StoredPreview | undefined
if (!result) {
resolve(null)
return
}
const forwards = URL.createObjectURL(result.forwards)
const backwards = URL.createObjectURL(result.backwards)
resolve({ forwards, backwards })
}
request.onerror = () => reject(request.error)
})
}
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readwrite')
const store = transaction.objectStore('previews')
let deletedCount = 0
return new Promise((resolve, reject) => {
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const key = cursor.primaryKey as string
if (!validKeys.has(key)) {
const deleteRequest = cursor.delete()
deleteRequest.onsuccess = () => {
deletedCount++
}
deleteRequest.onerror = () => {
console.warn('Failed to delete invalid entry:', key)
}
}
cursor.continue()
} else {
resolve(deletedCount)
}
}
request.onerror = () => reject(request.error)
})
}
}
export const skinPreviewStorage = new SkinPreviewStorage()