fix(stage-ui-live2d,stage-tamagotchi): use temp file to prevent config loss and handle more Live2D model load failures (#1233)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { BaseIssue, BaseSchema, InferIssue, InferOutput } from 'valibot'
|
||||
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { copyFile, rename, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { safeDestr } from 'destr'
|
||||
@@ -74,7 +74,10 @@ export function createConfig<TSchema extends PersistedSchema>(
|
||||
|
||||
const save = throttle(async () => {
|
||||
try {
|
||||
await writeFile(configPath(), JSON.stringify(persistenceMap.get(key)))
|
||||
const path = configPath()
|
||||
const tmpPath = `${path}.tmp`
|
||||
await writeFile(tmpPath, JSON.stringify(persistenceMap.get(key)))
|
||||
await rename(tmpPath, path)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to save config', error)
|
||||
@@ -83,7 +86,11 @@ export function createConfig<TSchema extends PersistedSchema>(
|
||||
|
||||
const writeHealingConfig = async (value: InferOutput<TSchema>) => {
|
||||
try {
|
||||
await writeFile(configPath(), JSON.stringify(value))
|
||||
const path = configPath()
|
||||
if (existsSync(path)) {
|
||||
await copyFile(path, `${path}.bak`).catch(err => console.warn('Failed to create backup for config:', path, err))
|
||||
}
|
||||
await writeFile(path, JSON.stringify(value))
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@@ -61,6 +61,7 @@ const props = withDefaults(defineProps<{
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'modelLoaded'): void
|
||||
(e: 'error', error: Error): void
|
||||
}>()
|
||||
|
||||
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
|
||||
@@ -369,6 +370,10 @@ async function loadModel() {
|
||||
|
||||
emits('modelLoaded')
|
||||
}
|
||||
catch (error) {
|
||||
console.error('[Live2D] Failed to load model:', error)
|
||||
emits('error', error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
finally {
|
||||
modelLoading.value = false
|
||||
componentState.value = 'mounted'
|
||||
|
||||
@@ -12,6 +12,18 @@ declare global {
|
||||
}
|
||||
|
||||
export class OPFSCache {
|
||||
static async clearAll(): Promise<void> {
|
||||
try {
|
||||
const root = await navigator.storage.getDirectory()
|
||||
for await (const entry of root.values()) {
|
||||
await root.removeEntry(entry.name, { recursive: true })
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[OPFS] Failed to clear cache:', e)
|
||||
}
|
||||
}
|
||||
|
||||
static async readDirectoryRecursive(dir: FileSystemDirectoryHandle, pathPrefix: string): Promise<File[]> {
|
||||
const files: File[] = []
|
||||
for await (const entry of dir.values()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { defaultModelParameters, useLive2d } from '@proj-airi/stage-ui-live2d'
|
||||
import { OPFSCache } from '@proj-airi/stage-ui-live2d/utils/opfs-loader'
|
||||
import { Button, Checkbox, FieldRange, SelectTab } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -81,6 +82,18 @@ function resetToDefaultParameters() {
|
||||
modelParameters.value = { ...defaultModelParameters }
|
||||
}
|
||||
|
||||
const clearingCache = ref(false)
|
||||
|
||||
async function clearModelCache() {
|
||||
clearingCache.value = true
|
||||
try {
|
||||
await OPFSCache.clearAll()
|
||||
}
|
||||
finally {
|
||||
clearingCache.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Runtime motion selection handlers
|
||||
function handleMotionSelect(motion: any) {
|
||||
selectedRuntimeMotion.value = motion.displayPath // Store full path
|
||||
@@ -375,6 +388,14 @@ onUnmounted(() => {
|
||||
Reset To Default Parameters
|
||||
</button>
|
||||
|
||||
<button
|
||||
mt-2 w-full border rounded bg-neutral-100 px-4 py-2 text-sm text-neutral-700 font-medium transition-colors dark:border-neutral-700 dark:bg-neutral-800 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700
|
||||
:disabled="clearingCache"
|
||||
@click="clearModelCache"
|
||||
>
|
||||
{{ clearingCache ? 'Clearing...' : 'Clear Model Cache' }}
|
||||
</button>
|
||||
|
||||
<!-- Head Rotation -->
|
||||
<div mb-2 mt-4 text-xs text-neutral-500 font-semibold dark:text-neutral-400>
|
||||
Head Rotation
|
||||
|
||||
Reference in New Issue
Block a user