mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
388 lines
14 KiB
JavaScript
388 lines
14 KiB
JavaScript
// user-settings.js - User preferences and configuration management
|
|
|
|
const SETTINGS_STORAGE_KEY = 'astrolabe-user-settings';
|
|
|
|
// Default settings configuration
|
|
const DEFAULT_SETTINGS = {
|
|
version: 1,
|
|
|
|
editor: {
|
|
fontSize: 12, // 10-18px
|
|
theme: 'vs-light', // 'vs-light' | 'vs-dark' | 'hc-black'
|
|
minimap: false, // true | false
|
|
wordWrap: 'on', // 'on' | 'off'
|
|
lineNumbers: 'on', // 'on' | 'off'
|
|
tabSize: 2 // 2-8 spaces
|
|
},
|
|
|
|
performance: {
|
|
renderDebounce: 1500 // 300-3000ms - delay before visualization renders
|
|
},
|
|
|
|
ui: {
|
|
theme: 'light', // 'light' | 'experimental'
|
|
previewFitMode: 'default' // 'default' | 'width' | 'full'
|
|
},
|
|
|
|
formatting: {
|
|
dateFormat: 'smart', // 'smart' | 'locale' | 'iso' | 'custom'
|
|
customDateFormat: 'yyyy-MM-dd HH:mm' // Used when dateFormat === 'custom'
|
|
}
|
|
};
|
|
|
|
// Current user settings (loaded from localStorage or defaults)
|
|
let userSettings = null;
|
|
|
|
// Initialize settings - load from localStorage or use defaults
|
|
function initSettings() {
|
|
try {
|
|
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
// Merge with defaults to handle version upgrades
|
|
userSettings = mergeWithDefaults(parsed);
|
|
} else {
|
|
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading user settings:', error);
|
|
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
|
}
|
|
return userSettings;
|
|
}
|
|
|
|
// Merge stored settings with defaults (handles new settings in updates)
|
|
function mergeWithDefaults(stored) {
|
|
const merged = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
|
|
|
// Merge each section
|
|
for (const section in stored) {
|
|
if (merged[section] && typeof merged[section] === 'object') {
|
|
Object.assign(merged[section], stored[section]);
|
|
} else {
|
|
merged[section] = stored[section];
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
// Save settings to localStorage
|
|
function saveSettings() {
|
|
try {
|
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(userSettings));
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error saving user settings:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Get all current settings
|
|
function getSettings() {
|
|
if (!userSettings) {
|
|
initSettings();
|
|
}
|
|
return userSettings;
|
|
}
|
|
|
|
// Get a specific setting by path (e.g., 'editor.fontSize')
|
|
// Simplified: assumes 2-level structure (section.key)
|
|
function getSetting(path) {
|
|
const settings = getSettings();
|
|
const [section, key] = path.split('.');
|
|
return settings?.[section]?.[key];
|
|
}
|
|
|
|
// Update a specific setting by path
|
|
// Simplified: assumes 2-level structure (section.key)
|
|
function updateSetting(path, value) {
|
|
const settings = getSettings();
|
|
const [section, key] = path.split('.');
|
|
|
|
if (settings[section]) {
|
|
settings[section][key] = value;
|
|
return saveSettings();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Update multiple settings at once
|
|
function updateSettings(updates) {
|
|
const settings = getSettings();
|
|
|
|
for (const path in updates) {
|
|
const [section, key] = path.split('.');
|
|
if (settings[section]) {
|
|
settings[section][key] = updates[path];
|
|
}
|
|
}
|
|
|
|
return saveSettings();
|
|
}
|
|
|
|
// Reset all settings to defaults
|
|
function resetSettings() {
|
|
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
|
return saveSettings();
|
|
}
|
|
|
|
// Format date according to user settings
|
|
function formatDate(isoString, useFullFormat = false) {
|
|
const date = new Date(isoString);
|
|
const format = getSetting('formatting.dateFormat');
|
|
|
|
if (!useFullFormat && format === 'smart') {
|
|
// Smart format: relative for recent dates
|
|
const diffMs = new Date() - date;
|
|
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`;
|
|
if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
|
|
if (diffDays === 0) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
if (diffDays === 1) return 'Yesterday';
|
|
if (diffDays < 7) return `${diffDays} days ago`;
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
if (format === 'locale') {
|
|
return useFullFormat
|
|
? date.toLocaleString([], {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
})
|
|
: date.toLocaleDateString();
|
|
}
|
|
|
|
if (format === 'iso') {
|
|
return useFullFormat
|
|
? date.toISOString()
|
|
: date.toISOString().split('T')[0];
|
|
}
|
|
|
|
if (format === 'custom') {
|
|
const customFormat = getSetting('formatting.customDateFormat');
|
|
return formatCustomDate(date, customFormat);
|
|
}
|
|
|
|
// Fallback to locale
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
// Format date using custom format tokens
|
|
function formatCustomDate(date, format) {
|
|
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
|
|
const tokens = {
|
|
'yyyy': date.getFullYear(),
|
|
'yy': String(date.getFullYear()).slice(-2),
|
|
'MM': pad(date.getMonth() + 1),
|
|
'M': date.getMonth() + 1,
|
|
'dd': pad(date.getDate()),
|
|
'd': date.getDate(),
|
|
'HH': pad(date.getHours()),
|
|
'H': date.getHours(),
|
|
'hh': pad(date.getHours() % 12 || 12),
|
|
'h': date.getHours() % 12 || 12,
|
|
'mm': pad(date.getMinutes()),
|
|
'm': date.getMinutes(),
|
|
'ss': pad(date.getSeconds()),
|
|
's': date.getSeconds(),
|
|
'a': date.getHours() < 12 ? 'am' : 'pm',
|
|
'A': date.getHours() < 12 ? 'AM' : 'PM'
|
|
};
|
|
|
|
let result = format;
|
|
// Sort by length descending to replace longer tokens first
|
|
const sortedTokens = Object.keys(tokens).sort((a, b) => b.length - a.length);
|
|
for (const token of sortedTokens) {
|
|
result = result.replace(new RegExp(token, 'g'), tokens[token]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Validate setting value - simplified with inline validation rules
|
|
function validateSetting(path, value) {
|
|
const rules = {
|
|
'editor.fontSize': () => typeof value === 'number' && value >= 10 && value <= 18 ? [] : ['Font size must be between 10 and 18'],
|
|
'editor.theme': () => ['vs-light', 'vs-dark', 'hc-black'].includes(value) ? [] : ['Invalid editor theme'],
|
|
'editor.tabSize': () => typeof value === 'number' && value >= 2 && value <= 8 ? [] : ['Tab size must be between 2 and 8'],
|
|
'performance.renderDebounce': () => typeof value === 'number' && value >= 300 && value <= 3000 ? [] : ['Render debounce must be between 300-3000ms'],
|
|
'formatting.dateFormat': () => ['smart', 'locale', 'iso', 'custom'].includes(value) ? [] : ['Invalid date format'],
|
|
'ui.theme': () => ['light', 'experimental'].includes(value) ? [] : ['Invalid UI theme']
|
|
};
|
|
|
|
return rules[path] ? rules[path]() : [];
|
|
}
|
|
|
|
// Alpine.js Component for settings panel
|
|
// Thin wrapper - Alpine handles form state and reactivity, user-settings.js handles storage
|
|
function settingsPanel() {
|
|
return {
|
|
// Form state (loaded from settings on open)
|
|
uiTheme: 'light',
|
|
fontSize: 12,
|
|
editorTheme: 'vs-light',
|
|
tabSize: 2,
|
|
minimap: false,
|
|
wordWrap: true,
|
|
lineNumbers: true,
|
|
renderDebounce: 1500,
|
|
dateFormat: 'smart',
|
|
customDateFormat: 'yyyy-MM-dd HH:mm',
|
|
|
|
// Original values for dirty checking
|
|
originalSettings: null,
|
|
|
|
// Initialize component with current settings
|
|
init() {
|
|
this.loadSettings();
|
|
},
|
|
|
|
// Load settings from storage into form
|
|
loadSettings() {
|
|
const settings = getSettings();
|
|
this.uiTheme = settings.ui.theme;
|
|
this.fontSize = settings.editor.fontSize;
|
|
this.editorTheme = settings.editor.theme;
|
|
this.tabSize = settings.editor.tabSize;
|
|
this.minimap = settings.editor.minimap;
|
|
this.wordWrap = settings.editor.wordWrap === 'on';
|
|
this.lineNumbers = settings.editor.lineNumbers === 'on';
|
|
this.renderDebounce = settings.performance.renderDebounce;
|
|
this.dateFormat = settings.formatting.dateFormat;
|
|
this.customDateFormat = settings.formatting.customDateFormat;
|
|
|
|
// Store original values for dirty checking
|
|
this.originalSettings = JSON.stringify(this.getCurrentFormState());
|
|
},
|
|
|
|
// Get current form state as object
|
|
getCurrentFormState() {
|
|
return {
|
|
uiTheme: this.uiTheme,
|
|
fontSize: this.fontSize,
|
|
editorTheme: this.editorTheme,
|
|
tabSize: this.tabSize,
|
|
minimap: this.minimap,
|
|
wordWrap: this.wordWrap,
|
|
lineNumbers: this.lineNumbers,
|
|
renderDebounce: this.renderDebounce,
|
|
dateFormat: this.dateFormat,
|
|
customDateFormat: this.customDateFormat
|
|
};
|
|
},
|
|
|
|
// Check if settings have been modified
|
|
get isDirty() {
|
|
return this.originalSettings !== JSON.stringify(this.getCurrentFormState());
|
|
},
|
|
|
|
// Show custom date format field when 'custom' is selected
|
|
get showCustomDateFormat() {
|
|
return this.dateFormat === 'custom';
|
|
},
|
|
|
|
// Apply settings and save
|
|
apply() {
|
|
const newSettings = {
|
|
'ui.theme': this.uiTheme,
|
|
'editor.fontSize': parseInt(this.fontSize),
|
|
'editor.theme': this.editorTheme,
|
|
'editor.tabSize': parseInt(this.tabSize),
|
|
'editor.minimap': this.minimap,
|
|
'editor.wordWrap': this.wordWrap ? 'on' : 'off',
|
|
'editor.lineNumbers': this.lineNumbers ? 'on' : 'off',
|
|
'performance.renderDebounce': parseInt(this.renderDebounce),
|
|
'formatting.dateFormat': this.dateFormat,
|
|
'formatting.customDateFormat': this.customDateFormat
|
|
};
|
|
|
|
// Validate settings
|
|
let hasErrors = false;
|
|
for (const [path, value] of Object.entries(newSettings)) {
|
|
const errors = validateSetting(path, value);
|
|
if (errors.length > 0) {
|
|
Toast.show(errors.join(', '), 'error');
|
|
hasErrors = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hasErrors) return;
|
|
|
|
// Save settings
|
|
if (updateSettings(newSettings)) {
|
|
// Apply theme to document
|
|
document.documentElement.setAttribute('data-theme', this.uiTheme);
|
|
|
|
// Sync editor theme with UI theme
|
|
const editorTheme = this.uiTheme === 'experimental' ? 'vs-dark' : 'vs-light';
|
|
newSettings['editor.theme'] = editorTheme;
|
|
|
|
// Apply editor settings immediately
|
|
if (editor) {
|
|
editor.updateOptions({
|
|
fontSize: newSettings['editor.fontSize'],
|
|
theme: editorTheme,
|
|
tabSize: newSettings['editor.tabSize'],
|
|
minimap: { enabled: newSettings['editor.minimap'] },
|
|
wordWrap: newSettings['editor.wordWrap'],
|
|
lineNumbers: newSettings['editor.lineNumbers']
|
|
});
|
|
}
|
|
|
|
// Update the editor theme in settings
|
|
updateSetting('editor.theme', editorTheme);
|
|
|
|
// Update debounced render function
|
|
if (typeof updateRenderDebounce === 'function') {
|
|
updateRenderDebounce(newSettings['performance.renderDebounce']);
|
|
}
|
|
|
|
// Re-render snippet list to reflect date format changes
|
|
renderSnippetList();
|
|
|
|
// Update metadata display if a snippet is selected
|
|
if (Alpine.store('snippets').currentSnippetId) {
|
|
const snippet = SnippetStorage.getSnippet(Alpine.store('snippets').currentSnippetId);
|
|
if (snippet) {
|
|
document.getElementById('snippet-created').textContent = formatDate(snippet.created, true);
|
|
document.getElementById('snippet-modified').textContent = formatDate(snippet.modified, true);
|
|
}
|
|
}
|
|
|
|
Toast.success('Settings applied successfully');
|
|
closeSettingsModal();
|
|
|
|
// Track event
|
|
Analytics.track('settings-apply', 'Applied settings');
|
|
} else {
|
|
Toast.error('Failed to save settings');
|
|
}
|
|
},
|
|
|
|
// Reset to defaults
|
|
reset() {
|
|
if (confirm('Reset all settings to defaults? This cannot be undone.')) {
|
|
resetSettings();
|
|
this.loadSettings();
|
|
Toast.success('Settings reset to defaults');
|
|
}
|
|
},
|
|
|
|
// Cancel changes and close modal
|
|
cancel() {
|
|
closeSettingsModal();
|
|
}
|
|
};
|
|
}
|