A modern, privacy-focused webmail client built with Next.js and the JMAP protocol. Designed for Stalwart Mail Server. Features: - Full email operations (compose, reply, forward, threading) - Real-time push notifications - Dark/light theme support - Mobile responsive design - Keyboard shortcuts - Drag-and-drop organization - i18n (English/French) - Security-first (external content blocked, HTML sanitization)
214 lines
5.8 KiB
TypeScript
214 lines
5.8 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export type FontSize = 'small' | 'medium' | 'large';
|
|
export type ListDensity = 'compact' | 'regular' | 'comfortable';
|
|
export type DeleteAction = 'trash' | 'permanent';
|
|
export type ReplyMode = 'reply' | 'replyAll';
|
|
export type DateFormat = 'regional' | 'iso' | 'custom';
|
|
export type TimeFormat = '12h' | '24h';
|
|
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
|
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
|
|
|
interface SettingsState {
|
|
// Appearance
|
|
fontSize: FontSize;
|
|
listDensity: ListDensity;
|
|
animationsEnabled: boolean;
|
|
|
|
// Language & Region
|
|
dateFormat: DateFormat;
|
|
timeFormat: TimeFormat;
|
|
firstDayOfWeek: FirstDayOfWeek;
|
|
|
|
// Email Behavior
|
|
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
|
|
deleteAction: DeleteAction;
|
|
showPreview: boolean;
|
|
emailsPerPage: number;
|
|
externalContentPolicy: ExternalContentPolicy;
|
|
|
|
// Composer
|
|
autoSaveDraftInterval: number; // milliseconds
|
|
sendConfirmation: boolean;
|
|
defaultReplyMode: ReplyMode;
|
|
|
|
// Privacy & Security
|
|
sessionTimeout: number; // minutes (0 = never)
|
|
|
|
// Advanced
|
|
debugMode: boolean;
|
|
|
|
// Actions
|
|
updateSetting: <K extends keyof SettingsState>(
|
|
key: K,
|
|
value: SettingsState[K]
|
|
) => void;
|
|
resetToDefaults: () => void;
|
|
exportSettings: () => string;
|
|
importSettings: (json: string) => boolean;
|
|
}
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
// Appearance
|
|
fontSize: 'medium' as FontSize,
|
|
listDensity: 'regular' as ListDensity,
|
|
animationsEnabled: true,
|
|
|
|
// Language & Region
|
|
dateFormat: 'regional' as DateFormat,
|
|
timeFormat: '24h' as TimeFormat,
|
|
firstDayOfWeek: 1 as FirstDayOfWeek, // Monday
|
|
|
|
// Email Behavior
|
|
markAsReadDelay: 0, // Instant
|
|
deleteAction: 'trash' as DeleteAction,
|
|
showPreview: true,
|
|
emailsPerPage: 50,
|
|
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
|
|
|
// Composer
|
|
autoSaveDraftInterval: 60000, // 1 minute
|
|
sendConfirmation: false,
|
|
defaultReplyMode: 'reply' as ReplyMode,
|
|
|
|
// Privacy & Security
|
|
sessionTimeout: 0, // Never
|
|
|
|
// Advanced
|
|
debugMode: false,
|
|
};
|
|
|
|
export const useSettingsStore = create<SettingsState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...DEFAULT_SETTINGS,
|
|
|
|
updateSetting: (key, value) => {
|
|
set({ [key]: value });
|
|
|
|
// Apply font size to document root
|
|
if (key === 'fontSize') {
|
|
applyFontSize(value as FontSize);
|
|
}
|
|
|
|
// Apply list density to document root
|
|
if (key === 'listDensity') {
|
|
applyListDensity(value as ListDensity);
|
|
}
|
|
|
|
// Apply animations to document root
|
|
if (key === 'animationsEnabled') {
|
|
applyAnimations(value as boolean);
|
|
}
|
|
},
|
|
|
|
resetToDefaults: () => {
|
|
set(DEFAULT_SETTINGS);
|
|
applyFontSize(DEFAULT_SETTINGS.fontSize);
|
|
applyListDensity(DEFAULT_SETTINGS.listDensity);
|
|
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
|
|
},
|
|
|
|
exportSettings: () => {
|
|
const state = get();
|
|
const settings = {
|
|
fontSize: state.fontSize,
|
|
listDensity: state.listDensity,
|
|
animationsEnabled: state.animationsEnabled,
|
|
dateFormat: state.dateFormat,
|
|
timeFormat: state.timeFormat,
|
|
firstDayOfWeek: state.firstDayOfWeek,
|
|
markAsReadDelay: state.markAsReadDelay,
|
|
deleteAction: state.deleteAction,
|
|
showPreview: state.showPreview,
|
|
emailsPerPage: state.emailsPerPage,
|
|
externalContentPolicy: state.externalContentPolicy,
|
|
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
|
sendConfirmation: state.sendConfirmation,
|
|
defaultReplyMode: state.defaultReplyMode,
|
|
sessionTimeout: state.sessionTimeout,
|
|
debugMode: state.debugMode,
|
|
};
|
|
return JSON.stringify(settings, null, 2);
|
|
},
|
|
|
|
importSettings: (json: string) => {
|
|
try {
|
|
const settings = JSON.parse(json);
|
|
|
|
// Validate settings
|
|
if (typeof settings !== 'object' || settings === null) {
|
|
return false;
|
|
}
|
|
|
|
// Apply settings
|
|
Object.keys(settings).forEach((key) => {
|
|
if (key in DEFAULT_SETTINGS) {
|
|
set({ [key]: settings[key] });
|
|
}
|
|
});
|
|
|
|
// Apply visual settings
|
|
applyFontSize(get().fontSize);
|
|
applyListDensity(get().listDensity);
|
|
applyAnimations(get().animationsEnabled);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to import settings:', error);
|
|
return false;
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: 'settings-storage',
|
|
version: 1,
|
|
}
|
|
)
|
|
);
|
|
|
|
// Helper functions to apply settings to DOM
|
|
function applyFontSize(size: FontSize) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const sizeMap = {
|
|
small: '14px',
|
|
medium: '16px',
|
|
large: '18px',
|
|
};
|
|
root.style.setProperty('--font-size-base', sizeMap[size]);
|
|
}
|
|
|
|
function applyListDensity(density: ListDensity) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const densityMap = {
|
|
compact: '32px',
|
|
regular: '48px',
|
|
comfortable: '64px',
|
|
};
|
|
root.style.setProperty('--list-item-height', densityMap[density]);
|
|
}
|
|
|
|
function applyAnimations(enabled: boolean) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
if (enabled) {
|
|
root.style.removeProperty('--transition-duration');
|
|
} else {
|
|
root.style.setProperty('--transition-duration', '0s');
|
|
}
|
|
}
|
|
|
|
// Initialize settings on load
|
|
if (typeof window !== 'undefined') {
|
|
const store = useSettingsStore.getState();
|
|
applyFontSize(store.fontSize);
|
|
applyListDensity(store.listDensity);
|
|
applyAnimations(store.animationsEnabled);
|
|
}
|