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>
|
||||
Reference in New Issue
Block a user