UNPKG

@iebh/vuex-tera-json

Version:

A Vuex plugin for syncing state with Tera (using JSON files)

917 lines 39.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SaveStatus = void 0; exports.createTeraSync = createTeraSync; // index.ts const nanoid_1 = require("nanoid"); const p_retry_1 = __importDefault(require("p-retry")); const api = __importStar(require("./helpers/api")); /** * Represents the possible save statuses of the state. */ var SaveStatus; (function (SaveStatus) { /** Indicates that all current changes are safely stored */ SaveStatus["SAVED"] = "Saved"; /** Indicates that there are modifications pending to be saved */ SaveStatus["UNSAVED"] = "Unsaved changes"; /** Indicates that a save operation is currently in progress */ SaveStatus["SAVING"] = "Saving..."; })(SaveStatus || (exports.SaveStatus = SaveStatus = {})); // --- Implementation --- const DEBUG = true; const DEFAULT_CONFIG = { keyPrefix: '', isSeparateStateForEachUser: false, autoSaveIntervalMinutes: 15, showInitialAlert: false, enableSaveHotkey: true, loadImmediately: true, onBeforeSave: () => true, resetState: () => { }, // Provide a no-op default }; const debugLog = (...args) => { if (DEBUG) console.log('[TERA File Sync]:', ...args); }; const logError = (error, context) => { console.error(`[TERA File Sync] ${context}:`, error); }; const validateConfig = (config) => { if (typeof config.keyPrefix !== 'string') { throw new Error('keyPrefix must be a string'); } if (typeof config.isSeparateStateForEachUser !== 'boolean') { throw new Error('isSeparateStateForEachUser must be a boolean'); } if (typeof config.autoSaveIntervalMinutes !== 'number' || config.autoSaveIntervalMinutes < 0) { throw new Error('autoSaveIntervalMinutes must be a non-negative number'); } if (typeof config.showInitialAlert !== 'boolean') { throw new Error('showInitialAlert must be a boolean'); } if (typeof config.enableSaveHotkey !== 'boolean') { throw new Error('enableSaveHotkey must be a boolean'); } if (typeof config.loadImmediately !== 'boolean') { throw new Error('loadImmediately must be a boolean'); } if (typeof config.onBeforeSave !== 'function') { throw new Error('onBeforeSave must be a function'); } }; const validateVueInstance = (instance) => { if (!instance) { throw new Error('Vue instance is required'); } if (!instance.$tera) { throw new Error('Vue instance must have $tera property'); } if (typeof instance.$tera.getUser !== 'function') { throw new Error('$tera.getUser must be a function'); } if (typeof instance.$tera.getProjectFile !== 'function') { throw new Error('$tera.getProjectFile must be a function'); } if (typeof instance.$tera.getProjectFileContents !== 'function') { throw new Error('$tera.getProjectFileContents must be a function'); } if (typeof instance.$tera.setProjectFileContents !== 'function') { throw new Error('$tera.setProjectFileContents must be a function'); } if (typeof instance.$tera.uiProgress !== 'function') { throw new Error('$tera.uiProgress must be a function'); } }; const mapSetToObject = (item) => { try { if (item instanceof Map) { debugLog('Converting Map to object'); const obj = { __isMap: true }; item.forEach((value, key) => { obj[key] = mapSetToObject(value); }); return obj; } if (item instanceof Set) { debugLog('Converting Set to array'); return { __isSet: true, values: Array.from(item).map(mapSetToObject), }; } if (Array.isArray(item)) { return item.map(mapSetToObject); } if (item && typeof item === 'object' && !(item instanceof Date)) { const obj = {}; Object.entries(item).forEach(([key, value]) => { obj[key] = mapSetToObject(value); }); return obj; } return item; } catch (error) { logError(error, 'mapSetToObject conversion failed'); throw error; } }; const objectToMapSet = (obj) => { try { if (!obj || typeof obj !== 'object' || obj instanceof Date) { return obj; } if ('__isMap' in obj) { debugLog('Converting object back to Map'); const map = new Map(); Object.entries(obj).forEach(([key, value]) => { if (key !== '__isMap') { map.set(key, objectToMapSet(value)); } }); return map; } if ('__isSet' in obj) { debugLog('Converting array back to Set'); return new Set(obj.values.map(objectToMapSet)); } if (Array.isArray(obj)) { return obj.map(objectToMapSet); } const newObj = {}; Object.entries(obj).forEach(([key, value]) => { newObj[key] = objectToMapSet(value); }); return newObj; } catch (error) { logError(error, 'objectToMapSet conversion failed'); throw error; } }; const showNotification = (message) => { if (typeof window !== 'undefined' && window.alert) { window.alert(message); } else { debugLog('Alert would be shown:', message); } }; class StoreAdapter { constructor(store) { this.store = store; this.unsubscribe = () => { }; if (this.constructor === StoreAdapter) { throw new Error("Abstract classes can't be instantiated."); } } destroy() { this.unsubscribe(); } } class VuexAdapter extends StoreAdapter { constructor(store) { super(store); this.store = store; } setup() { debugLog('Setting up Vuex adapter.'); if (this.store.hasModule('__tera_file_sync')) { debugLog('Vuex module __tera_file_sync already registered.'); return; } this.store.registerModule('__tera_file_sync', { namespaced: true, state: { saveStatus: SaveStatus.SAVED }, mutations: { updateSaveStatus(state, status) { state.saveStatus = status; }, }, getters: { getSaveStatus: (state) => state.saveStatus } }); } getState() { const _a = this.store.state, { __tera_file_sync } = _a, stateToSave = __rest(_a, ["__tera_file_sync"]); return stateToSave; } replaceState(newState) { this.store.replaceState(Object.assign(Object.assign({}, this.store.state), newState)); } updateSaveStatus(status) { this.store.commit('__tera_file_sync/updateSaveStatus', status); } subscribe(callback) { this.unsubscribe = this.store.subscribe((mutation) => { if (mutation.type.startsWith('__tera_file_sync/')) return; callback(mutation); }); } destroy() { super.destroy(); if (this.store.hasModule('__tera_file_sync')) { this.store.unregisterModule('__tera_file_sync'); debugLog('Unregistered Vuex module.'); } } } class PiniaAdapter extends StoreAdapter { constructor(store) { super(store); this.store = store; } setup() { debugLog('Setting up Pinia adapter.'); if (this.store.saveStatus === undefined) { throw new Error("Pinia store must have a 'saveStatus' property in its state for TERA File Sync to work."); } } getState() { const _a = this.store.$state, { saveStatus: _saveStatus } = _a, stateToSave = __rest(_a, ["saveStatus"]); return stateToSave; } replaceState(newState) { this.store.$patch(newState); } updateSaveStatus(status) { this.store.saveStatus = status; } subscribe(callback) { this.unsubscribe = this.store.$subscribe((mutation) => { if (mutation.events && mutation.events.key === 'saveStatus') return; callback(mutation); }); } } class PlainObjectAdapter extends StoreAdapter { constructor(store) { super(store); this.store = store; } setup() { debugLog('Setting up PlainObjectAdapter.'); } getState() { if (typeof this.store.getState !== 'function') { throw new Error("The provided object for PlainObjectAdapter must have a 'getState' method."); } return this.store.getState(); } replaceState(newState) { return __awaiter(this, void 0, void 0, function* () { if (typeof this.store.replaceState !== 'function') { throw new Error("The provided object for PlainObjectAdapter must have a 'replaceState' method."); } yield this.store.replaceState(newState); }); } updateSaveStatus(status) { if (typeof this.store.updateSaveStatus !== 'function') { throw new Error("The provided object for PlainObjectAdapter must have an 'updateSaveStatus' method."); } this.store.updateSaveStatus(status); } subscribe(callback) { if (typeof this.store.subscribe !== 'function') { throw new Error("The provided object for PlainObjectAdapter must have a 'subscribe' method."); } this.unsubscribe = this.store.subscribe(callback); } } class TeraFileSyncPlugin { get projectId() { var _a, _b, _c; if (!((_c = (_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.project) === null || _c === void 0 ? void 0 : _c.id)) { throw new Error("TERA project context is missing."); } return String(this.vueInstance.$tera.project.id); } get saveStatus() { return this._saveStatus; } constructor(config, adapter) { this.initialized = false; this.teraReady = false; this.vueInstance = null; this.userId = null; this.saveInProgress = false; this.autoSaveInterval = null; this.hasShownInitialAlert = false; this._saveStatus = SaveStatus.SAVED; const mergedConfig = Object.assign(Object.assign({}, DEFAULT_CONFIG), config); validateConfig(mergedConfig); this.config = mergedConfig; if (!(adapter instanceof StoreAdapter)) { throw new Error("A valid store adapter must be provided."); } this.adapter = adapter; this.adapter.setup(); this.keydownHandler = this.handleKeyDown.bind(this); this.beforeUnloadHandler = this.handleBeforeUnload.bind(this); } // --- Public API Methods --- setVueInstance(instance) { this.vueInstance = instance; } setTeraReady() { return __awaiter(this, void 0, void 0, function* () { if (!this.vueInstance) throw new Error("Vue instance must be set before calling setTeraReady."); validateVueInstance(this.vueInstance); this.teraReady = true; yield this.initializeState({ loadImmediately: this.config.loadImmediately }); this.setupAutoSave(); this.setupStateChangeTracking(); }); } saveState() { return __awaiter(this, void 0, void 0, function* () { return yield this.saveStateToFile(); }); } promptForNewJsonFile() { return __awaiter(this, void 0, void 0, function* () { return yield this.setStateFromPromptedJsonFile(); }); } loadAndApplyStateFromFile() { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; if (!((_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera)) { logError(new Error('TERA API not available'), 'State load failed.'); return false; } try { yield this.vueInstance.$tera.uiProgress({ title: 'Loading tool data', backdrop: 'static' }); const fileData = yield this.loadStateFromFile(); if (fileData) { // If a reset function is provided, call it before applying the state. if (this.config.resetState) { debugLog('Calling provided store reset function (async).'); yield this.config.resetState(); } // Extract file data and replace state const parsedState = objectToMapSet(fileData); yield this.adapter.replaceState(parsedState); debugLog('Store state replaced from file data.'); this.updateSaveStatus(SaveStatus.SAVED); return true; } else { debugLog('No file data found to load. Using default state.'); this.updateSaveStatus(SaveStatus.UNSAVED); return true; } } catch (error) { logError(error, 'State load and apply failed.'); showNotification('Error loading state from file.'); return false; } finally { if ((_c = (_b = this.vueInstance) === null || _b === void 0 ? void 0 : _b.$tera) === null || _c === void 0 ? void 0 : _c.uiProgress) { yield this.vueInstance.$tera.uiProgress(false); } } }); } getFileMetadata() { return __awaiter(this, void 0, void 0, function* () { if (!this.vueInstance) return null; try { const fileName = yield this.getStorageFileName(); if (!fileName) { debugLog('Cannot get metadata, no storage file name available.'); return null; } const fileObject = yield this.vueInstance.$tera.getProjectFile(fileName, { cache: false }); if (!fileObject) { debugLog(`Could not retrieve file metadata for ${fileName}.`); return null; } const metadata = { modified: new Date(fileObject.modified), }; debugLog(`Retrieved metadata for ${fileName}:`, metadata); return metadata; } catch (error) { if (error.message && error.message.includes('not found')) { debugLog('State file not found, no metadata available.'); return null; } logError(error, 'Failed to get file metadata'); return null; } }); } destroy() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); } this.unregisterHotkeys(); this.unregisterBeforeUnload(); this.adapter.destroy(); this.initialized = false; this.teraReady = false; } createDataFileBackup() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; if (!this.vueInstance) { logError(new Error('Vue instance not set'), 'Backup failed'); return false; } try { if (this.vueInstance.$tera.uiProgress) { yield this.vueInstance.$tera.uiProgress({ title: 'Creating backup...', backdrop: 'static' }); } // Get the data to backup let dataToBackup = mapSetToObject(this.adapter.getState()); if (!dataToBackup) { debugLog('No existing file found on disk for backup. Backing up current from disk instead.'); dataToBackup = yield this.loadStateFromFile(); } // Generate a formatted datetime string (YYYYMMDD-HHMMSS) const now = new Date(); const pad = (n) => n.toString().padStart(2, '0'); const current_datetime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; // Construct the backup filename const backupFileName = `data-${this.config.keyPrefix}-backup-${current_datetime}.json`; // Create the new file in TERA const backupFile = yield (0, p_retry_1.default)(() => this.vueInstance.$tera.createProjectFile(backupFileName), { retries: 2, minTimeout: 200, onFailedAttempt: error => debugLog(`[Create backup file attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`), }); // Write the data to the newly created file yield (0, p_retry_1.default)(() => backupFile.setContents(dataToBackup), { retries: 2, minTimeout: 200, onFailedAttempt: error => debugLog(`[Set backup contents attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`), }); debugLog(`Backup created successfully: ${backupFileName}`); // Optional: Show a success notification if Element UI or similar is available if (this.vueInstance.$notify) { this.vueInstance.$notify({ title: 'Backup Successful', message: `Backup saved as ${backupFileName}`, type: 'success' }); } return true; } catch (error) { logError(error, 'Failed to create data file backup'); showNotification('Failed to create backup file.'); return false; } finally { if ((_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.uiProgress) { yield this.vueInstance.$tera.uiProgress(false); } } }); } // --- Internal Methods --- handleKeyDown(event) { if ((event.ctrlKey || event.metaKey) && event.key === 's') { event.preventDefault(); debugLog('Ctrl+S hotkey detected, saving state'); this.saveStateToFile().then(success => { if (success) { debugLog('Save completed via hotkey'); } }); } } handleBeforeUnload(event) { if (this._saveStatus === SaveStatus.UNSAVED) { const message = 'You have unsaved changes. Are you sure you want to leave?'; event.returnValue = message; return message; } return undefined; } registerHotkeys() { if (!this.config.enableSaveHotkey) { debugLog('Save hotkey disabled in configuration'); return; } debugLog('Registering Ctrl+S hotkey'); if (typeof window !== 'undefined') { window.removeEventListener('keydown', this.keydownHandler); window.addEventListener('keydown', this.keydownHandler); } } unregisterHotkeys() { if (typeof window !== 'undefined') { window.removeEventListener('keydown', this.keydownHandler); debugLog('Unregistered hotkeys'); } } registerBeforeUnload() { if (typeof window !== 'undefined') { window.addEventListener('beforeunload', this.beforeUnloadHandler); debugLog('Registered beforeunload listener'); } } unregisterBeforeUnload() { if (typeof window !== 'undefined') { window.removeEventListener('beforeunload', this.beforeUnloadHandler); debugLog('Unregistered beforeunload listener'); } } showInitialAlert() { if (this.config.showInitialAlert && !this.hasShownInitialAlert) { this.hasShownInitialAlert = true; const message = "This TERA tool no longer automatically saves progress, use Ctrl+S, or click save in the top right corner, to save progress. (This is a short-term temporary pop-up, it will be removed at the start of April)"; if (this.vueInstance && this.vueInstance.$notify) { this.vueInstance.$notify({ title: 'Important', message, type: 'warning', duration: 10000, showClose: true, }); } else { setTimeout(() => showNotification(message), 1000); } debugLog('Showed initial manual save alert'); } } updateSaveStatus(status) { debugLog(`Updating save status: ${status}`); this._saveStatus = status; this.adapter.updateSaveStatus(status); } getStorageKey() { return __awaiter(this, void 0, void 0, function* () { if (this.config.isSeparateStateForEachUser) { if (!this.userId) { if (!this.vueInstance) throw new Error("Vue instance not available to get user ID."); try { const user = yield this.vueInstance.$tera.getUser(); this.userId = user.id; debugLog('User ID initialized:', this.userId); } catch (error) { logError(error, 'Failed to get user ID'); throw error; } } return `${this.config.keyPrefix}-${this.userId}`; } return `${this.config.keyPrefix}`; }); } getStorageFileName() { return __awaiter(this, arguments, void 0, function* ({ returnFullPath = false } = {}) { var _a, _b, _c; if (!((_c = (_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.project) === null || _c === void 0 ? void 0 : _c.id)) { throw new Error("Error getting fileStorageName: TERA project context is missing."); } if (!this.vueInstance.$tera.project.temp) { console.warn("Warning: $tera.project.temp is missing. Creating it."); this.vueInstance.$tera.project.temp = {}; } const key = yield this.getStorageKey(); let fileStorageName = this.vueInstance.$tera.project.temp[key]; if (!fileStorageName) { debugLog("No existing file for project/tool, creating one"); fileStorageName = `data-${this.config.keyPrefix}-${(0, nanoid_1.nanoid)()}.json`; const newFile = yield (0, p_retry_1.default)(() => this.vueInstance.$tera.createProjectFile(fileStorageName), { retries: 2, minTimeout: 200, onFailedAttempt: error => debugLog(`[Create file attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`), }); yield (0, p_retry_1.default)(() => newFile.setContents(this.adapter.getState()), { retries: 2, minTimeout: 200, onFailedAttempt: error => debugLog(`[Set contents attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`), }); yield (0, p_retry_1.default)(() => this.vueInstance.$tera.setProjectState(`temp.${key}`, fileStorageName), { retries: 2, minTimeout: 200, onFailedAttempt: error => debugLog(`[Set storage key attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`), }); } if (typeof fileStorageName !== 'string') { throw new Error(`fileStorageName is not a string: ${fileStorageName}`); } return returnFullPath ? `${this.vueInstance.$tera.project.id}/${fileStorageName}` : fileStorageName; }); } loadStateFromFile() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; if (!this.vueInstance) return null; let fileName = ''; try { let fileContent; yield (0, p_retry_1.default)(() => __awaiter(this, void 0, void 0, function* () { fileName = yield this.getStorageFileName(); debugLog(`Loading state from file: ${fileName}`); if (!fileName) throw new Error('No file name returned when expected!'); if (!this.vueInstance) throw new Error('this.vueInstance expected!'); fileContent = yield api.getFileContent(this.projectId, fileName, this.vueInstance.$tera); }), { retries: 3, minTimeout: 1000, factor: 2, onFailedAttempt: error => debugLog(`[Load Attempt ${error.attemptNumber}] Failed for ${fileName}. Retries left: ${error.retriesLeft}.`), }); if (!fileContent) { debugLog('File not found or empty'); return null; } debugLog('State loaded from file successfully:', fileContent); return fileContent; } catch (error) { if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('not found')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes("Unexpected end of JSON input"))) { debugLog('State file not found or corrupt, will be created on first save'); return null; } logError(error, 'Failed to load state from file'); showNotification('Failed to load state from file, using default state'); return null; } }); } saveStateToFile() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; if (!this.vueInstance) return false; if (this.config.onBeforeSave) { const validationResult = this.config.onBeforeSave(); if (validationResult !== true) { debugLog('Save prevented by onBeforeSave hook. Reason:', validationResult); if (typeof validationResult === 'string' && validationResult.length > 0) { showNotification(validationResult); } return false; } } if (this.saveInProgress) { debugLog('Save already in progress, skipping'); return false; } const state = this.adapter.getState(); let fileName = ''; try { this.saveInProgress = true; this.updateSaveStatus(SaveStatus.SAVING); yield this.vueInstance.$tera.uiProgress({ title: 'Saving tool data', backdrop: 'static' }); yield (0, p_retry_1.default)(() => __awaiter(this, void 0, void 0, function* () { var _a; fileName = yield this.getStorageFileName(); if (!fileName) throw new Error('No fileName returned'); const stateToSave = mapSetToObject(state); if (!this.vueInstance) throw new Error('this.vueInstance expected!'); yield api.saveFileContent(this.projectId, fileName, stateToSave, (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera); }), { retries: 3, minTimeout: 1000, factor: 2, onFailedAttempt: error => debugLog(`[Save Attempt ${error.attemptNumber}] Failed for ${fileName}. Retries left: ${error.retriesLeft}.`), }); this.updateSaveStatus(SaveStatus.SAVED); debugLog(`State saved to file: ${fileName}`); return true; } catch (error) { logError(error, 'Failed to save state to file'); showNotification('Failed to save state to file, hit F12 for debug information.'); this.updateSaveStatus(SaveStatus.UNSAVED); return false; } finally { this.saveInProgress = false; if ((_b = (_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera) === null || _b === void 0 ? void 0 : _b.uiProgress) { yield this.vueInstance.$tera.uiProgress(false); } } }); } initializeState() { return __awaiter(this, arguments, void 0, function* ({ loadImmediately = true } = {}) { var _a; if (!this.teraReady || !((_a = this.vueInstance) === null || _a === void 0 ? void 0 : _a.$tera)) { debugLog('TERA not ready, skipping initialization'); return; } // Only show UI progress if we are actually going to load immediately if (loadImmediately && this.vueInstance.$tera.uiProgress) { yield this.vueInstance.$tera.uiProgress({ title: 'Loading tool data', backdrop: 'static' }); } try { if (loadImmediately) { debugLog('Initializing with immediate load from file.'); yield this.loadAndApplyStateFromFile(); } else { debugLog('Skipping immediate load. State will be considered unsaved until loaded.'); this.updateSaveStatus(SaveStatus.UNSAVED); } this.showInitialAlert(); this.registerHotkeys(); this.registerBeforeUnload(); } catch (error) { logError(error, 'State initialization failed'); showNotification('Error initializing state from file'); } finally { this.initialized = true; // Only hide UI progress if we showed it in the first place if (loadImmediately && this.vueInstance.$tera.uiProgress) { yield this.vueInstance.$tera.uiProgress(false); } } }); } setupAutoSave() { if (this.config.autoSaveIntervalMinutes <= 0) { debugLog('Auto-save disabled'); return; } if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); } const intervalMs = this.config.autoSaveIntervalMinutes * 60 * 1000; debugLog(`Setting up auto-save every ${this.config.autoSaveIntervalMinutes} minutes`); this.autoSaveInterval = setInterval(() => { if (this._saveStatus !== SaveStatus.SAVED) { debugLog('Auto-save triggered'); this.saveStateToFile(); } else { debugLog('Auto-save skipped - no changes detected'); } }, intervalMs); } setupStateChangeTracking() { this.adapter.subscribe(() => { if (!this.saveInProgress) { this.updateSaveStatus(SaveStatus.UNSAVED); } }); } setStateFromPromptedJsonFile() { return __awaiter(this, void 0, void 0, function* () { if (!this.vueInstance) return; try { const projectFile = yield this.vueInstance.$tera.selectProjectFile({ title: 'Load JSON file', showHiddenFiles: true, filters: { ext: 'json', // Only show files with the 'json' extension }, }); if (!projectFile) { debugLog('User cancelled file selection.'); return; } const fileData = yield (0, p_retry_1.default)(() => projectFile.getContents({ format: 'json' }), { retries: 3, minTimeout: 1000, onFailedAttempt: error => debugLog(`[Load Prompted File Attempt ${error.attemptNumber}] Failed. Retries left: ${error.retriesLeft}.`), }); if (!fileData) { debugLog('Selected file is empty.'); showNotification('The selected file is empty.'); return; } const parsedState = objectToMapSet(fileData); yield this.adapter.replaceState(parsedState); const key = yield this.getStorageKey(); // Clear the current file association in TERA's project state (both local cache and persistent state). // This ensures the plugin no longer points to the previous file, nor does it link to the // specific file the user just selected. if (this.vueInstance.$tera.project.temp) { delete this.vueInstance.$tera.project.temp[key]; } yield this.vueInstance.$tera.setProjectState(`temp.${key}`, null); // Trigger a save. Since the key is cleared above, getStorageFileName will: // 1. Generate a NEW unique filename (data-prefix-nanoid.json) // 2. Create that file // 3. Write the current store state (the imported data) to it // 4. Set the temp key to point to this NEW file debugLog('Store initialized from prompted file data. Saving to new copy...'); yield this.saveStateToFile(); this.updateSaveStatus(SaveStatus.SAVED); } catch (error) { logError(error, 'Failed to set state from prompted JSON file'); showNotification('Failed to load data from the selected file.'); this.updateSaveStatus(SaveStatus.UNSAVED); } }); } } /** * Creates and initializes a new TERA file sync manager. * This is the main entry point for using the plugin. * * @param config - Plugin configuration options. * @param store - The Vuex store, Pinia store, or a custom aggregator object that conforms to the `PlainObjectStore` interface. * @returns The plugin instance, providing a public API to manage state synchronization. */ function createTeraSync(config, store) { let adapter; // Type-safe "sniffing" to determine the store type. if (store && typeof store.getState === 'function' && typeof store.replaceState === 'function' && typeof store.updateSaveStatus === 'function' && typeof store.subscribe === 'function') { debugLog('Detected custom aggregator object. Using PlainObjectAdapter.'); adapter = new PlainObjectAdapter(store); } else if (store && typeof store.$id === 'string' && typeof store.$patch === 'function') { debugLog('Detected Pinia store. Using PiniaAdapter.'); adapter = new PiniaAdapter(store); } else if (store && typeof store.commit === 'function' && typeof store.subscribe === 'function') { debugLog('Detected Vuex store. Using VuexAdapter.'); adapter = new VuexAdapter(store); } else { throw new Error('Could not determine store type. Please provide a valid Vuex store, Pinia store, or a custom aggregator object.'); } const plugin = new TeraFileSyncPlugin(config, adapter); return plugin; } //# sourceMappingURL=index.js.map