Import export
This commit is contained in:
211
src/lib/components/BackupPanel.svelte
Normal file
211
src/lib/components/BackupPanel.svelte
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { backupManager } from '$lib/utils/BackupManager';
|
||||||
|
|
||||||
|
let isExporting = false;
|
||||||
|
let isImporting = false;
|
||||||
|
let importError = '';
|
||||||
|
let importSuccess = '';
|
||||||
|
let backupStats = null;
|
||||||
|
|
||||||
|
// Handle export button click
|
||||||
|
async function handleExport() {
|
||||||
|
isExporting = true;
|
||||||
|
importError = '';
|
||||||
|
importSuccess = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
backupManager.exportBackup();
|
||||||
|
importSuccess = 'Backup exported successfully!';
|
||||||
|
} catch (error) {
|
||||||
|
importError = `Export failed: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
isExporting = false;
|
||||||
|
|
||||||
|
// Clear success message after a few seconds
|
||||||
|
if (importSuccess) {
|
||||||
|
setTimeout(() => {
|
||||||
|
importSuccess = '';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle import button click
|
||||||
|
function initiateImport() {
|
||||||
|
importError = '';
|
||||||
|
importSuccess = '';
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
isImporting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
try {
|
||||||
|
const json = event.target.result as string;
|
||||||
|
await backupManager.importBackup(json);
|
||||||
|
|
||||||
|
importSuccess = 'Backup imported successfully! The page will reload in 2 seconds.';
|
||||||
|
|
||||||
|
// Reload the page after a short delay to ensure all stores are updated
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 2000);
|
||||||
|
} catch (error) {
|
||||||
|
importError = `Import failed: ${error.message}`;
|
||||||
|
isImporting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
importError = 'Failed to read the backup file';
|
||||||
|
isImporting = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
} catch (error) {
|
||||||
|
importError = `Import failed: ${error.message}`;
|
||||||
|
isImporting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get backup statistics
|
||||||
|
function getBackupStats() {
|
||||||
|
try {
|
||||||
|
const backup = backupManager.createBackup();
|
||||||
|
const profileCount = backup.data.profiles?.length || 0;
|
||||||
|
const triggerCount = backup.data.triggers?.length || 0;
|
||||||
|
const hasSettings = !!backup.data.settings;
|
||||||
|
|
||||||
|
backupStats = {
|
||||||
|
profileCount,
|
||||||
|
triggerCount,
|
||||||
|
hasSettings,
|
||||||
|
timestamp: new Date().toLocaleString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get backup stats:', error);
|
||||||
|
backupStats = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
getBackupStats();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="backup-panel">
|
||||||
|
<h4>Backup & Restore</h4>
|
||||||
|
|
||||||
|
<div class="backup-info">
|
||||||
|
<p>
|
||||||
|
Create a complete backup of all your MUD client data, including profiles, triggers, and settings.
|
||||||
|
You can use this backup to restore your configuration on another device or browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if backupStats}
|
||||||
|
<div class="backup-stats">
|
||||||
|
<strong>Current data:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>{backupStats.profileCount} profile{backupStats.profileCount !== 1 ? 's' : ''}</li>
|
||||||
|
<li>{backupStats.triggerCount} trigger{backupStats.triggerCount !== 1 ? 's' : ''}</li>
|
||||||
|
<li>Settings: {backupStats.hasSettings ? 'Yes' : 'No'}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if importError}
|
||||||
|
<div class="error-message">
|
||||||
|
{importError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if importSuccess}
|
||||||
|
<div class="success-message">
|
||||||
|
{importSuccess}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="backup-actions">
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={handleExport}
|
||||||
|
disabled={isExporting || isImporting}
|
||||||
|
aria-busy={isExporting}>
|
||||||
|
{isExporting ? 'Exporting...' : 'Export Backup'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
on:click={initiateImport}
|
||||||
|
disabled={isExporting || isImporting}
|
||||||
|
aria-busy={isImporting}>
|
||||||
|
{isImporting ? 'Importing...' : 'Import Backup'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.backup-panel {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-input-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-info {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-stats {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-stats ul {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backup-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: #721c24;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: #155724;
|
||||||
|
background-color: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
371
src/lib/components/SettingsPanel.svelte
Normal file
371
src/lib/components/SettingsPanel.svelte
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { uiSettings, accessibilitySettings } from '$lib/stores/mudStore';
|
||||||
|
import { settingsManager } from '$lib/settings/SettingsManager';
|
||||||
|
import BackupPanel from './BackupPanel.svelte';
|
||||||
|
|
||||||
|
// Declare global window property for volume debounce
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
volumeDebounceTimeout?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset settings to defaults
|
||||||
|
function resetSettings() {
|
||||||
|
if (confirm('Are you sure you want to reset all settings to defaults?')) {
|
||||||
|
settingsManager.resetSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export settings to JSON
|
||||||
|
function exportSettings() {
|
||||||
|
const settingsJson = settingsManager.exportSettings();
|
||||||
|
const blob = new Blob([settingsJson], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create a link and trigger download
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'svelte-mud-settings.json';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import settings from JSON file
|
||||||
|
function importSettings() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const json = event.target.result as string;
|
||||||
|
settingsManager.importSettings(json);
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Failed to import settings: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Ensure settings are loaded from localStorage on component mount
|
||||||
|
// This ensures the UI displays the values stored in localStorage
|
||||||
|
// No need to explicitly set the uiSettings and accessibilitySettings store values
|
||||||
|
// as settingsManager already does this during initialization
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-list">
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Dark Mode</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$uiSettings.isDarkMode}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Show Timestamps</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$uiSettings.showTimestamps}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">ANSI Colors</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$uiSettings.ansiColor}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Font</span>
|
||||||
|
<select bind:value={$uiSettings.font} class="form-control">
|
||||||
|
<option value="monospace">Monospace</option>
|
||||||
|
<option value="'Courier New', monospace">Courier New</option>
|
||||||
|
<option value="'Roboto Mono', monospace">Roboto Mono</option>
|
||||||
|
<option value="'Source Code Pro', monospace">Source Code Pro</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Global Sound Volume</span>
|
||||||
|
<div class="range-control">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
on:input={(e) => {
|
||||||
|
// Debounce volume changes to avoid performance issues with rapid changes
|
||||||
|
if (window.volumeDebounceTimeout) {
|
||||||
|
clearTimeout(window.volumeDebounceTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read value directly from the input
|
||||||
|
const newVolume = parseFloat(e.target.value);
|
||||||
|
|
||||||
|
// Update the store with a slight delay to avoid excessive updates
|
||||||
|
window.volumeDebounceTimeout = setTimeout(() => {
|
||||||
|
uiSettings.update(settings => ({
|
||||||
|
...settings,
|
||||||
|
globalVolume: newVolume
|
||||||
|
}));
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
|
bind:value={$uiSettings.globalVolume}
|
||||||
|
>
|
||||||
|
<span class="range-value">{($uiSettings.globalVolume * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Debugging</h4>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Show GMCP Messages</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$uiSettings.debugGmcp}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<div class="setting-description">
|
||||||
|
Shows GMCP protocol messages in the output window for debugging
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>Accessibility</h4>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Text-to-Speech</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$accessibilitySettings.textToSpeech}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">High Contrast</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$accessibilitySettings.highContrast}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Font Size</span>
|
||||||
|
<div class="range-control">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="12"
|
||||||
|
max="24"
|
||||||
|
step="1"
|
||||||
|
bind:value={$accessibilitySettings.fontSize}
|
||||||
|
>
|
||||||
|
<span class="range-value">{$accessibilitySettings.fontSize}px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $accessibilitySettings.textToSpeech}
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Interrupt Speech on Enter</span>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" bind:checked={$accessibilitySettings.interruptSpeechOnEnter}>
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
<div class="setting-description">
|
||||||
|
Automatically stop speaking when the Enter key is pressed
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Speech Rate</span>
|
||||||
|
<div class="range-control">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
bind:value={$accessibilitySettings.speechRate}
|
||||||
|
>
|
||||||
|
<span class="range-value">{$accessibilitySettings.speechRate.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Speech Pitch</span>
|
||||||
|
<div class="range-control">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
bind:value={$accessibilitySettings.speechPitch}
|
||||||
|
>
|
||||||
|
<span class="range-value">{$accessibilitySettings.speechPitch.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<span class="setting-name">Speech Volume</span>
|
||||||
|
<div class="range-control">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
bind:value={$accessibilitySettings.speechVolume}
|
||||||
|
>
|
||||||
|
<span class="range-value">{($accessibilitySettings.speechVolume * 100).toFixed(0)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h4>Settings Management</h4>
|
||||||
|
|
||||||
|
<div class="settings-actions">
|
||||||
|
<button class="btn btn-sm" on:click={exportSettings}>Export Settings Only</button>
|
||||||
|
<button class="btn btn-sm" on:click={importSettings}>Import Settings Only</button>
|
||||||
|
<button class="btn btn-sm btn-danger" on:click={resetSettings}>Reset to Defaults</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BackupPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-panel {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-input-bg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-description {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Switch styling */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-value {
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
251
src/lib/settings/SettingsManager.ts
Normal file
251
src/lib/settings/SettingsManager.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { EventEmitter } from '$lib/utils/EventEmitter';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { accessibilitySettings, uiSettings } from '$lib/stores/mudStore';
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
accessibility: {
|
||||||
|
textToSpeech: boolean;
|
||||||
|
highContrast: boolean;
|
||||||
|
fontSize: number;
|
||||||
|
lineSpacing: number;
|
||||||
|
speechRate: number;
|
||||||
|
speechPitch: number;
|
||||||
|
speechVolume: number;
|
||||||
|
interruptSpeechOnEnter: boolean;
|
||||||
|
};
|
||||||
|
ui: {
|
||||||
|
isDarkMode: boolean;
|
||||||
|
showTimestamps: boolean;
|
||||||
|
showSidebar: boolean;
|
||||||
|
splitViewDirection: 'horizontal' | 'vertical';
|
||||||
|
inputHistorySize: number;
|
||||||
|
outputBufferSize: number;
|
||||||
|
ansiColor: boolean;
|
||||||
|
font: string;
|
||||||
|
debugGmcp: boolean;
|
||||||
|
globalVolume: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SettingsManager extends EventEmitter {
|
||||||
|
private settings: Settings;
|
||||||
|
private readonly STORAGE_KEY = 'svelte-mud-settings';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Initialize with default settings
|
||||||
|
this.settings = {
|
||||||
|
accessibility: {
|
||||||
|
textToSpeech: false,
|
||||||
|
highContrast: false,
|
||||||
|
fontSize: 16,
|
||||||
|
lineSpacing: 1.2,
|
||||||
|
speechRate: 1,
|
||||||
|
speechPitch: 1,
|
||||||
|
speechVolume: 1,
|
||||||
|
interruptSpeechOnEnter: true
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
isDarkMode: true,
|
||||||
|
showTimestamps: true,
|
||||||
|
showSidebar: true,
|
||||||
|
splitViewDirection: 'horizontal',
|
||||||
|
inputHistorySize: 100,
|
||||||
|
outputBufferSize: 1000,
|
||||||
|
ansiColor: true,
|
||||||
|
font: 'monospace',
|
||||||
|
debugGmcp: false,
|
||||||
|
globalVolume: 0.7
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load settings from storage
|
||||||
|
this.loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load settings from localStorage
|
||||||
|
*/
|
||||||
|
private loadSettings(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return; // Skip during SSR
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedSettings = localStorage.getItem(this.STORAGE_KEY);
|
||||||
|
|
||||||
|
if (storedSettings) {
|
||||||
|
const parsedSettings = JSON.parse(storedSettings);
|
||||||
|
|
||||||
|
// Merge with defaults to ensure all properties exist
|
||||||
|
this.settings = {
|
||||||
|
accessibility: {
|
||||||
|
...this.settings.accessibility,
|
||||||
|
...parsedSettings.accessibility
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
...this.settings.ui,
|
||||||
|
...parsedSettings.ui
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Loaded settings from localStorage:', this.settings);
|
||||||
|
|
||||||
|
// Update Svelte stores with loaded settings
|
||||||
|
accessibilitySettings.set(this.settings.accessibility);
|
||||||
|
uiSettings.set(this.settings.ui);
|
||||||
|
|
||||||
|
this.emit('settingsLoaded', this.settings);
|
||||||
|
} else {
|
||||||
|
console.log('No settings found in localStorage, using defaults');
|
||||||
|
// Update Svelte stores with default settings
|
||||||
|
accessibilitySettings.set(this.settings.accessibility);
|
||||||
|
uiSettings.set(this.settings.ui);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load settings from localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save settings to localStorage
|
||||||
|
*/
|
||||||
|
public saveSettings(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return; // Skip during SSR
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current values from stores
|
||||||
|
this.settings.accessibility = get(accessibilitySettings);
|
||||||
|
this.settings.ui = get(uiSettings);
|
||||||
|
|
||||||
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.settings));
|
||||||
|
console.log('Saved settings to localStorage:', this.settings);
|
||||||
|
|
||||||
|
this.emit('settingsSaved', this.settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings to localStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update accessibility settings
|
||||||
|
*/
|
||||||
|
public updateAccessibilitySettings(newSettings: Partial<Settings['accessibility']>): void {
|
||||||
|
accessibilitySettings.update(current => {
|
||||||
|
const updated = { ...current, ...newSettings };
|
||||||
|
this.settings.accessibility = updated;
|
||||||
|
this.saveSettings();
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('accessibilitySettingsUpdated', get(accessibilitySettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI settings
|
||||||
|
*/
|
||||||
|
public updateUiSettings(newSettings: Partial<Settings['ui']>): void {
|
||||||
|
uiSettings.update(current => {
|
||||||
|
const updated = { ...current, ...newSettings };
|
||||||
|
this.settings.ui = updated;
|
||||||
|
this.saveSettings();
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('uiSettingsUpdated', get(uiSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset settings to defaults
|
||||||
|
*/
|
||||||
|
public resetSettings(): void {
|
||||||
|
// Create default settings
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
accessibility: {
|
||||||
|
textToSpeech: false,
|
||||||
|
highContrast: false,
|
||||||
|
fontSize: 16,
|
||||||
|
lineSpacing: 1.2,
|
||||||
|
speechRate: 1,
|
||||||
|
speechPitch: 1,
|
||||||
|
speechVolume: 1,
|
||||||
|
interruptSpeechOnEnter: true
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
isDarkMode: true,
|
||||||
|
showTimestamps: true,
|
||||||
|
showSidebar: true,
|
||||||
|
splitViewDirection: 'horizontal',
|
||||||
|
inputHistorySize: 100,
|
||||||
|
outputBufferSize: 1000,
|
||||||
|
ansiColor: true,
|
||||||
|
font: 'monospace',
|
||||||
|
debugGmcp: false,
|
||||||
|
globalVolume: 0.7
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update stores
|
||||||
|
accessibilitySettings.set(defaultSettings.accessibility);
|
||||||
|
uiSettings.set(defaultSettings.ui);
|
||||||
|
|
||||||
|
// Update internal settings and save
|
||||||
|
this.settings = defaultSettings;
|
||||||
|
this.saveSettings();
|
||||||
|
|
||||||
|
this.emit('settingsReset', defaultSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import settings from a JSON string
|
||||||
|
*/
|
||||||
|
public importSettings(json: string): void {
|
||||||
|
try {
|
||||||
|
const imported = JSON.parse(json);
|
||||||
|
|
||||||
|
if (typeof imported === 'object' && imported !== null) {
|
||||||
|
// Create a valid settings object with defaults for missing properties
|
||||||
|
const newSettings: Settings = {
|
||||||
|
accessibility: {
|
||||||
|
...this.settings.accessibility,
|
||||||
|
...(imported.accessibility || {})
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
...this.settings.ui,
|
||||||
|
...(imported.ui || {})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update stores
|
||||||
|
accessibilitySettings.set(newSettings.accessibility);
|
||||||
|
uiSettings.set(newSettings.ui);
|
||||||
|
|
||||||
|
// Update internal settings and save
|
||||||
|
this.settings = newSettings;
|
||||||
|
this.saveSettings();
|
||||||
|
|
||||||
|
this.emit('settingsImported', newSettings);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to import settings:', error);
|
||||||
|
throw new Error('Failed to import settings. Invalid format.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export settings to a JSON string
|
||||||
|
*/
|
||||||
|
public exportSettings(): string {
|
||||||
|
// Get current values from stores
|
||||||
|
this.settings.accessibility = get(accessibilitySettings);
|
||||||
|
this.settings.ui = get(uiSettings);
|
||||||
|
|
||||||
|
return JSON.stringify(this.settings, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
export const settingsManager = new SettingsManager();
|
||||||
47
src/lib/settings/settingsTest.js
Normal file
47
src/lib/settings/settingsTest.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// This is a simple browser console test that you can run to verify settings localStorage functionality
|
||||||
|
// Copy and paste this into the browser console after loading the application
|
||||||
|
|
||||||
|
function testSettingsStorage() {
|
||||||
|
console.log("=== Settings Storage Test ===");
|
||||||
|
|
||||||
|
// Get current settings from localStorage
|
||||||
|
const currentSettings = localStorage.getItem('svelte-mud-settings');
|
||||||
|
console.log("Current settings in localStorage:", currentSettings ? JSON.parse(currentSettings) : "None");
|
||||||
|
|
||||||
|
// Get settings from the stores
|
||||||
|
const mudStore = (window.mudStore || {});
|
||||||
|
if (!mudStore.uiSettings || !mudStore.accessibilitySettings) {
|
||||||
|
console.error("Could not access mudStore. Make sure it's exposed to window in development.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle dark mode
|
||||||
|
const isDarkMode = mudStore.uiSettings.isDarkMode;
|
||||||
|
console.log(`Current dark mode: ${isDarkMode}, toggling to ${!isDarkMode}`);
|
||||||
|
mudStore.uiSettings.isDarkMode = !isDarkMode;
|
||||||
|
|
||||||
|
// Check localStorage again after changes
|
||||||
|
setTimeout(() => {
|
||||||
|
const updatedSettings = localStorage.getItem('svelte-mud-settings');
|
||||||
|
console.log("Updated settings in localStorage:", updatedSettings ? JSON.parse(updatedSettings) : "None");
|
||||||
|
|
||||||
|
if (!updatedSettings) {
|
||||||
|
console.error("Settings were not saved to localStorage!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedSettings = JSON.parse(updatedSettings);
|
||||||
|
if (parsedSettings.ui.isDarkMode !== !isDarkMode) {
|
||||||
|
console.error("Dark mode setting was not updated correctly in localStorage!");
|
||||||
|
} else {
|
||||||
|
console.log("✅ Settings were properly saved to localStorage");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert the change
|
||||||
|
console.log("Reverting dark mode setting...");
|
||||||
|
mudStore.uiSettings.isDarkMode = isDarkMode;
|
||||||
|
}, 200); // Wait for the debounce timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testSettingsStorage();
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { writable, derived, get } from 'svelte/store';
|
import { writable, derived, get } from 'svelte/store';
|
||||||
|
import { settingsManager } from '$lib/settings/SettingsManager';
|
||||||
import type { MudProfile } from '$lib/profiles/ProfileManager';
|
import type { MudProfile } from '$lib/profiles/ProfileManager';
|
||||||
import type { MudConnection } from '$lib/connection/MudConnection';
|
import type { MudConnection } from '$lib/connection/MudConnection';
|
||||||
import type { Trigger } from '$lib/triggers/TriggerSystem';
|
import type { Trigger } from '$lib/triggers/TriggerSystem';
|
||||||
@@ -53,6 +54,23 @@ export const uiSettings = writable({
|
|||||||
globalVolume: 0.7 // Global volume control for sounds (0-1)
|
globalVolume: 0.7 // Global volume control for sounds (0-1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Subscribe to settings changes to save to localStorage
|
||||||
|
accessibilitySettings.subscribe(value => {
|
||||||
|
// Skip during SSR
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// Use a small timeout to batch multiple rapid changes
|
||||||
|
setTimeout(() => settingsManager.saveSettings(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uiSettings.subscribe(value => {
|
||||||
|
// Skip during SSR
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// Use a small timeout to batch multiple rapid changes
|
||||||
|
setTimeout(() => settingsManager.saveSettings(), 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Store for input history
|
// Store for input history
|
||||||
export const inputHistory = writable<string[]>([]);
|
export const inputHistory = writable<string[]>([]);
|
||||||
export const inputHistoryIndex = writable<number>(-1);
|
export const inputHistoryIndex = writable<number>(-1);
|
||||||
|
|||||||
266
src/lib/utils/BackupManager.ts
Normal file
266
src/lib/utils/BackupManager.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { EventEmitter } from '$lib/utils/EventEmitter';
|
||||||
|
import { ProfileManager } from '$lib/profiles/ProfileManager';
|
||||||
|
import { TriggerSystem } from '$lib/triggers/TriggerSystem';
|
||||||
|
import { settingsManager } from '$lib/settings/SettingsManager';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { profiles, triggers, accessibilitySettings, uiSettings } from '$lib/stores/mudStore';
|
||||||
|
|
||||||
|
// Storage keys used by the app
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
PROFILES: 'svelte-mud-profiles',
|
||||||
|
TRIGGERS: 'mud-triggers',
|
||||||
|
SETTINGS: 'svelte-mud-settings'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current backup format version
|
||||||
|
const BACKUP_FORMAT_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
// Interface for backup file format
|
||||||
|
interface BackupData {
|
||||||
|
version: string;
|
||||||
|
timestamp: number;
|
||||||
|
data: {
|
||||||
|
profiles?: any;
|
||||||
|
triggers?: any;
|
||||||
|
settings?: any;
|
||||||
|
[key: string]: any; // Allow for future storage items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BackupManager extends EventEmitter {
|
||||||
|
private profileManager: ProfileManager;
|
||||||
|
private triggerSystem: TriggerSystem;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// We'll initialize these when needed to avoid initialization order issues
|
||||||
|
this.profileManager = null;
|
||||||
|
this.triggerSystem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize managers if needed
|
||||||
|
*/
|
||||||
|
private ensureManagers() {
|
||||||
|
if (!this.profileManager) {
|
||||||
|
this.profileManager = new ProfileManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.triggerSystem) {
|
||||||
|
this.triggerSystem = new TriggerSystem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a full backup of all app data
|
||||||
|
*/
|
||||||
|
public createBackup(): BackupData {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
throw new Error('Cannot create backup during server-side rendering');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup object
|
||||||
|
const backup: BackupData = {
|
||||||
|
version: BACKUP_FORMAT_VERSION,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all localStorage items that match our known keys
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
|
||||||
|
// Skip items that don't look like our app data
|
||||||
|
if (!key.startsWith('svelte-mud-') && !key.startsWith('mud-')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = localStorage.getItem(key);
|
||||||
|
if (value) {
|
||||||
|
// Parse the JSON value
|
||||||
|
const parsedValue = JSON.parse(value);
|
||||||
|
|
||||||
|
// Store by category
|
||||||
|
if (key === STORAGE_KEYS.PROFILES) {
|
||||||
|
backup.data.profiles = parsedValue;
|
||||||
|
} else if (key === STORAGE_KEYS.TRIGGERS) {
|
||||||
|
backup.data.triggers = parsedValue;
|
||||||
|
} else if (key === STORAGE_KEYS.SETTINGS) {
|
||||||
|
backup.data.settings = parsedValue;
|
||||||
|
} else {
|
||||||
|
// Store other items directly by key
|
||||||
|
backup.data[key] = parsedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to parse localStorage item ${key}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export backup as JSON file
|
||||||
|
*/
|
||||||
|
public exportBackup(): void {
|
||||||
|
try {
|
||||||
|
const backup = this.createBackup();
|
||||||
|
const backupJson = JSON.stringify(backup, null, 2);
|
||||||
|
const blob = new Blob([backupJson], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create a link to download the backup
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
|
||||||
|
// Format date for filename
|
||||||
|
const date = new Date();
|
||||||
|
const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
|
|
||||||
|
a.download = `svelte-mud-backup-${dateStr}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
this.emit('backupExported', backup);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export backup:', error);
|
||||||
|
throw new Error(`Failed to export backup: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import backup from a JSON string
|
||||||
|
*/
|
||||||
|
public async importBackup(json: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Parse backup data
|
||||||
|
const backup = JSON.parse(json) as BackupData;
|
||||||
|
|
||||||
|
// Verify format version
|
||||||
|
if (!backup.version) {
|
||||||
|
throw new Error('Invalid backup file format: Missing version');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the backup is too new for this version of the app
|
||||||
|
// Simple semver major version check
|
||||||
|
const backupMajorVersion = parseInt(backup.version.split('.')[0], 10);
|
||||||
|
const currentMajorVersion = parseInt(BACKUP_FORMAT_VERSION.split('.')[0], 10);
|
||||||
|
|
||||||
|
if (backupMajorVersion > currentMajorVersion) {
|
||||||
|
throw new Error(`Backup file version (${backup.version}) is newer than the app can handle (${BACKUP_FORMAT_VERSION})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the managers initialized
|
||||||
|
this.ensureManagers();
|
||||||
|
|
||||||
|
// Restore profiles
|
||||||
|
if (backup.data.profiles) {
|
||||||
|
await this.restoreProfiles(backup.data.profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore triggers
|
||||||
|
if (backup.data.triggers) {
|
||||||
|
await this.restoreTriggers(backup.data.triggers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore settings
|
||||||
|
if (backup.data.settings) {
|
||||||
|
await this.restoreSettings(backup.data.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore any other data
|
||||||
|
for (const key in backup.data) {
|
||||||
|
if (
|
||||||
|
key !== 'profiles' &&
|
||||||
|
key !== 'triggers' &&
|
||||||
|
key !== 'settings' &&
|
||||||
|
key.startsWith('svelte-mud-') || key.startsWith('mud-')
|
||||||
|
) {
|
||||||
|
localStorage.setItem(key, JSON.stringify(backup.data[key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('backupImported', backup);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to import backup:', error);
|
||||||
|
throw new Error(`Failed to import backup: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore profiles from backup
|
||||||
|
*/
|
||||||
|
private async restoreProfiles(profilesData: any[]): Promise<void> {
|
||||||
|
if (!Array.isArray(profilesData)) {
|
||||||
|
throw new Error('Invalid profiles data: Expected array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing profiles
|
||||||
|
localStorage.setItem(STORAGE_KEYS.PROFILES, JSON.stringify([]));
|
||||||
|
|
||||||
|
// Import each profile
|
||||||
|
for (const profile of profilesData) {
|
||||||
|
this.profileManager.addProfile(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the profiles store
|
||||||
|
profiles.set(this.profileManager.getProfiles());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore triggers from backup
|
||||||
|
*/
|
||||||
|
private async restoreTriggers(triggersData: any[]): Promise<void> {
|
||||||
|
if (!Array.isArray(triggersData)) {
|
||||||
|
throw new Error('Invalid triggers data: Expected array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing triggers
|
||||||
|
localStorage.setItem(STORAGE_KEYS.TRIGGERS, JSON.stringify([]));
|
||||||
|
|
||||||
|
// Import each trigger
|
||||||
|
for (const trigger of triggersData) {
|
||||||
|
this.triggerSystem.addTrigger(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the triggers store
|
||||||
|
triggers.set(this.triggerSystem.getTriggers());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore settings from backup
|
||||||
|
*/
|
||||||
|
private async restoreSettings(settingsData: any): Promise<void> {
|
||||||
|
if (typeof settingsData !== 'object' || settingsData === null) {
|
||||||
|
throw new Error('Invalid settings data: Expected object');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update settings in localStorage
|
||||||
|
localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(settingsData));
|
||||||
|
|
||||||
|
// Update the settings stores
|
||||||
|
if (settingsData.accessibility) {
|
||||||
|
accessibilitySettings.set(settingsData.accessibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsData.ui) {
|
||||||
|
uiSettings.set(settingsData.ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure settings manager knows about the changes
|
||||||
|
settingsManager.saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
export const backupManager = new BackupManager();
|
||||||
|
|
||||||
|
// Expose to window for testing in development mode
|
||||||
|
if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') {
|
||||||
|
(window as any).backupManager = backupManager;
|
||||||
|
}
|
||||||
43
src/lib/utils/backupTest.js
Normal file
43
src/lib/utils/backupTest.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// This is a simple browser console test that you can run to verify backup/restore functionality
|
||||||
|
// Copy and paste this into the browser console after loading the application
|
||||||
|
|
||||||
|
function testBackupSystem() {
|
||||||
|
console.log("=== Backup System Test ===");
|
||||||
|
|
||||||
|
// First check if we can access the backup manager
|
||||||
|
if (!window.backupManager) {
|
||||||
|
console.error("Could not access backupManager. Add 'window.backupManager = backupManager;' to BackupManager.ts for testing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a backup object without downloading the file
|
||||||
|
console.log("Creating backup object...");
|
||||||
|
const backup = window.backupManager.createBackup();
|
||||||
|
|
||||||
|
console.log("Backup object created:", backup);
|
||||||
|
console.log("Backup version:", backup.version);
|
||||||
|
console.log("Timestamp:", new Date(backup.timestamp).toLocaleString());
|
||||||
|
|
||||||
|
// Check what data was backed up
|
||||||
|
console.log("Profiles found:", backup.data.profiles ? backup.data.profiles.length : 0);
|
||||||
|
console.log("Triggers found:", backup.data.triggers ? backup.data.triggers.length : 0);
|
||||||
|
console.log("Settings found:", backup.data.settings ? "Yes" : "No");
|
||||||
|
|
||||||
|
// Count total number of items in the backup
|
||||||
|
const totalItems = Object.keys(backup.data).length;
|
||||||
|
console.log("Total data items:", totalItems);
|
||||||
|
|
||||||
|
if (totalItems === 0) {
|
||||||
|
console.error("No data was found in the backup. Make sure you have some profiles, triggers, or settings saved.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Backup creation successful");
|
||||||
|
|
||||||
|
// We can't test actual import here as it would overwrite the user's data
|
||||||
|
console.log("To test import functionality, export a backup file,");
|
||||||
|
console.log("modify some settings, then import the backup file back.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testBackupSystem();
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { profiles, activeProfileId, uiSettings, accessibilitySettings } from '$lib/stores/mudStore';
|
import { profiles, activeProfileId, uiSettings, accessibilitySettings } from '$lib/stores/mudStore';
|
||||||
import { ProfileManager } from '$lib/profiles/ProfileManager';
|
import { ProfileManager } from '$lib/profiles/ProfileManager';
|
||||||
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
|
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
|
||||||
|
import { settingsManager } from '$lib/settings/SettingsManager';
|
||||||
import { shortcutManager } from '$lib/utils/KeyboardShortcutManager';
|
import { shortcutManager } from '$lib/utils/KeyboardShortcutManager';
|
||||||
import PwaUpdater from '$lib/components/PwaUpdater.svelte';
|
import PwaUpdater from '$lib/components/PwaUpdater.svelte';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
@@ -157,6 +158,9 @@
|
|||||||
|
|
||||||
profileManager = new ProfileManager();
|
profileManager = new ProfileManager();
|
||||||
|
|
||||||
|
// Settings manager is initialized as a singleton when imported
|
||||||
|
// We don't need to create a new instance
|
||||||
|
|
||||||
// Initialize accessibility manager for global control
|
// Initialize accessibility manager for global control
|
||||||
accessibilityManager = new AccessibilityManager();
|
accessibilityManager = new AccessibilityManager();
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
}
|
}
|
||||||
import MudMdi from '$lib/components/MudMdi.svelte';
|
import MudMdi from '$lib/components/MudMdi.svelte';
|
||||||
import KeyboardShortcutsHelp from '$lib/components/KeyboardShortcutsHelp.svelte';
|
import KeyboardShortcutsHelp from '$lib/components/KeyboardShortcutsHelp.svelte';
|
||||||
|
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||||
import { ModalHelper } from '$lib/utils/ModalHelper';
|
import { ModalHelper } from '$lib/utils/ModalHelper';
|
||||||
import {
|
import {
|
||||||
profiles,
|
profiles,
|
||||||
@@ -465,172 +466,7 @@
|
|||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-list">
|
<SettingsPanel />
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Dark Mode</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$uiSettings.isDarkMode}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Show Timestamps</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$uiSettings.showTimestamps}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">ANSI Colors</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$uiSettings.ansiColor}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Font</span>
|
|
||||||
<select bind:value={$uiSettings.font} class="form-control">
|
|
||||||
<option value="monospace">Monospace</option>
|
|
||||||
<option value="'Courier New', monospace">Courier New</option>
|
|
||||||
<option value="'Roboto Mono', monospace">Roboto Mono</option>
|
|
||||||
<option value="'Source Code Pro', monospace">Source Code Pro</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Global Sound Volume</span>
|
|
||||||
<div class="range-control">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
on:input={(e) => {
|
|
||||||
// Debounce volume changes to avoid performance issues with rapid changes
|
|
||||||
if (window.volumeDebounceTimeout) {
|
|
||||||
clearTimeout(window.volumeDebounceTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read value directly from the input
|
|
||||||
const newVolume = parseFloat(e.target.value);
|
|
||||||
|
|
||||||
// Update the store with a slight delay to avoid excessive updates
|
|
||||||
window.volumeDebounceTimeout = setTimeout(() => {
|
|
||||||
uiSettings.update(settings => ({
|
|
||||||
...settings,
|
|
||||||
globalVolume: newVolume
|
|
||||||
}));
|
|
||||||
}, 100);
|
|
||||||
}}
|
|
||||||
bind:value={$uiSettings.globalVolume}
|
|
||||||
>
|
|
||||||
<span class="range-value">{($uiSettings.globalVolume * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4>Debugging</h4>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Show GMCP Messages</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$uiSettings.debugGmcp}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
<div class="setting-description">
|
|
||||||
Shows GMCP protocol messages in the output window for debugging
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4>Accessibility</h4>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Text-to-Speech</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$accessibilitySettings.textToSpeech}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">High Contrast</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$accessibilitySettings.highContrast}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Font Size</span>
|
|
||||||
<div class="range-control">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="12"
|
|
||||||
max="24"
|
|
||||||
step="1"
|
|
||||||
bind:value={$accessibilitySettings.fontSize}
|
|
||||||
>
|
|
||||||
<span class="range-value">{$accessibilitySettings.fontSize}px</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if $accessibilitySettings.textToSpeech}
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Interrupt Speech on Enter</span>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" bind:checked={$accessibilitySettings.interruptSpeechOnEnter}>
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</label>
|
|
||||||
<div class="setting-description">
|
|
||||||
Automatically stop speaking when the Enter key is pressed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Speech Rate</span>
|
|
||||||
<div class="range-control">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.5"
|
|
||||||
max="2"
|
|
||||||
step="0.1"
|
|
||||||
bind:value={$accessibilitySettings.speechRate}
|
|
||||||
>
|
|
||||||
<span class="range-value">{$accessibilitySettings.speechRate.toFixed(1)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Speech Pitch</span>
|
|
||||||
<div class="range-control">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0.5"
|
|
||||||
max="2"
|
|
||||||
step="0.1"
|
|
||||||
bind:value={$accessibilitySettings.speechPitch}
|
|
||||||
>
|
|
||||||
<span class="range-value">{$accessibilitySettings.speechPitch.toFixed(1)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<span class="setting-name">Speech Volume</span>
|
|
||||||
<div class="range-control">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.1"
|
|
||||||
bind:value={$accessibilitySettings.speechVolume}
|
|
||||||
>
|
|
||||||
<span class="range-value">{($accessibilitySettings.speechVolume * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user