Settings System
Complete guide to the settings management system in DesQTA
Settings System Overview
DesQTA uses a comprehensive settings system that stores user preferences locally and optionally syncs them to BetterSEQTA Cloud. Settings are managed by the Rust backend and exposed via Tauri commands.
Settings Structure
Settings Data Model
// src-tauri/src/utils/settings.rs
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Settings {
// Dashboard
pub shortcuts: Vec<Shortcut>,
pub feeds: Vec<Feed>,
// Appearance
pub accent_color: String,
pub theme: String,
pub current_theme: Option<String>,
pub enhanced_animations: bool,
pub disable_school_picture: bool,
// Features
pub weather_enabled: bool,
pub weather_city: String,
pub weather_country: String,
pub force_use_location: bool,
pub reminders_enabled: bool,
// AI Integrations
pub gemini_api_key: Option<String>,
pub ai_integrations_enabled: Option<bool>,
pub grade_analyser_enabled: Option<bool>,
pub lesson_summary_analyser_enabled: Option<bool>,
// UI Behavior
pub auto_collapse_sidebar: bool,
pub auto_expand_sidebar_hover: bool,
pub global_search_enabled: bool,
// Developer
pub dev_sensitive_info_hider: bool,
pub dev_force_offline_mode: bool,
// Cloud
pub accepted_cloud_eula: bool,
// Localization
pub language: String,
// Navigation
#[serde(default)]
pub menu_order: Option<Vec<String>>,
// Onboarding
#[serde(default)]
pub has_been_through_onboarding: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Shortcut {
pub name: String,
pub icon: String,
pub url: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Feed {
pub url: String,
}
Settings Storage
File Location
Settings are stored per profile:
$DATA_DIR/DesQTA/profiles/{profile_id}/settings.json
Loading Settings
impl Settings {
pub fn load() -> Self {
let path = settings_file();
if let Ok(mut file) = fs::File::open(&path) {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_ok() {
if let Ok(settings) = serde_json::from_str::<Settings>(&contents) {
return settings;
}
}
}
Settings::default()
}
pub fn save(&self) -> io::Result<()> {
let path = settings_file();
// Create parent directory if needed
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let json = serde_json::to_string_pretty(self)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
fs::write(&path, json)
}
}
Tauri Commands
Get All Settings
#[tauri::command]
pub fn get_settings() -> Result<Settings, String> {
Ok(Settings::load())
}
Get Settings Subset
#[tauri::command]
pub fn get_settings_subset(
keys: Vec<String>
) -> Result<HashMap<String, serde_json::Value>, String> {
let settings = Settings::load();
let mut subset = HashMap::new();
for key in keys {
match key.as_str() {
"shortcuts" => subset.insert("shortcuts".to_string(), json!(settings.shortcuts)),
"feeds" => subset.insert("feeds".to_string(), json!(settings.feeds)),
"weather_enabled" => subset.insert("weather_enabled".to_string(), json!(settings.weather_enabled)),
"accent_color" => subset.insert("accent_color".to_string(), json!(settings.accent_color)),
"theme" => subset.insert("theme".to_string(), json!(settings.theme)),
// ... more keys
_ => {}
}
}
Ok(subset)
}
Save Settings
#[tauri::command]
pub fn save_settings(settings: Settings) -> Result<(), String> {
settings.save()
.map_err(|e| format!("Failed to save settings: {}", e))
}
Update Settings (Partial)
#[tauri::command]
pub fn update_settings(
updates: HashMap<String, serde_json::Value>
) -> Result<(), String> {
let mut settings = Settings::load();
// Update fields from HashMap
for (key, value) in updates {
match key.as_str() {
"accent_color" => {
if let Some(color) = value.as_str() {
settings.accent_color = color.to_string();
}
}
"theme" => {
if let Some(theme) = value.as_str() {
settings.theme = theme.to_string();
}
}
"shortcuts" => {
if let Ok(shortcuts) = serde_json::from_value::<Vec<Shortcut>>(value) {
settings.shortcuts = shortcuts;
}
}
// ... more fields
_ => {}
}
}
settings.save()
.map_err(|e| format!("Failed to save settings: {}", e))
}
Frontend Integration
Loading Settings
import { invoke } from '@tauri-apps/api/core';
// Load all settings
const settings = await invoke<Settings>('get_settings');
// Load subset (more efficient)
const subset = await invoke<Record<string, any>>('get_settings_subset', {
keys: ['accent_color', 'theme', 'shortcuts']
});
Saving Settings
// Save all settings
await invoke('save_settings', { settings });
// Update partial settings
await invoke('update_settings', {
updates: {
accent_color: '#ff0000',
theme: 'dark'
}
});
Settings Sync Service
Frontend Sync Service
// src/lib/services/settingsSync.ts
interface SettingsQueue {
[key: string]: any;
}
let settingsQueue: SettingsQueue = {};
let syncTimeout: ReturnType<typeof setTimeout> | null = null;
export async function saveSettingsWithQueue(
patch: Record<string, any>
): Promise<void> {
// Add to queue
Object.assign(settingsQueue, patch);
// Debounce saves
if (syncTimeout) {
clearTimeout(syncTimeout);
}
syncTimeout = setTimeout(async () => {
await flushSettingsQueue();
}, 500); // Wait 500ms for more changes
}
export async function flushSettingsQueue(): Promise<void> {
if (Object.keys(settingsQueue).length === 0) return;
const updates = { ...settingsQueue };
settingsQueue = {};
try {
// Save to backend
await invoke('update_settings', { updates });
// Sync to cloud if enabled
const cloudUser = await cloudAuthService.init();
if (cloudUser) {
await cloudSettingsService.sync(updates);
}
} catch (error) {
logger.error('settingsSync', 'flushSettingsQueue', `Failed: ${error}`, { error });
}
}
Cloud Synchronization
Cloud Settings Service
// src/lib/services/cloudSettingsService.ts
export class CloudSettingsService {
private baseUrl = 'https://accounts.betterseqta.org/api';
async sync(settings: Record<string, any>): Promise<void> {
const token = await this.getToken();
if (!token) return;
await fetch(`${this.baseUrl}/settings/sync`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ settings })
});
}
async load(): Promise<Record<string, any> | null> {
const token = await this.getToken();
if (!token) return null;
const response = await fetch(`${this.baseUrl}/settings`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
}
}
export const cloudSettingsService = new CloudSettingsService();
Settings Defaults
Default Values
impl Default for Settings {
fn default() -> Self {
Self {
shortcuts: Vec::new(),
feeds: Vec::new(),
weather_enabled: false,
weather_city: String::new(),
weather_country: String::new(),
reminders_enabled: true,
force_use_location: false,
accent_color: "#3b82f6".to_string(), // Blue-500
theme: "dark".to_string(),
current_theme: Some("default".to_string()),
disable_school_picture: false,
enhanced_animations: true,
gemini_api_key: None,
ai_integrations_enabled: Some(false),
grade_analyser_enabled: Some(true),
lesson_summary_analyser_enabled: Some(true),
auto_collapse_sidebar: false,
auto_expand_sidebar_hover: false,
global_search_enabled: false,
dev_sensitive_info_hider: false,
dev_force_offline_mode: false,
accepted_cloud_eula: false,
language: "en".to_string(),
menu_order: None,
has_been_through_onboarding: false,
}
}
}
Settings Usage Patterns
Pattern 1: Load on Mount
<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
let accentColor = $state('#3b82f6');
let theme = $state<'light' | 'dark' | 'system'>('system');
onMount(async () => {
const subset = await invoke('get_settings_subset', {
keys: ['accent_color', 'theme']
});
accentColor = subset.accent_color || '#3b82f6';
theme = subset.theme || 'system';
});
</script>
Pattern 2: Reactive Settings
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { saveSettingsWithQueue } from '$lib/services/settingsSync';
let shortcuts = $state<Shortcut[]>([]);
async function loadShortcuts() {
const subset = await invoke('get_settings_subset', {
keys: ['shortcuts']
});
shortcuts = subset.shortcuts || [];
}
async function saveShortcuts() {
await saveSettingsWithQueue({ shortcuts });
}
function addShortcut(shortcut: Shortcut) {
shortcuts = [...shortcuts, shortcut];
saveShortcuts();
}
</script>
Pattern 3: Settings Store
// src/lib/stores/settings.ts
import { writable } from 'svelte/store';
import { invoke } from '@tauri-apps/api/core';
export const settings = writable<Partial<Settings>>({});
export async function loadSettings() {
const loaded = await invoke<Settings>('get_settings');
settings.set(loaded);
}
export async function updateSetting(key: string, value: any) {
settings.update(s => ({ ...s, [key]: value }));
await invoke('update_settings', { updates: { [key]: value } });
}
Settings Validation
Type Validation
impl Settings {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
// Validate accent color format
if !self.accent_color.starts_with('#') || self.accent_color.len() != 7 {
errors.push("Invalid accent color format".to_string());
}
// Validate theme
if !["light", "dark", "system"].contains(&self.theme.as_str()) {
errors.push("Invalid theme value".to_string());
}
// Validate shortcuts
for shortcut in &self.shortcuts {
if shortcut.name.is_empty() {
errors.push("Shortcut name cannot be empty".to_string());
}
if !shortcut.url.starts_with("http://") && !shortcut.url.starts_with("https://") {
errors.push(format!("Invalid shortcut URL: {}", shortcut.url));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
Settings Migration
Version-Based Migration
#[derive(Debug, Serialize, Deserialize)]
struct SettingsV1 {
accent_color: String,
theme: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct SettingsV2 {
accent_color: String,
theme: String,
current_theme: Option<String>, // New field
}
impl Settings {
pub fn migrate() -> Self {
let path = settings_file();
let contents = fs::read_to_string(&path).ok()?;
// Try to detect version
if contents.contains("current_theme") {
// Already V2
serde_json::from_str(&contents).ok()
} else {
// V1, migrate
let v1: SettingsV1 = serde_json::from_str(&contents).ok()?;
Some(Settings {
accent_color: v1.accent_color,
theme: v1.theme,
current_theme: Some("default".to_string()),
..Settings::default()
})
}
}
}
Best Practices
1. Use Subset Loading
// ✅ Good - Load only what you need
const subset = await invoke('get_settings_subset', {
keys: ['accent_color', 'theme']
});
// ❌ Avoid - Loading all settings
const allSettings = await invoke('get_settings');
2. Debounce Saves
// ✅ Good - Debounced saves
await saveSettingsWithQueue({ accent_color: color });
// ❌ Avoid - Immediate saves for every change
await invoke('update_settings', { updates: { accent_color: color } });
3. Validate Before Save
// ✅ Good - Validate before saving
function updateAccentColor(color: string) {
if (!/^#[0-9A-F]{6}$/i.test(color)) {
throw new Error('Invalid color format');
}
saveSettingsWithQueue({ accent_color: color });
}
Next Steps
- Cloud Sync - Cloud synchronization
- Profile Management - Multi-profile support
On this page