完善浏览器配置库与未保存提示
- 新增 IndexedDB 配置保存、加载、删除及批量导入导出 - 配置列表支持选中、双击加载和悬浮操作提示 - 新增未保存提示设置,默认关闭并持久化到浏览器 - 优化配置管理界面并同步九种语言文案 - 更新开发文档与生产构建产物
This commit is contained in:
1 parent
8bd9154414
commit
46083c8d2a
30 files changed
+2244
-1293
No files matched your search
@@ -29,7 +29,7 @@ interface EditorSnapshot {
|
||||
spineAssets: Map<number, LoadedSpineAsset>
|
||||
}
|
||||
|
||||
export const editorHistoryState = reactive({ undoCount: 0 })
|
||||
export const editorHistoryState = reactive({ undoCount: 0, dirty: false })
|
||||
|
||||
let managedStore: EditorStore | null = null
|
||||
let currentSnapshot: EditorSnapshot | null = null
|
||||
@@ -37,6 +37,7 @@ let undoStack: EditorSnapshot[] = []
|
||||
let stopHistoryWatch: WatchStopHandle | null = null
|
||||
let commitTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let applyingSnapshot = false
|
||||
let savedSignature = ''
|
||||
|
||||
function captureData(store: EditorStore): EditorSnapshotData {
|
||||
const systems = store.systems.map((system) => {
|
||||
@@ -95,8 +96,9 @@ function commitCurrentChange() {
|
||||
trimUndoStack(store.settings.undoLimit)
|
||||
}
|
||||
|
||||
function scheduleCommit() {
|
||||
function scheduleCommit(signature: string) {
|
||||
if (applyingSnapshot) return
|
||||
editorHistoryState.dirty = signature !== savedSignature
|
||||
if (commitTimer) clearTimeout(commitTimer)
|
||||
commitTimer = setTimeout(() => {
|
||||
commitTimer = null
|
||||
@@ -157,6 +159,7 @@ function restoreSnapshot(store: EditorStore, target: EditorSnapshot) {
|
||||
repairSelection(store)
|
||||
store.recalcTotalFrames()
|
||||
currentSnapshot = target
|
||||
editorHistoryState.dirty = target.signature !== savedSignature
|
||||
} finally {
|
||||
applyingSnapshot = false
|
||||
releaseSnapshot(previousSnapshot)
|
||||
@@ -172,7 +175,9 @@ export function installEditorHistory(store: EditorStore) {
|
||||
undoStack = []
|
||||
managedStore = store
|
||||
currentSnapshot = createSnapshot(store)
|
||||
savedSignature = currentSnapshot.signature
|
||||
editorHistoryState.undoCount = 0
|
||||
editorHistoryState.dirty = false
|
||||
stopHistoryWatch = watch(
|
||||
() => dataSignature(captureData(store)),
|
||||
scheduleCommit,
|
||||
@@ -180,6 +185,12 @@ export function installEditorHistory(store: EditorStore) {
|
||||
)
|
||||
}
|
||||
|
||||
export function markEditorSaved(store: EditorStore) {
|
||||
if (managedStore !== store) return
|
||||
savedSignature = dataSignature(captureData(store))
|
||||
editorHistoryState.dirty = false
|
||||
}
|
||||
|
||||
export function setUndoLimit(store: EditorStore, value: number) {
|
||||
const limit = Math.min(200, Math.max(1, Math.round(Number(value) || 20)))
|
||||
store.settings.undoLimit = limit
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createZipArchive } from '../export/zipArchive'
|
||||
import { translate } from '../i18n'
|
||||
|
||||
const DATABASE_NAME = 'SpineParticle.ProjectLibrary'
|
||||
const STORE_NAME = 'projects'
|
||||
const DATABASE_VERSION = 1
|
||||
|
||||
export const PROJECT_SAVE_EVENT = 'spine-particle:request-save'
|
||||
|
||||
export interface LocalProject {
|
||||
name: string
|
||||
text: string
|
||||
savedAt: string
|
||||
size: number
|
||||
}
|
||||
|
||||
let databasePromise: Promise<IDBDatabase> | null = null
|
||||
|
||||
function openDatabase() {
|
||||
if (!('indexedDB' in globalThis)) return Promise.reject(new Error(translate('config.storageUnavailable')))
|
||||
if (databasePromise) return databasePromise
|
||||
databasePromise = new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION)
|
||||
request.onupgradeneeded = () => {
|
||||
const database = request.result
|
||||
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
||||
database.createObjectStore(STORE_NAME, { keyPath: 'name' })
|
||||
}
|
||||
}
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error || new Error(translate('config.storageUnavailable')))
|
||||
})
|
||||
return databasePromise
|
||||
}
|
||||
|
||||
function requestResult<T>(request: IDBRequest<T>) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error || new Error(translate('config.storageUnavailable')))
|
||||
})
|
||||
}
|
||||
|
||||
function transactionDone(transaction: IDBTransaction) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
transaction.oncomplete = () => resolve()
|
||||
transaction.onerror = () => reject(transaction.error || new Error(translate('config.storageUnavailable')))
|
||||
transaction.onabort = () => reject(transaction.error || new Error(translate('config.storageUnavailable')))
|
||||
})
|
||||
}
|
||||
|
||||
export function normalizeProjectName(value: string) {
|
||||
const name = value.trim().replace(/\.json$/i, '').trim()
|
||||
if (!name) throw new Error(translate('config.local.nameRequired'))
|
||||
if (/[\\/:*?"<>|\u0000-\u001f]/.test(name)) throw new Error(translate('config.local.nameInvalid'))
|
||||
return name
|
||||
}
|
||||
|
||||
export function projectJsonFileName(name: string) {
|
||||
return `${name}.json`
|
||||
}
|
||||
|
||||
export async function listLocalProjects() {
|
||||
const database = await openDatabase()
|
||||
const transaction = database.transaction(STORE_NAME, 'readonly')
|
||||
const projects = await requestResult(transaction.objectStore(STORE_NAME).getAll() as IDBRequest<LocalProject[]>)
|
||||
return projects.sort((a, b) => b.savedAt.localeCompare(a.savedAt))
|
||||
}
|
||||
|
||||
export async function getLocalProject(name: string) {
|
||||
const database = await openDatabase()
|
||||
const transaction = database.transaction(STORE_NAME, 'readonly')
|
||||
return requestResult(transaction.objectStore(STORE_NAME).get(name) as IDBRequest<LocalProject | undefined>)
|
||||
}
|
||||
|
||||
export async function saveLocalProject(name: string, text: string) {
|
||||
const normalizedName = normalizeProjectName(name)
|
||||
const project: LocalProject = {
|
||||
name: normalizedName,
|
||||
text,
|
||||
savedAt: new Date().toISOString(),
|
||||
size: new Blob([text]).size,
|
||||
}
|
||||
const database = await openDatabase()
|
||||
const transaction = database.transaction(STORE_NAME, 'readwrite')
|
||||
transaction.objectStore(STORE_NAME).put(project)
|
||||
await transactionDone(transaction)
|
||||
return project
|
||||
}
|
||||
|
||||
export async function deleteLocalProject(name: string) {
|
||||
const database = await openDatabase()
|
||||
const transaction = database.transaction(STORE_NAME, 'readwrite')
|
||||
transaction.objectStore(STORE_NAME).delete(name)
|
||||
await transactionDone(transaction)
|
||||
}
|
||||
|
||||
export function downloadTextFile(text: string, fileName: string) {
|
||||
const blob = new Blob([text], { type: 'application/json;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.style.display = 'none'
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
anchor.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0)
|
||||
}
|
||||
|
||||
export function downloadProjectsArchive(projects: LocalProject[], fileName: string) {
|
||||
const encoder = new TextEncoder()
|
||||
const archive = createZipArchive(projects.map((project) => ({
|
||||
name: projectJsonFileName(project.name),
|
||||
data: encoder.encode(project.text),
|
||||
})))
|
||||
const url = URL.createObjectURL(archive)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.style.display = 'none'
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
anchor.remove()
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export type SystemSettingsState = Pick<SettingsState,
|
||||
| 'axisFontSize'
|
||||
| 'tickWidth'
|
||||
| 'showAxisLabels'
|
||||
| 'unsavedChangesPrompt'
|
||||
| 'axisColor'
|
||||
>
|
||||
|
||||
@@ -48,6 +49,7 @@ export function captureSystemSettings(settings: SettingsState): SystemSettingsSt
|
||||
axisFontSize: settings.axisFontSize,
|
||||
tickWidth: settings.tickWidth,
|
||||
showAxisLabels: settings.showAxisLabels,
|
||||
unsavedChangesPrompt: settings.unsavedChangesPrompt,
|
||||
axisColor: settings.axisColor,
|
||||
}
|
||||
}
|
||||
@@ -71,6 +73,7 @@ function normalizedSystemSettings(value: Partial<SystemSettingsState>, current:
|
||||
axisFontSize: Math.round(clamp(value.axisFontSize, 8, 40, current.axisFontSize)),
|
||||
tickWidth: clamp(value.tickWidth, 1, 5, current.tickWidth),
|
||||
showAxisLabels: booleanValue('showAxisLabels'),
|
||||
unsavedChangesPrompt: booleanValue('unsavedChangesPrompt'),
|
||||
axisColor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "Dicke der Teilstriche",
|
||||
"axisColor": "Achsenfarbe",
|
||||
"showAxisLabels": "Skalenwert anzeigen",
|
||||
"unsavedPrompt": "Warnung bei ungespeicherten Änderungen",
|
||||
"cacheNote": "Die gesamten Systemeinstellungen werden automatisch im aktuellen Browser gespeichert"
|
||||
},
|
||||
"config": {
|
||||
"title": "Konfiguration",
|
||||
"save": "Konfiguration speichern",
|
||||
"saveTitle": "Alle Editorkonfigurationen speichern (Ctrl+S)",
|
||||
"saveTitle": "Konfiguration im Browser-Cache speichern",
|
||||
"load": "Konfiguration laden",
|
||||
"loadTitle": "Lokale Editorkonfiguration laden",
|
||||
"presets": "Voreinstellungsliste",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "Standardressourcen werden analysiert und wiederhergestellt...",
|
||||
"presetComplete": "Das Laden der Voreinstellungen ist abgeschlossen",
|
||||
"presetListError": "Voreinstellungsliste kann nicht gelesen werden",
|
||||
"presetReadError": "Voreinstellung konnte nicht gelesen werden: {file}"
|
||||
"presetReadError": "Voreinstellung konnte nicht gelesen werden: {file}",
|
||||
"local": {
|
||||
"listTitle": "Konfigurationsliste",
|
||||
"empty": "Keine Konfigurationen in diesem Browser gespeichert",
|
||||
"saveTitle": "Konfiguration im Browser-Cache speichern",
|
||||
"name": "Konfigurationsname",
|
||||
"namePlaceholder": "Zum Beispiel: Feuerwerkseffekt",
|
||||
"load": "Laden",
|
||||
"loadHint": "Doppelklicken zum Laden",
|
||||
"download": "Herunterladen",
|
||||
"delete": "Löschen",
|
||||
"downloadCurrent": "Aktuelle Konfiguration herunterladen",
|
||||
"downloadAll": "Alle Konfigurationen herunterladen",
|
||||
"loadAll": "Alle Konfigurationen laden",
|
||||
"loadAllConfirm": "{count} Konfigurationsdateien importieren? Gleichnamige Konfigurationen werden überschrieben.",
|
||||
"loadAllDone": "{count} Konfigurationen aus Dateien geladen",
|
||||
"loadAllPartial": "{imported} Konfigurationen importiert; {failed} Dateien fehlgeschlagen",
|
||||
"overwriteSave": "„{name}“ existiert bereits. Überschreiben?",
|
||||
"loadOverwrite": "Beim Laden von „{name}“ wird die aktuelle Szene ersetzt und ungespeicherte Änderungen gehen verloren. Fortfahren?",
|
||||
"deleteConfirm": "„{name}“ löschen?",
|
||||
"nameRequired": "Geben Sie einen Konfigurationsnamen ein",
|
||||
"nameInvalid": "Der Name darf \\ / : * ? \" < > | nicht enthalten",
|
||||
"storageUnavailable": "Der lokale Browserspeicher ist nicht verfügbar",
|
||||
"saved": "Im Browser gespeichert: {name}",
|
||||
"loaded": "Aus dem Browser geladen: {name}",
|
||||
"deleted": "Browserkonfiguration gelöscht: {name}",
|
||||
"downloadEmpty": "Keine Konfigurationen zum Herunterladen",
|
||||
"downloaded": "Heruntergeladen: {name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "Ziehen Sie den quadratischen Griff, um den Bogen anzupassen. · Doppelklicken Sie auf eine leere Stelle, um einen neuen zu erstellen. · Doppelklicken Sie auf einen Knoten, um ihn zu löschen",
|
||||
|
||||
@@ -87,12 +87,13 @@
|
||||
"tickWidth": "Tick line width",
|
||||
"axisColor": "Axis color",
|
||||
"showAxisLabels": "Show tick values",
|
||||
"unsavedPrompt": "Prompt for unsaved changes",
|
||||
"cacheNote": "Settings are saved automatically in this browser"
|
||||
},
|
||||
"config": {
|
||||
"title": "Configuration",
|
||||
"save": "Save Config",
|
||||
"saveTitle": "Save the complete editor configuration (Ctrl+S)",
|
||||
"saveTitle": "Save configuration to browser cache",
|
||||
"load": "Load Config",
|
||||
"loadTitle": "Load an editor configuration file",
|
||||
"presets": "Preset list",
|
||||
@@ -110,7 +111,35 @@
|
||||
"presetRestoring": "Parsing and restoring preset resources…",
|
||||
"presetComplete": "Preset loaded",
|
||||
"presetListError": "Unable to read preset list",
|
||||
"presetReadError": "Unable to read preset: {file}"
|
||||
"presetReadError": "Unable to read preset: {file}",
|
||||
"local": {
|
||||
"listTitle": "Configuration List",
|
||||
"empty": "No configurations saved in this browser",
|
||||
"saveTitle": "Save configuration to browser cache",
|
||||
"name": "Configuration name",
|
||||
"namePlaceholder": "For example: Firework effect",
|
||||
"load": "Load",
|
||||
"loadHint": "Double-click to load",
|
||||
"download": "Download",
|
||||
"delete": "Delete",
|
||||
"downloadCurrent": "Download Current Configuration",
|
||||
"downloadAll": "Download All Configurations",
|
||||
"loadAll": "Load All Configurations",
|
||||
"loadAllConfirm": "Import {count} configuration files? Existing configurations with the same names will be overwritten.",
|
||||
"loadAllDone": "Loaded {count} configurations from files",
|
||||
"loadAllPartial": "Imported {imported} configurations; {failed} files failed",
|
||||
"overwriteSave": "“{name}” already exists. Overwrite it?",
|
||||
"loadOverwrite": "Loading “{name}” will replace the current scene and discard unsaved changes. Continue?",
|
||||
"deleteConfirm": "Delete “{name}”?",
|
||||
"nameRequired": "Enter a configuration name",
|
||||
"nameInvalid": "The name cannot contain \\ / : * ? \" < > |",
|
||||
"storageUnavailable": "Browser local storage is unavailable",
|
||||
"saved": "Saved to browser: {name}",
|
||||
"loaded": "Loaded from browser: {name}",
|
||||
"deleted": "Deleted browser configuration: {name}",
|
||||
"downloadEmpty": "No configurations to download",
|
||||
"downloaded": "Downloaded: {name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "Drag square handles · Double-click empty space to add · Double-click a node to delete",
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "espesor de la marca de verificación",
|
||||
"axisColor": "color del eje",
|
||||
"showAxisLabels": "Mostrar valor de escala",
|
||||
"unsavedPrompt": "Avisar sobre cambios sin guardar",
|
||||
"cacheNote": "Toda la configuración del sistema se guarda automáticamente en el navegador actual."
|
||||
},
|
||||
"config": {
|
||||
"title": "Gestión de configuración",
|
||||
"save": "Guardar configuración",
|
||||
"saveTitle": "Guarde todas las configuraciones del editor (Ctrl+S)",
|
||||
"saveTitle": "Guardar configuración en la caché del navegador",
|
||||
"load": "Cargar configuración",
|
||||
"loadTitle": "Cargar la configuración del editor local",
|
||||
"presets": "Lista preestablecida",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "Analizando y restaurando recursos predeterminados...",
|
||||
"presetComplete": "Carga preestablecida completada",
|
||||
"presetListError": "No se puede leer la lista preestablecida",
|
||||
"presetReadError": "No se puede leer el ajuste preestablecido: {file}"
|
||||
"presetReadError": "No se puede leer el ajuste preestablecido: {file}",
|
||||
"local": {
|
||||
"listTitle": "Lista de configuraciones",
|
||||
"empty": "No hay configuraciones guardadas en este navegador",
|
||||
"saveTitle": "Guardar configuración en la caché del navegador",
|
||||
"name": "Nombre de la configuración",
|
||||
"namePlaceholder": "Por ejemplo: Efecto de fuegos artificiales",
|
||||
"load": "Cargar",
|
||||
"loadHint": "Doble clic para cargar",
|
||||
"download": "Descargar",
|
||||
"delete": "Eliminar",
|
||||
"downloadCurrent": "Descargar configuración actual",
|
||||
"downloadAll": "Descargar todas las configuraciones",
|
||||
"loadAll": "Cargar todas las configuraciones",
|
||||
"loadAllConfirm": "¿Importar {count} archivos de configuración? Las configuraciones con el mismo nombre se sobrescribirán.",
|
||||
"loadAllDone": "{count} configuraciones cargadas desde archivos",
|
||||
"loadAllPartial": "{imported} configuraciones importadas; {failed} archivos fallaron",
|
||||
"overwriteSave": "«{name}» ya existe. ¿Sobrescribir?",
|
||||
"loadOverwrite": "Cargar «{name}» sustituirá la escena actual y descartará los cambios no guardados. ¿Continuar?",
|
||||
"deleteConfirm": "¿Eliminar «{name}»?",
|
||||
"nameRequired": "Introduce un nombre de configuración",
|
||||
"nameInvalid": "El nombre no puede contener \\ / : * ? \" < > |",
|
||||
"storageUnavailable": "El almacenamiento local del navegador no está disponible",
|
||||
"saved": "Guardado en el navegador: {name}",
|
||||
"loaded": "Cargado desde el navegador: {name}",
|
||||
"deleted": "Configuración eliminada del navegador: {name}",
|
||||
"downloadEmpty": "No hay configuraciones para descargar",
|
||||
"downloaded": "Descargado: {name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "Arrastre el controlador cuadrado para ajustar el arco · Haga doble clic en un espacio en blanco para crear uno nuevo · Haga doble clic en un nodo para eliminarlo",
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "épaisseur de la graduation",
|
||||
"axisColor": "couleur de l'axe",
|
||||
"showAxisLabels": "Afficher la valeur de l'échelle",
|
||||
"unsavedPrompt": "Avertir des modifications non enregistrées",
|
||||
"cacheNote": "L'ensemble des paramètres du système sont automatiquement enregistrés dans le navigateur actuel"
|
||||
},
|
||||
"config": {
|
||||
"title": "Configuration",
|
||||
"save": "Enregistrer",
|
||||
"saveTitle": "Enregistrez toutes les configurations de l'éditeur (Ctrl+S)",
|
||||
"saveTitle": "Enregistrer la configuration dans le cache du navigateur",
|
||||
"load": "Charger",
|
||||
"loadTitle": "Charger la configuration de l'éditeur local",
|
||||
"presets": "Liste des préréglages",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "Analyse et restauration des ressources par défaut...",
|
||||
"presetComplete": "Chargement du préréglage terminé",
|
||||
"presetListError": "Impossible de lire la liste des préréglages",
|
||||
"presetReadError": "Impossible de lire le préréglage : {file}"
|
||||
"presetReadError": "Impossible de lire le préréglage : {file}",
|
||||
"local": {
|
||||
"listTitle": "Liste des configurations",
|
||||
"empty": "Aucune configuration enregistrée dans ce navigateur",
|
||||
"saveTitle": "Enregistrer la configuration dans le cache du navigateur",
|
||||
"name": "Nom de la configuration",
|
||||
"namePlaceholder": "Par exemple : Effet de feux d’artifice",
|
||||
"load": "Charger",
|
||||
"loadHint": "Double-cliquez pour charger",
|
||||
"download": "Télécharger",
|
||||
"delete": "Supprimer",
|
||||
"downloadCurrent": "Télécharger la configuration actuelle",
|
||||
"downloadAll": "Télécharger toutes les configurations",
|
||||
"loadAll": "Charger toutes les configurations",
|
||||
"loadAllConfirm": "Importer {count} fichiers de configuration ? Les configurations portant le même nom seront écrasées.",
|
||||
"loadAllDone": "{count} configurations chargées depuis les fichiers",
|
||||
"loadAllPartial": "{imported} configurations importées ; {failed} fichiers en échec",
|
||||
"overwriteSave": "« {name} » existe déjà. L’écraser ?",
|
||||
"loadOverwrite": "Le chargement de « {name} » remplacera la scène actuelle et perdra les modifications non enregistrées. Continuer ?",
|
||||
"deleteConfirm": "Supprimer « {name} » ?",
|
||||
"nameRequired": "Saisissez un nom de configuration",
|
||||
"nameInvalid": "Le nom ne peut pas contenir \\ / : * ? \" < > |",
|
||||
"storageUnavailable": "Le stockage local du navigateur est indisponible",
|
||||
"saved": "Enregistré dans le navigateur : {name}",
|
||||
"loaded": "Chargé depuis le navigateur : {name}",
|
||||
"deleted": "Configuration supprimée du navigateur : {name}",
|
||||
"downloadEmpty": "Aucune configuration à télécharger",
|
||||
"downloaded": "Téléchargé : {name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "Faites glisser la poignée carrée pour ajuster l'arc · Double-cliquez sur un espace vide pour en créer un nouveau · Double-cliquez sur un nœud pour le supprimer",
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "目盛りの太さ",
|
||||
"axisColor": "軸の色",
|
||||
"showAxisLabels": "表示スケール値",
|
||||
"unsavedPrompt": "未保存の変更を確認",
|
||||
"cacheNote": "システム設定全体が現在のブラウザに自動的に保存されます"
|
||||
},
|
||||
"config": {
|
||||
"title": "設定管理",
|
||||
"save": "設定を保存",
|
||||
"saveTitle": "すべてのエディタ設定を保存する (Ctrl+S)",
|
||||
"saveTitle": "設定をブラウザキャッシュに保存",
|
||||
"load": "設定を読み込む",
|
||||
"loadTitle": "ローカルエディタ設定をロードする",
|
||||
"presets": "プリセットリスト",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "デフォルトのリソースを解析して復元しています...",
|
||||
"presetComplete": "プリセットのロードが完了しました",
|
||||
"presetListError": "プリセットリストを読み取れません",
|
||||
"presetReadError": "プリセットを読み取れません: {file}"
|
||||
"presetReadError": "プリセットを読み取れません: {file}",
|
||||
"local": {
|
||||
"listTitle": "設定一覧",
|
||||
"empty": "このブラウザに保存された設定はありません",
|
||||
"saveTitle": "設定をブラウザキャッシュに保存",
|
||||
"name": "設定名",
|
||||
"namePlaceholder": "例:花火エフェクト",
|
||||
"load": "読み込む",
|
||||
"loadHint": "ダブルクリックで読み込む",
|
||||
"download": "ダウンロード",
|
||||
"delete": "削除",
|
||||
"downloadCurrent": "現在の設定をダウンロード",
|
||||
"downloadAll": "すべての設定をダウンロード",
|
||||
"loadAll": "すべての設定を読み込む",
|
||||
"loadAllConfirm": "{count} 個の設定ファイルを読み込みます。同名の設定は上書きされます。続行しますか?",
|
||||
"loadAllDone": "ファイルから {count} 個の設定を読み込みました",
|
||||
"loadAllPartial": "{imported} 個の設定を読み込み、{failed} 個のファイルが失敗しました",
|
||||
"overwriteSave": "「{name}」は既に存在します。上書きしますか?",
|
||||
"loadOverwrite": "「{name}」を読み込むと現在のシーンが置き換えられ、未保存の変更は失われます。続行しますか?",
|
||||
"deleteConfirm": "「{name}」を削除しますか?",
|
||||
"nameRequired": "設定名を入力してください",
|
||||
"nameInvalid": "設定名に \\ / : * ? \" < > | は使用できません",
|
||||
"storageUnavailable": "ブラウザのローカルストレージを利用できません",
|
||||
"saved": "ブラウザに保存しました:{name}",
|
||||
"loaded": "ブラウザから読み込みました:{name}",
|
||||
"deleted": "ブラウザの設定を削除しました:{name}",
|
||||
"downloadEmpty": "ダウンロードできる設定がありません",
|
||||
"downloaded": "ダウンロードしました:{name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "四角いハンドルをドラッグして円弧を調整します。 · 空白スペースをダブルクリックして新しいスペースを作成します。 · ノードをダブルクリックして削除します。",
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "눈금 표시 두께",
|
||||
"axisColor": "축 색상",
|
||||
"showAxisLabels": "표시 스케일 값",
|
||||
"unsavedPrompt": "저장되지 않은 변경 확인",
|
||||
"cacheNote": "전체 시스템 설정이 현재 브라우저에 자동으로 저장됩니다."
|
||||
},
|
||||
"config": {
|
||||
"title": "설정 관리",
|
||||
"save": "설정 저장",
|
||||
"saveTitle": "모든 편집기 구성 저장(Ctrl+S)",
|
||||
"saveTitle": "브라우저 캐시에 설정 저장",
|
||||
"load": "설정 불러오기",
|
||||
"loadTitle": "로컬 편집기 구성 로드",
|
||||
"presets": "프리셋 목록",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "기본 리소스를 구문 분석하고 복원하는 중...",
|
||||
"presetComplete": "프리셋 로딩 완료",
|
||||
"presetListError": "사전 설정 목록을 읽을 수 없습니다.",
|
||||
"presetReadError": "사전 설정을 읽을 수 없습니다: {file}"
|
||||
"presetReadError": "사전 설정을 읽을 수 없습니다: {file}",
|
||||
"local": {
|
||||
"listTitle": "설정 목록",
|
||||
"empty": "이 브라우저에 저장된 설정이 없습니다",
|
||||
"saveTitle": "브라우저 캐시에 설정 저장",
|
||||
"name": "설정 이름",
|
||||
"namePlaceholder": "예: 폭죽 효과",
|
||||
"load": "불러오기",
|
||||
"loadHint": "더블클릭하여 불러오기",
|
||||
"download": "다운로드",
|
||||
"delete": "삭제",
|
||||
"downloadCurrent": "현재 설정 다운로드",
|
||||
"downloadAll": "모든 설정 다운로드",
|
||||
"loadAll": "모든 설정 불러오기",
|
||||
"loadAllConfirm": "설정 파일 {count}개를 가져옵니다. 같은 이름의 설정은 덮어씁니다. 계속할까요?",
|
||||
"loadAllDone": "파일에서 설정 {count}개를 불러왔습니다",
|
||||
"loadAllPartial": "설정 {imported}개를 가져왔고 {failed}개 파일이 실패했습니다",
|
||||
"overwriteSave": "'{name}'이(가) 이미 있습니다. 덮어쓸까요?",
|
||||
"loadOverwrite": "'{name}'을(를) 불러오면 현재 장면이 교체되고 저장하지 않은 변경 사항이 사라집니다. 계속할까요?",
|
||||
"deleteConfirm": "'{name}'을(를) 삭제할까요?",
|
||||
"nameRequired": "설정 이름을 입력하세요",
|
||||
"nameInvalid": "설정 이름에는 \\ / : * ? \" < > | 문자를 사용할 수 없습니다",
|
||||
"storageUnavailable": "브라우저 로컬 저장소를 사용할 수 없습니다",
|
||||
"saved": "브라우저에 저장했습니다: {name}",
|
||||
"loaded": "브라우저에서 불러왔습니다: {name}",
|
||||
"deleted": "브라우저 설정을 삭제했습니다: {name}",
|
||||
"downloadEmpty": "다운로드할 설정이 없습니다",
|
||||
"downloaded": "다운로드했습니다: {name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "원호를 조정하려면 사각형 핸들을 드래그하세요. · 새 공간을 만들려면 빈 공간을 두 번 클릭하세요. · 삭제하려면 노드를 두 번 클릭하세요.",
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "espessura da marca",
|
||||
"axisColor": "cor do eixo",
|
||||
"showAxisLabels": "Exibir valor da escala",
|
||||
"unsavedPrompt": "Avisar sobre alterações não salvas",
|
||||
"cacheNote": "Todas as configurações do sistema são salvas automaticamente no navegador atual"
|
||||
},
|
||||
"config": {
|
||||
"title": "Gerenciamento de configuração",
|
||||
"save": "Salvar configuração",
|
||||
"saveTitle": "Salve todas as configurações do editor (Ctrl+S)",
|
||||
"saveTitle": "Salvar configuração no cache do navegador",
|
||||
"load": "Carregar configuração",
|
||||
"loadTitle": "Carregar configuração do editor local",
|
||||
"presets": "Lista predefinida",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "Analisando e restaurando recursos padrão...",
|
||||
"presetComplete": "Carregamento predefinido concluído",
|
||||
"presetListError": "Não é possível ler a lista predefinida",
|
||||
"presetReadError": "Não é possível ler a predefinição: {file}"
|
||||
"presetReadError": "Não é possível ler a predefinição: {file}",
|
||||
"local": {
|
||||
"listTitle": "Lista de configurações",
|
||||
"empty": "Nenhuma configuração salva neste navegador",
|
||||
"saveTitle": "Salvar configuração no cache do navegador",
|
||||
"name": "Nome da configuração",
|
||||
"namePlaceholder": "Por exemplo: Efeito de fogos",
|
||||
"load": "Carregar",
|
||||
"loadHint": "Clique duplo para carregar",
|
||||
"download": "Baixar",
|
||||
"delete": "Excluir",
|
||||
"downloadCurrent": "Baixar configuração atual",
|
||||
"downloadAll": "Baixar todas as configurações",
|
||||
"loadAll": "Carregar todas as configurações",
|
||||
"loadAllConfirm": "Importar {count} arquivos de configuração? Configurações com o mesmo nome serão substituídas.",
|
||||
"loadAllDone": "{count} configurações carregadas dos arquivos",
|
||||
"loadAllPartial": "{imported} configurações importadas; {failed} arquivos falharam",
|
||||
"overwriteSave": "“{name}” já existe. Substituir?",
|
||||
"loadOverwrite": "Carregar “{name}” substituirá a cena atual e descartará alterações não salvas. Continuar?",
|
||||
"deleteConfirm": "Excluir “{name}”?",
|
||||
"nameRequired": "Digite um nome de configuração",
|
||||
"nameInvalid": "O nome não pode conter \\ / : * ? \" < > |",
|
||||
"storageUnavailable": "O armazenamento local do navegador não está disponível",
|
||||
"saved": "Salvo no navegador: {name}",
|
||||
"loaded": "Carregado do navegador: {name}",
|
||||
"deleted": "Configuração excluída do navegador: {name}",
|
||||
"downloadEmpty": "Nenhuma configuração para baixar",
|
||||
"downloaded": "Baixado: {name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "Arraste a alça quadrada para ajustar o arco · Clique duas vezes em um espaço em branco para criar um novo · Clique duas vezes em um nó para excluí-lo",
|
||||
|
||||
@@ -87,12 +87,13 @@
|
||||
"tickWidth": "刻度线粗细",
|
||||
"axisColor": "坐标轴颜色",
|
||||
"showAxisLabels": "显示刻度数值",
|
||||
"unsavedPrompt": "未保存提示",
|
||||
"cacheNote": "整个系统设置会自动保存到当前浏览器"
|
||||
},
|
||||
"config": {
|
||||
"title": "配置管理",
|
||||
"save": "保存配置",
|
||||
"saveTitle": "保存全部编辑器配置 (Ctrl+S)",
|
||||
"saveTitle": "保存配置到浏览器缓存",
|
||||
"load": "加载配置",
|
||||
"loadTitle": "加载本地编辑器配置",
|
||||
"presets": "预设列表",
|
||||
@@ -110,7 +111,35 @@
|
||||
"presetRestoring": "正在解析并恢复预设资源…",
|
||||
"presetComplete": "预设加载完成",
|
||||
"presetListError": "无法读取预设列表",
|
||||
"presetReadError": "无法读取预设:{file}"
|
||||
"presetReadError": "无法读取预设:{file}",
|
||||
"local": {
|
||||
"listTitle": "配置列表",
|
||||
"empty": "浏览器中还没有保存的配置",
|
||||
"saveTitle": "保存配置到浏览器缓存",
|
||||
"name": "配置名称",
|
||||
"namePlaceholder": "例如:烟花效果",
|
||||
"load": "加载",
|
||||
"loadHint": "双击以加载",
|
||||
"download": "下载",
|
||||
"delete": "删除",
|
||||
"downloadCurrent": "下载当前配置",
|
||||
"downloadAll": "下载所有配置",
|
||||
"loadAll": "加载所有配置",
|
||||
"loadAllConfirm": "将导入 {count} 个配置文件,同名配置会被覆盖。是否继续?",
|
||||
"loadAllDone": "已从文件加载 {count} 个配置",
|
||||
"loadAllPartial": "已导入 {imported} 个配置,{failed} 个文件失败",
|
||||
"overwriteSave": "“{name}”已存在,是否覆盖?",
|
||||
"loadOverwrite": "加载“{name}”会覆盖当前场景,未保存的修改将丢失。是否继续?",
|
||||
"deleteConfirm": "确定删除“{name}”吗?",
|
||||
"nameRequired": "请输入配置名称",
|
||||
"nameInvalid": "配置名称不能包含 \\ / : * ? \" < > |",
|
||||
"storageUnavailable": "浏览器本地存储不可用",
|
||||
"saved": "已保存到浏览器:{name}",
|
||||
"loaded": "已从浏览器加载:{name}",
|
||||
"deleted": "已删除缓存配置:{name}",
|
||||
"downloadEmpty": "没有可下载的配置",
|
||||
"downloaded": "已下载:{name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "拖动方形手柄调整弧度 · 双击空白新建 · 双击节点删除",
|
||||
|
||||
@@ -75,12 +75,13 @@
|
||||
"tickWidth": "刻度線粗細",
|
||||
"axisColor": "座標軸顏色",
|
||||
"showAxisLabels": "顯示刻度數值",
|
||||
"unsavedPrompt": "未儲存提示",
|
||||
"cacheNote": "整個系統設定會自動儲存到目前瀏覽器"
|
||||
},
|
||||
"config": {
|
||||
"title": "設定管理",
|
||||
"save": "儲存設定",
|
||||
"saveTitle": "儲存全部編輯器配置 (Ctrl+S)",
|
||||
"saveTitle": "儲存設定到瀏覽器快取",
|
||||
"load": "載入設定",
|
||||
"loadTitle": "載入本機編輯器配置",
|
||||
"presets": "預設列表",
|
||||
@@ -98,7 +99,35 @@
|
||||
"presetRestoring": "正在解析並恢復預設資源…",
|
||||
"presetComplete": "預設載入完成",
|
||||
"presetListError": "無法讀取預設列表",
|
||||
"presetReadError": "無法讀取預設:{file}"
|
||||
"presetReadError": "無法讀取預設:{file}",
|
||||
"local": {
|
||||
"listTitle": "設定列表",
|
||||
"empty": "瀏覽器中尚無已儲存的設定",
|
||||
"saveTitle": "儲存設定到瀏覽器快取",
|
||||
"name": "設定名稱",
|
||||
"namePlaceholder": "例如:煙花效果",
|
||||
"load": "載入",
|
||||
"loadHint": "按兩下以載入",
|
||||
"download": "下載",
|
||||
"delete": "刪除",
|
||||
"downloadCurrent": "下載目前設定",
|
||||
"downloadAll": "下載所有設定",
|
||||
"loadAll": "載入所有設定",
|
||||
"loadAllConfirm": "將匯入 {count} 個設定檔,同名設定會被覆寫。是否繼續?",
|
||||
"loadAllDone": "已從檔案載入 {count} 個設定",
|
||||
"loadAllPartial": "已匯入 {imported} 個設定,{failed} 個檔案失敗",
|
||||
"overwriteSave": "「{name}」已存在,是否覆寫?",
|
||||
"loadOverwrite": "載入「{name}」會覆寫目前場景,未儲存的修改將遺失。是否繼續?",
|
||||
"deleteConfirm": "確定刪除「{name}」嗎?",
|
||||
"nameRequired": "請輸入設定名稱",
|
||||
"nameInvalid": "設定名稱不能包含 \\ / : * ? \" < > |",
|
||||
"storageUnavailable": "瀏覽器本機儲存空間無法使用",
|
||||
"saved": "已儲存到瀏覽器:{name}",
|
||||
"loaded": "已從瀏覽器載入:{name}",
|
||||
"deleted": "已刪除瀏覽器設定:{name}",
|
||||
"downloadEmpty": "沒有可下載的設定",
|
||||
"downloaded": "已下載:{name}"
|
||||
}
|
||||
},
|
||||
"curve": {
|
||||
"bezierHint": "拖曳方形手把調整弧度 · 雙擊空白新建 · 雙擊節點刪除",
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface SettingsState {
|
||||
tickWidth: number
|
||||
/** 是否显示坐标轴刻度数值 */
|
||||
showAxisLabels: boolean
|
||||
/** 有未保存修改时,刷新或关闭页面前显示浏览器原生提示。 */
|
||||
unsavedChangesPrompt: boolean
|
||||
/** 坐标轴数值颜色(#rrggbb) */
|
||||
axisColor: string
|
||||
/** 最大撤回操作数。 */
|
||||
@@ -143,6 +145,7 @@ export const useParticleStore = defineStore('particle', {
|
||||
axisFontSize: 12,
|
||||
tickWidth: 1,
|
||||
showAxisLabels: true,
|
||||
unsavedChangesPrompt: false,
|
||||
axisColor: '#7a86ad',
|
||||
undoLimit: 20,
|
||||
backgroundX: 0,
|
||||
|
||||
+11
-3
@@ -40,9 +40,9 @@ import { loopPlaybackRange } from '../core/loopSegmentTiming'
|
||||
import { collectSubEmitterIds } from '../core/subEmitterIds'
|
||||
import { useParticleStore, type CollisionBody, type ParticleSystem } from '../store/particleStore'
|
||||
import { SpineRuntimeLayer } from '../spine/SpineRuntimeLayer'
|
||||
import { installEditorHistory, undoLastOperation } from '../editor/editorHistory'
|
||||
import { editorHistoryState, installEditorHistory, undoLastOperation } from '../editor/editorHistory'
|
||||
import { useEditorShortcuts } from '../editor/useEditorShortcuts'
|
||||
import { downloadEditorConfig } from '../editor/editorConfig'
|
||||
import { PROJECT_SAVE_EVENT } from '../editor/projectLibrary'
|
||||
import HelpDialog from './HelpDialog.vue'
|
||||
import StageToolbar from './StageToolbar.vue'
|
||||
import StageSettingsPanel from './StageSettingsPanel.vue'
|
||||
@@ -166,6 +166,12 @@ function onStageDoubleClick(e: MouseEvent) {
|
||||
pathEditing.onStageDoubleClick(e)
|
||||
}
|
||||
|
||||
function onBeforeUnload(event: BeforeUnloadEvent) {
|
||||
if (!store.settings.unsavedChangesPrompt || !editorHistoryState.dirty) return
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
function onWheelEvent(e: WheelEvent) {
|
||||
onWheel(stageContext, e)
|
||||
}
|
||||
@@ -182,7 +188,7 @@ useEditorShortcuts({
|
||||
canvasElement: holder,
|
||||
canvasFocused,
|
||||
undo: undoLastOperation,
|
||||
save: () => downloadEditorConfig(store),
|
||||
save: () => window.dispatchEvent(new Event(PROJECT_SAVE_EVENT)),
|
||||
togglePlay: () => store.togglePlay(),
|
||||
toStart: () => store.toStart(),
|
||||
setTool: (tool) => { activeTool.value = tool },
|
||||
@@ -553,9 +559,11 @@ onMounted(async () => {
|
||||
},
|
||||
)
|
||||
installEditorHistory(store)
|
||||
window.addEventListener('beforeunload', onBeforeUnload)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
cancelAnimationFrame(raf)
|
||||
spineRuntime?.destroy()
|
||||
spineRuntime = null
|
||||
|
||||
@@ -74,6 +74,15 @@ function normalizeBoneDebugSizes() {
|
||||
</label>
|
||||
<label class="sp-chk"><input type="checkbox" v-model="store.settings.showAxisLabels" />{{ t('settings.showAxisLabels') }}</label>
|
||||
</template>
|
||||
<div class="sp-divider"></div>
|
||||
<label class="sp-switch-row">
|
||||
<span>{{ t('settings.unsavedPrompt') }}</span>
|
||||
<span class="sp-switch-control">
|
||||
<input type="checkbox" v-model="store.settings.unsavedChangesPrompt" :aria-label="t('settings.unsavedPrompt')" />
|
||||
<span class="sp-switch-ui" aria-hidden="true"></span>
|
||||
<b>{{ store.settings.unsavedChangesPrompt ? t('common.enabled') : t('common.disabled') }}</b>
|
||||
</span>
|
||||
</label>
|
||||
<div class="sp-cache-note">{{ t('settings.cacheNote') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -87,6 +96,7 @@ function normalizeBoneDebugSizes() {
|
||||
.sp-head { display: flex; justify-content: space-between; align-items: center; font-weight: 700; color: #cdf; margin-bottom: 10px; }
|
||||
.sp-section-title { margin: 2px 0 8px; color: #8a93bb; font-size: 11px; }
|
||||
.sp-switch-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: #aab; }
|
||||
.sp-switch-row > span:first-child { min-width: 0; flex: 1; }
|
||||
.sp-switch-control { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.sp-switch-control input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.sp-switch-ui { position: relative; width: 34px; height: 18px; border-radius: 999px; background: #39445a; cursor: pointer; transition: background .16s ease; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div class="group config-management">
|
||||
<div class="group-head" @click="toggle('config')">
|
||||
<span class="caret">{{ collapsed.config ? '▸' : '▾' }}</span><span>{{ $t('config.title') }}</span>
|
||||
<button type="button" class="group-download" @click.stop="downloadCurrentProject">{{ $t('config.local.downloadCurrent') }}</button>
|
||||
</div>
|
||||
<div class="group-body config-management-body" v-show="!collapsed.config">
|
||||
<div class="config-toolbar">
|
||||
@@ -14,8 +15,67 @@
|
||||
<input ref="configFileInput" hidden type="file" accept=".json,application/json" @change="onConfigFileChange" />
|
||||
</div>
|
||||
<div v-if="configMessage" class="config-message" :class="{ error: configError }">{{ configMessage }}</div>
|
||||
<div class="project-library">
|
||||
<div class="project-library-title">{{ $t('config.local.listTitle') }}</div>
|
||||
<div v-if="localProjects.length" class="project-list" role="listbox">
|
||||
<div
|
||||
v-for="project in localProjects"
|
||||
:key="project.name"
|
||||
class="project-row"
|
||||
:class="{ selected: selectedLocalProjectName === project.name }"
|
||||
:title="$t('config.local.loadHint')"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
:aria-selected="selectedLocalProjectName === project.name"
|
||||
@click="selectLocalProject(project)"
|
||||
@dblclick="loadLocalProject(project)"
|
||||
@keydown.enter.prevent="loadLocalProject(project)"
|
||||
@keydown.space.prevent="selectLocalProject(project)"
|
||||
>
|
||||
<span class="project-name">{{ project.name }}</span>
|
||||
<div class="project-actions" @dblclick.stop>
|
||||
<button type="button" @click.stop="removeLocalProject(project)">{{ $t('config.local.delete') }}</button>
|
||||
<button type="button" @click.stop="downloadLocalProject(project)">{{ $t('config.local.download') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="project-empty">{{ $t('config.local.empty') }}</div>
|
||||
</div>
|
||||
<div class="project-bulk-actions">
|
||||
<button type="button" :disabled="projectStorageBusy" @click="downloadAllLocalProjects">
|
||||
{{ $t('config.local.downloadAll') }}
|
||||
</button>
|
||||
<button type="button" :disabled="projectStorageBusy" @click="projectFilesInput?.click()">
|
||||
{{ $t('config.local.loadAll') }}
|
||||
</button>
|
||||
<input ref="projectFilesInput" hidden type="file" multiple accept=".json,application/json" @change="loadAllProjectFiles" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div v-if="projectSaveOpen" class="project-save-backdrop" @pointerdown.self="closeProjectSaveDialog">
|
||||
<form class="project-save-dialog" @submit.prevent="confirmSaveConfig" @keydown.esc.prevent="closeProjectSaveDialog">
|
||||
<div class="project-save-head">
|
||||
<span>{{ $t('config.local.saveTitle') }}</span>
|
||||
<button type="button" :disabled="projectStorageBusy" @click="closeProjectSaveDialog">✕</button>
|
||||
</div>
|
||||
<label>
|
||||
<span>{{ $t('config.local.name') }}</span>
|
||||
<input
|
||||
v-model="projectNameDraft"
|
||||
class="inp"
|
||||
:placeholder="$t('config.local.namePlaceholder')"
|
||||
autofocus
|
||||
/>
|
||||
</label>
|
||||
<div v-if="projectSaveError" class="project-save-error">{{ projectSaveError }}</div>
|
||||
<div class="project-save-actions">
|
||||
<button type="button" :disabled="projectStorageBusy" @click="closeProjectSaveDialog">{{ $t('common.cancel') }}</button>
|
||||
<button type="submit" class="primary" :disabled="projectStorageBusy">{{ $t('config.save') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -28,7 +88,14 @@ import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { useParticlePanelContext } from './particlePanelContext'
|
||||
import { resolvePresetDisplayName, type PresetOption } from './presetNames'
|
||||
const { configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, saveConfig, onConfigFileChange, refreshPresets, loadSelectedPreset, collapsed, toggle, select } = useParticlePanelContext()
|
||||
const {
|
||||
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
|
||||
localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject,
|
||||
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets,
|
||||
loadSelectedPreset, collapsed, toggle, select,
|
||||
} = useParticlePanelContext()
|
||||
const { locale } = useI18n()
|
||||
function presetDisplayName(preset: PresetOption) {
|
||||
return resolvePresetDisplayName(preset, locale.value)
|
||||
@@ -37,6 +104,11 @@ function presetDisplayName(preset: PresetOption) {
|
||||
|
||||
<style scoped>
|
||||
.config-management-body { padding-bottom: 2px; }
|
||||
.group-download {
|
||||
margin-left: auto; height: 24px; padding: 0 8px; border: 1px solid #485673; border-radius: 5px;
|
||||
background: #202a3b; color: #d7e0f0; font-size: 10px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.group-download:hover { border-color: #7774ff; background: #303858; color: #fff; }
|
||||
.config-toolbar { display: grid; grid-template-columns: auto auto minmax(0,1fr); gap: 6px; margin-bottom: 10px; }
|
||||
.config-toolbar button,.config-toolbar select {
|
||||
min-width: 0; height: 30px; box-sizing: border-box; border: 1px solid #3b4864; border-radius: 6px;
|
||||
@@ -48,6 +120,50 @@ function presetDisplayName(preset: PresetOption) {
|
||||
.config-toolbar select:disabled { color: #68738a; background: #1b2130; }
|
||||
.config-message { margin: -4px 0 9px; color: #83b9a2; font-size: 10px; }
|
||||
.config-message.error { color: #e8959e; }
|
||||
.project-library { margin-top: 4px; padding: 9px; border: 1px solid #2f3b52; border-radius: 7px; background: #111725; }
|
||||
.project-library-title { margin-bottom: 6px; color: #cbd5e8; font-size: 11px; font-weight: 700; }
|
||||
.project-list { display: flex; flex-direction: column; max-height: 190px; overflow-y: auto; }
|
||||
.project-row {
|
||||
display: flex; min-height: 32px; align-items: center; gap: 8px; border-top: 1px solid #222d40;
|
||||
padding: 0 9px; cursor: pointer; outline: none; user-select: none;
|
||||
}
|
||||
.project-row:first-child { border-top: 0; }
|
||||
.project-row:hover { background: #182033; }
|
||||
.project-row.selected { background: #25264b; box-shadow: inset 2px 0 #8179ff; }
|
||||
.project-row:focus-visible { box-shadow: inset 0 0 0 1px #8179ff; }
|
||||
.project-name { min-width: 0; flex: 1; overflow: hidden; color: #c7d2e5; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.project-actions { display: flex; flex: 0 0 auto; gap: 6px; }
|
||||
.project-actions button {
|
||||
display: inline-flex; height: 24px; align-items: center; padding: 0 7px; box-sizing: border-box;
|
||||
border: 1px solid #3b4864; border-radius: 5px; background: #1b2332; color: #9eabd3; font-size: 10px; cursor: pointer;
|
||||
}
|
||||
.project-actions button:hover { border-color: #7169dc; background: #29324b; color: #fff; }
|
||||
.project-actions button:first-child:hover { border-color: #8c5462; background: #35222b; color: #ef9da9; }
|
||||
.project-empty { padding: 9px 2px; color: #68758d; font-size: 10px; text-align: center; }
|
||||
.project-bulk-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 8px; }
|
||||
.project-bulk-actions button {
|
||||
width: 100%; min-height: 34px; padding: 5px 6px; border: 1px solid #4d4264; border-radius: 7px;
|
||||
background: #241c31; color: #e4d9f2; font-size: 11px; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.project-bulk-actions button:hover:not(:disabled) { border-color: #8a6fbd; background: #35264a; }
|
||||
.project-bulk-actions button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.project-save-backdrop {
|
||||
position: fixed; z-index: 3000; inset: 0; display: grid; place-items: center;
|
||||
background: rgba(5, 8, 15, .72); backdrop-filter: blur(3px);
|
||||
}
|
||||
.project-save-dialog {
|
||||
width: min(380px, calc(100vw - 32px)); padding: 15px; border: 1px solid #465273; border-radius: 10px;
|
||||
background: #171d2b; color: #d8e0ef; box-shadow: 0 20px 60px rgba(0,0,0,.55);
|
||||
}
|
||||
.project-save-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; font-size: 14px; font-weight: 700; }
|
||||
.project-save-head button { width: 24px; height: 24px; border: 1px solid #3b4864; border-radius: 5px; background: #252f43; color: #bfc9dc; cursor: pointer; }
|
||||
.project-save-dialog label { display: flex; flex-direction: column; gap: 6px; color: #8f9cb8; font-size: 11px; }
|
||||
.project-save-dialog .inp { width: 100%; height: 34px; box-sizing: border-box; }
|
||||
.project-save-error { margin-top: 8px; color: #ef9aa7; font-size: 10px; line-height: 1.4; }
|
||||
.project-save-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
||||
.project-save-actions button { height: 31px; padding: 0 14px; border: 1px solid #3d4b66; border-radius: 6px; background: #27334a; color: #cbd5e7; cursor: pointer; }
|
||||
.project-save-actions button.primary { border-color: #625ff1; background: #514dcc; color: #fff; }
|
||||
.project-save-actions button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.group { border: 1px solid #2a2a3c; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
|
||||
.group-head {
|
||||
display: flex; align-items: center; gap: 6px; padding: 7px 10px; cursor: pointer;
|
||||
|
||||
@@ -37,7 +37,13 @@ export function useParticlePanel() {
|
||||
const store = useParticleStore()
|
||||
const { t } = useI18n()
|
||||
const { exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorHealth, connectorPairUrl, connectorExportErrorCode, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, connectorHealthTimer, connectorHealthRequestId, refreshConnectorHealth, nextPaint, prepareExportFrames, exportSpineJson, exportSpineProject, exportSpineImages } = useParticleExport(store)
|
||||
const { configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, saveConfig, onConfigFileChange, refreshPresets, loadSelectedPreset } = useProjectPresets(store)
|
||||
const {
|
||||
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
|
||||
localProjects, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject,
|
||||
removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectedLocalProjectName, selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset,
|
||||
} = useProjectPresets(store)
|
||||
function resolvedCurveEditorMode(mode: '跟随导出' | '线性' | '贝塞尔') {
|
||||
if (mode === '跟随导出') return exportSettings.value.keyframeCurve === 'bezier' ? 'bezier' : 'linear'
|
||||
return mode === '贝塞尔' ? 'bezier' : 'linear'
|
||||
@@ -589,5 +595,5 @@ function onSpawnPathChange() {
|
||||
// 重置/删除图片 → 回到默认 star.png
|
||||
|
||||
|
||||
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, saveConfig, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
|
||||
return { store, exportSettings, exportBusy, exportTarget, exportMessage, exportError, connectorPairUrl, connectorStatusText, connectorStatusClass, connectorReady, connectorAuthorizationUrl, connectorDisabledReason, connectorButtonTitle, refreshConnectorHealth, exportSpineJson, exportSpineProject, exportSpineImages, configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, localProjects, selectedLocalProjectName, selectLocalProject, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy, projectFilesInput, saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject, downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, loadAllProjectFiles, onConfigFileChange, refreshPresets, loadSelectedPreset, resolvedCurveEditorMode, sys, textureFileInput, sequenceFileInput, trailTextureFileInput, addTrailResource, openTrailTexturePicker, onTrailRigChange, onTrailResourceBoneCountChange, onTrailResourceRotationChange, onTrailTextureFile, resetTrailTexture, removeTrailResource, setTrailResourceWeight, toggleTrailResourceLock, setTrailColor, commitTrailColor, addImageResource, openTexturePicker, openSequencePicker, onTextureFile, onSequenceFiles, resetTexture, setResourceFixedColor, commitResourceFixedColor, removeImageResource, setResourceWeight, toggleResourceLock, automaticLoopDuration, loopDurationFrames, activeObjectType, activeCollider, activePath, activeSpine, canCopyObject, selectedSpineBone, boundSpineBone, renderLayerSlotPickerOpen, renderLayerSlotSearch, renderLayerReadySpines, renderLayerSpineReady, renderLayerSlots, renderLayerFilteredSlots, renderLayerSlotLabel, renderLayerResolvedLabel, renderLayerWarning, setRenderLayerSlotMode, selectRenderLayerSpine, pickRenderLayerSlot, clearRenderLayerSlot, collisionPickerOpen, selectedCollisionBodies, availableCollisionBodies, addCollisionBody, removeCollisionBody, collapsed, toggle, setEmitMode, setLoopDurationFrames, setStreamBehavior, setLoopSection, setLoopSectionDuration, setLoopCustomDuration, onEmitterFollowModeChange, bindSelectedBone, resetModule, normalizeExportFps, select, addSys, addCollider, addPath, addSpine, copySelectedObject, applyPathPreset, clearPathAnchors, randomizeSeed, setSpeedOverLifeMode, prepareScaleOverLifeCurve, setScaleOverLifeMode, onShapeChange, onSpawnPathChange, LINEAR_CURVE, FLAT_CURVE, ALPHA_CURVE, RESOURCE_ALPHA_CURVE, TRAIL_ALPHA_CURVE, TRAIL_SHAPE_CURVE }
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import ParticleAttributeControl from '../ParticleAttributeControl.vue'
|
||||
import ColorGradientEditor from '../ColorGradientEditor.vue'
|
||||
import ImageResourceCard from '../ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../../spine/SpineObjectPanel.vue'
|
||||
import { downloadEditorConfig, loadEditorConfigFile, loadEditorConfigText } from '../../editor/editorConfig'
|
||||
import { createEditorConfig, loadEditorConfigFile, loadEditorConfigText, parseEditorConfig } from '../../editor/editorConfig'
|
||||
import { beginTransfer, readResponseTextWithProgress } from '../../editor/transferProgress'
|
||||
import { defaultSpineExportSettings, ensureSpineExportSettings } from '../../export/spineExportSettings'
|
||||
import { buildSpineJson, downloadSpineJson } from '../../export/spineJsonExporter'
|
||||
@@ -25,7 +25,19 @@ import {
|
||||
} from '../../export/spineConnectorClient'
|
||||
import { automaticLoopTransitionFrames } from '../../core/loopSegmentTiming'
|
||||
import { translate } from '../../i18n'
|
||||
import { markEditorSaved } from '../../editor/editorHistory'
|
||||
import { parsePresetIndex, type PresetOption } from './presetNames'
|
||||
import {
|
||||
PROJECT_SAVE_EVENT,
|
||||
deleteLocalProject as deleteStoredProject,
|
||||
downloadProjectsArchive,
|
||||
downloadTextFile,
|
||||
listLocalProjects,
|
||||
normalizeProjectName,
|
||||
projectJsonFileName,
|
||||
saveLocalProject,
|
||||
type LocalProject,
|
||||
} from '../../editor/projectLibrary'
|
||||
|
||||
export function useProjectPresets(store: ReturnType<typeof useParticleStore>) {
|
||||
const configFileInput = ref<HTMLInputElement | null>(null)
|
||||
@@ -34,10 +46,167 @@ const presetOptions = ref<PresetOption[]>([])
|
||||
const presetLoading = ref(false)
|
||||
const configMessage = ref('')
|
||||
const configError = ref(false)
|
||||
const localProjects = ref<LocalProject[]>([])
|
||||
const projectSaveOpen = ref(false)
|
||||
const projectNameDraft = ref('')
|
||||
const projectSaveError = ref('')
|
||||
const projectStorageBusy = ref(false)
|
||||
const projectFilesInput = ref<HTMLInputElement | null>(null)
|
||||
const selectedLocalProjectName = ref('')
|
||||
|
||||
function defaultProjectName() {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, '').replace('T', '-')
|
||||
return `SpineParticle-${stamp}`
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
downloadEditorConfig(store)
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.saved')
|
||||
projectSaveError.value = ''
|
||||
projectNameDraft.value = defaultProjectName()
|
||||
projectSaveOpen.value = true
|
||||
}
|
||||
|
||||
function closeProjectSaveDialog() {
|
||||
if (projectStorageBusy.value) return
|
||||
projectSaveOpen.value = false
|
||||
projectSaveError.value = ''
|
||||
}
|
||||
|
||||
async function refreshLocalProjects() {
|
||||
try {
|
||||
localProjects.value = await listLocalProjects()
|
||||
if (!localProjects.value.some((project) => project.name === selectedLocalProjectName.value)) {
|
||||
selectedLocalProjectName.value = ''
|
||||
}
|
||||
} catch (error) {
|
||||
localProjects.value = []
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : translate('config.local.storageUnavailable')
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSaveConfig() {
|
||||
if (projectStorageBusy.value) return
|
||||
projectSaveError.value = ''
|
||||
projectStorageBusy.value = true
|
||||
try {
|
||||
const name = normalizeProjectName(projectNameDraft.value)
|
||||
if (localProjects.value.some((project) => project.name === name)
|
||||
&& !window.confirm(translate('config.local.overwriteSave', { name }))) {
|
||||
return
|
||||
}
|
||||
const text = JSON.stringify(createEditorConfig(store), null, 2)
|
||||
await saveLocalProject(name, text)
|
||||
markEditorSaved(store)
|
||||
selectedLocalProjectName.value = name
|
||||
await refreshLocalProjects()
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.local.saved', { name })
|
||||
projectSaveOpen.value = false
|
||||
} catch (error) {
|
||||
projectSaveError.value = error instanceof Error ? error.message : translate('config.local.storageUnavailable')
|
||||
} finally {
|
||||
projectStorageBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectLocalProject(project: LocalProject) {
|
||||
selectedLocalProjectName.value = project.name
|
||||
}
|
||||
|
||||
async function loadLocalProject(project: LocalProject) {
|
||||
if (!window.confirm(translate('config.local.loadOverwrite', { name: project.name }))) return
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.loading')
|
||||
try {
|
||||
await loadEditorConfigText(store, project.text)
|
||||
markEditorSaved(store)
|
||||
configMessage.value = translate('config.local.loaded', { name: project.name })
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
|
||||
function downloadLocalProject(project: LocalProject) {
|
||||
const fileName = projectJsonFileName(project.name)
|
||||
downloadTextFile(project.text, fileName)
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.local.downloaded', { name: fileName })
|
||||
}
|
||||
|
||||
function downloadCurrentProject() {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, '').replace('T', '-')
|
||||
const fileName = `SpineParticle-${stamp}.json`
|
||||
downloadTextFile(JSON.stringify(createEditorConfig(store), null, 2), fileName)
|
||||
markEditorSaved(store)
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.local.downloaded', { name: fileName })
|
||||
}
|
||||
|
||||
async function removeLocalProject(project: LocalProject) {
|
||||
if (!window.confirm(translate('config.local.deleteConfirm', { name: project.name }))) return
|
||||
try {
|
||||
await deleteStoredProject(project.name)
|
||||
if (selectedLocalProjectName.value === project.name) selectedLocalProjectName.value = ''
|
||||
await refreshLocalProjects()
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.local.deleted', { name: project.name })
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : translate('config.local.storageUnavailable')
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAllLocalProjects() {
|
||||
try {
|
||||
const projects = await listLocalProjects()
|
||||
if (!projects.length) {
|
||||
configError.value = true
|
||||
configMessage.value = translate('config.local.downloadEmpty')
|
||||
return
|
||||
}
|
||||
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, '').replace('T', '-')
|
||||
const fileName = `SpineParticle-configs-${stamp}.zip`
|
||||
downloadProjectsArchive(projects, fileName)
|
||||
configError.value = false
|
||||
configMessage.value = translate('config.local.downloaded', { name: fileName })
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
configMessage.value = error instanceof Error ? error.message : translate('config.local.storageUnavailable')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAllProjectFiles(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const files = Array.from(input.files || [])
|
||||
input.value = ''
|
||||
if (!files.length || projectStorageBusy.value) return
|
||||
if (!window.confirm(translate('config.local.loadAllConfirm', { count: files.length }))) return
|
||||
projectStorageBusy.value = true
|
||||
let imported = 0
|
||||
let failed = 0
|
||||
try {
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (!/\.json$/i.test(file.name)) throw new Error(translate('errors.chooseJsonConfig'))
|
||||
const text = await file.text()
|
||||
parseEditorConfig(text)
|
||||
const name = normalizeProjectName(file.name)
|
||||
await saveLocalProject(name, text)
|
||||
imported++
|
||||
} catch {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
await refreshLocalProjects()
|
||||
configError.value = failed > 0
|
||||
configMessage.value = failed
|
||||
? translate('config.local.loadAllPartial', { imported, failed })
|
||||
: translate('config.local.loadAllDone', { count: imported })
|
||||
} finally {
|
||||
projectStorageBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onConfigFileChange(event: Event) {
|
||||
@@ -48,6 +217,7 @@ async function onConfigFileChange(event: Event) {
|
||||
configError.value = false
|
||||
try {
|
||||
await loadEditorConfigFile(store, file)
|
||||
markEditorSaved(store)
|
||||
configMessage.value = translate('config.loaded', { name: file.name })
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
@@ -85,6 +255,7 @@ async function loadSelectedPreset() {
|
||||
transfer.update(58, translate('config.presetParsing'))
|
||||
transfer.update(70, translate('config.presetRestoring'))
|
||||
await loadEditorConfigText(store, text)
|
||||
markEditorSaved(store)
|
||||
configMessage.value = translate('config.presetLoaded', { name: file.replace(/\.json$/i, '') })
|
||||
await transfer.finish(translate('config.presetComplete'))
|
||||
} catch (error) {
|
||||
@@ -99,7 +270,18 @@ async function loadSelectedPreset() {
|
||||
}
|
||||
|
||||
|
||||
onMounted(refreshPresets)
|
||||
onMounted(() => {
|
||||
refreshPresets()
|
||||
refreshLocalProjects()
|
||||
window.addEventListener(PROJECT_SAVE_EVENT, saveConfig)
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener(PROJECT_SAVE_EVENT, saveConfig))
|
||||
|
||||
return { configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError, saveConfig, onConfigFileChange, refreshPresets, loadSelectedPreset }
|
||||
return {
|
||||
configFileInput, selectedPreset, presetOptions, presetLoading, configMessage, configError,
|
||||
localProjects, selectedLocalProjectName, projectSaveOpen, projectNameDraft, projectSaveError, projectStorageBusy,
|
||||
saveConfig, closeProjectSaveDialog, confirmSaveConfig, loadLocalProject, downloadLocalProject,
|
||||
downloadCurrentProject, removeLocalProject, downloadAllLocalProjects, projectFilesInput, loadAllProjectFiles,
|
||||
selectLocalProject, onConfigFileChange, refreshPresets, loadSelectedPreset,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user