Story 3.3 done
This commit is contained in:
@@ -93,7 +93,8 @@ export class PresetExportDialog extends _AppBase {
|
||||
return {
|
||||
presetCount,
|
||||
sceneName,
|
||||
filename: this._exportManager.generateExportFilename(sceneName, false),
|
||||
hasScene: !!currentScene,
|
||||
filename: this._exportManager.generateExportFilename(sceneName),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,14 +166,14 @@ export class PresetExportDialog extends _AppBase {
|
||||
btn.textContent = '';
|
||||
const spinner = document.createElement('i');
|
||||
spinner.className = 'fas fa-spinner fa-spin';
|
||||
const text = document.createTextNode(' Exporting...');
|
||||
const text = document.createTextNode(' ' + this._adapter.i18n.localize('scrying-pool.presetExport.exporting'));
|
||||
btn.appendChild(spinner);
|
||||
btn.appendChild(text);
|
||||
|
||||
// Export presets
|
||||
const jsonString = await this._exportManager.exportAllPresets();
|
||||
const currentScene = this._adapter.scenes.current?.();
|
||||
const worldName = this._adapter.scenes.current?.()?.parent?.name ?? currentScene?.name ?? 'world';
|
||||
const worldName = currentScene?.parent?.name ?? currentScene?.name ?? 'world';
|
||||
const filename = this._exportManager.generateExportFilename(worldName);
|
||||
|
||||
// Trigger download
|
||||
@@ -180,7 +181,7 @@ export class PresetExportDialog extends _AppBase {
|
||||
|
||||
// Show success notification
|
||||
if (this._adapter.notifications) {
|
||||
this._adapter.notifications.info('Scene presets exported successfully.');
|
||||
this._adapter.notifications.info(this._adapter.i18n.localize('scrying-pool.presetExport.exportSuccess'));
|
||||
}
|
||||
|
||||
// Close dialog
|
||||
@@ -188,7 +189,7 @@ export class PresetExportDialog extends _AppBase {
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
if (this._adapter.notifications) {
|
||||
this._adapter.notifications.error('Failed to export presets: ' + escapeHtml(errorMsg));
|
||||
this._adapter.notifications.error(this._adapter.i18n.localize('scrying-pool.presetExport.exportFailed') + ': ' + escapeHtml(errorMsg));
|
||||
}
|
||||
} finally {
|
||||
if (btn) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { PresetImportExportManager } from '../../core/PresetImportExportManager.js';
|
||||
import { isValidScenePreset } from '../../contracts/scene-preset.js';
|
||||
import { escapeHtml } from '../../utils/html.js';
|
||||
|
||||
// Maximum file size: 5MB
|
||||
@@ -78,6 +79,10 @@ export class PresetImportDialog extends _AppBase {
|
||||
/** @type {boolean} */
|
||||
this._requiresConfirmation = false;
|
||||
|
||||
// Concurrency guard for async file operations
|
||||
/** @type {boolean} */
|
||||
this._parsingInProgress = false;
|
||||
|
||||
// Event listener tracking for cleanup
|
||||
/** @type {Array<{element: Element, type: string, listener: EventListener}>} */
|
||||
this._eventListeners = [];
|
||||
@@ -110,8 +115,8 @@ export class PresetImportDialog extends _AppBase {
|
||||
previewItems: this._previewItems,
|
||||
requiresConfirmation: this._requiresConfirmation,
|
||||
selectedFileName: this._selectedFile?.name ?? null,
|
||||
mergeLabel: 'Merge',
|
||||
replaceLabel: 'Replace',
|
||||
mergeLabel: this._adapter.i18n.localize('scrying-pool.presetImport.importModeMerge'),
|
||||
replaceLabel: this._adapter.i18n.localize('scrying-pool.presetImport.importModeReplace'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -272,14 +277,17 @@ export class PresetImportDialog extends _AppBase {
|
||||
* @private
|
||||
*/
|
||||
async _parseAndPreviewFile() {
|
||||
if (!this._selectedFile) {
|
||||
this._previewItems = [];
|
||||
this._requiresConfirmation = false;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
if (this._parsingInProgress) return;
|
||||
this._parsingInProgress = true;
|
||||
|
||||
try {
|
||||
if (!this._selectedFile) {
|
||||
this._previewItems = [];
|
||||
this._requiresConfirmation = false;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await this._readFileAsText(this._selectedFile);
|
||||
const data = JSON.parse(content);
|
||||
|
||||
@@ -290,19 +298,24 @@ export class PresetImportDialog extends _AppBase {
|
||||
this._previewItems = [];
|
||||
const existingNames = new Set(this._scenePresetManager.list().map(p => p.name));
|
||||
|
||||
for (const [name] of Object.entries(data.presets || {})) {
|
||||
for (const [name, presetData] of Object.entries(data.presets || {})) {
|
||||
let valid = true;
|
||||
let error = undefined;
|
||||
|
||||
try {
|
||||
// Check if preset name already exists (for merge mode preview)
|
||||
if (this._mode === 'merge' && existingNames.has(name)) {
|
||||
valid = false;
|
||||
error = 'Already exists - will be skipped';
|
||||
}
|
||||
} catch (err) {
|
||||
// Check if preset name already exists (for merge mode preview)
|
||||
if (this._mode === 'merge' && existingNames.has(name)) {
|
||||
valid = false;
|
||||
error = escapeHtml(err instanceof Error ? err.message : String(err));
|
||||
error = this._adapter.i18n.localize('scrying-pool.presetImport.previewWillSkip');
|
||||
}
|
||||
|
||||
// Validate preset structure for preview accuracy
|
||||
if (valid && presetData) {
|
||||
try {
|
||||
isValidScenePreset(presetData);
|
||||
} catch (err) {
|
||||
valid = false;
|
||||
error = escapeHtml(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
this._previewItems.push({ name, valid, error });
|
||||
@@ -318,9 +331,11 @@ export class PresetImportDialog extends _AppBase {
|
||||
this.render();
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
this._previewItems = [{ name: this._selectedFile.name, valid: false, error: escapeHtml(errorMsg) }];
|
||||
this._previewItems = [{ name: this._selectedFile?.name ?? 'unknown', valid: false, error: escapeHtml(errorMsg) }];
|
||||
this._requiresConfirmation = false;
|
||||
this.render();
|
||||
} finally {
|
||||
this._parsingInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +371,7 @@ export class PresetImportDialog extends _AppBase {
|
||||
async _onImport() {
|
||||
if (!this._selectedFile) {
|
||||
if (this._adapter.notifications) {
|
||||
this._adapter.notifications.warn('Please select a file first');
|
||||
this._adapter.notifications.warn(this._adapter.i18n.localize('scrying-pool.presetImport.selectFileFirst'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -403,7 +418,7 @@ export class PresetImportDialog extends _AppBase {
|
||||
btn.textContent = '';
|
||||
const spinner = document.createElement('i');
|
||||
spinner.className = 'fas fa-spinner fa-spin';
|
||||
const text = document.createTextNode(' Importing...');
|
||||
const text = document.createTextNode(' ' + this._adapter.i18n.localize('scrying-pool.presetImport.importing'));
|
||||
btn.appendChild(spinner);
|
||||
btn.appendChild(text);
|
||||
|
||||
@@ -420,13 +435,13 @@ export class PresetImportDialog extends _AppBase {
|
||||
// Show errors
|
||||
const errorMessages = result.errors.map(e => escapeHtml(e)).join('\n');
|
||||
if (this._adapter.notifications) {
|
||||
this._adapter.notifications.error('Failed to import presets\n' + errorMessages);
|
||||
this._adapter.notifications.error(this._adapter.i18n.localize('scrying-pool.presetImport.importFailed') + '\n' + errorMessages);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||
if (this._adapter.notifications) {
|
||||
this._adapter.notifications.error('Failed to import presets: ' + escapeHtml(errorMsg));
|
||||
this._adapter.notifications.error(this._adapter.i18n.localize('scrying-pool.presetImport.importFailed') + ': ' + escapeHtml(errorMsg));
|
||||
}
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user