mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
chore: reorganize file structure
This commit is contained in:
299
src/js/user-settings.js
Normal file
299
src/js/user-settings.js
Normal file
@@ -0,0 +1,299 @@
|
||||
// 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'
|
||||
},
|
||||
|
||||
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')
|
||||
function getSetting(path) {
|
||||
const settings = getSettings();
|
||||
const parts = path.split('.');
|
||||
let value = settings;
|
||||
|
||||
for (const part of parts) {
|
||||
if (value && typeof value === 'object' && part in value) {
|
||||
value = value[part];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Update a specific setting by path
|
||||
function updateSetting(path, value) {
|
||||
const settings = getSettings();
|
||||
const parts = path.split('.');
|
||||
let target = settings;
|
||||
|
||||
// Navigate to the parent object
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!target[part] || typeof target[part] !== 'object') {
|
||||
target[part] = {};
|
||||
}
|
||||
target = target[part];
|
||||
}
|
||||
|
||||
// Set the value
|
||||
const lastPart = parts[parts.length - 1];
|
||||
target[lastPart] = value;
|
||||
|
||||
return saveSettings();
|
||||
}
|
||||
|
||||
// Update multiple settings at once
|
||||
function updateSettings(updates) {
|
||||
const settings = getSettings();
|
||||
|
||||
for (const path in updates) {
|
||||
const parts = path.split('.');
|
||||
let target = settings;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (!target[part] || typeof target[part] !== 'object') {
|
||||
target[part] = {};
|
||||
}
|
||||
target = target[part];
|
||||
}
|
||||
|
||||
const lastPart = parts[parts.length - 1];
|
||||
target[lastPart] = updates[path];
|
||||
}
|
||||
|
||||
return saveSettings();
|
||||
}
|
||||
|
||||
// Reset all settings to defaults
|
||||
function resetSettings() {
|
||||
userSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
|
||||
return saveSettings();
|
||||
}
|
||||
|
||||
// Export settings as JSON
|
||||
function exportSettings() {
|
||||
return JSON.stringify(getSettings(), null, 2);
|
||||
}
|
||||
|
||||
// Import settings from JSON string
|
||||
function importSettings(jsonString) {
|
||||
try {
|
||||
const imported = JSON.parse(jsonString);
|
||||
userSettings = mergeWithDefaults(imported);
|
||||
return saveSettings();
|
||||
} catch (error) {
|
||||
console.error('Error importing settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
function validateSetting(path, value) {
|
||||
const errors = [];
|
||||
|
||||
if (path === 'editor.fontSize') {
|
||||
if (typeof value !== 'number' || value < 10 || value > 18) {
|
||||
errors.push('Font size must be between 10 and 18');
|
||||
}
|
||||
}
|
||||
|
||||
if (path === 'editor.theme') {
|
||||
const validThemes = ['vs-light', 'vs-dark', 'hc-black'];
|
||||
if (!validThemes.includes(value)) {
|
||||
errors.push('Invalid theme value');
|
||||
}
|
||||
}
|
||||
|
||||
if (path === 'editor.tabSize') {
|
||||
if (typeof value !== 'number' || value < 2 || value > 8) {
|
||||
errors.push('Tab size must be between 2 and 8');
|
||||
}
|
||||
}
|
||||
|
||||
if (path === 'performance.renderDebounce') {
|
||||
if (typeof value !== 'number' || value < 300 || value > 3000) {
|
||||
errors.push('Render debounce must be between 300 and 3000ms');
|
||||
}
|
||||
}
|
||||
|
||||
if (path === 'formatting.dateFormat') {
|
||||
const validFormats = ['smart', 'locale', 'iso', 'custom'];
|
||||
if (!validFormats.includes(value)) {
|
||||
errors.push('Invalid date format value');
|
||||
}
|
||||
}
|
||||
|
||||
if (path === 'ui.theme') {
|
||||
const validThemes = ['light', 'experimental'];
|
||||
if (!validThemes.includes(value)) {
|
||||
errors.push('Invalid theme value');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
Reference in New Issue
Block a user