feat: reorganize settings into 6 groups with clearer tabs
This commit is contained in:
+144
-82
@@ -9,7 +9,6 @@ import {
|
||||
LogOut,
|
||||
Settings as SettingsIcon,
|
||||
Palette,
|
||||
Mail,
|
||||
User,
|
||||
Shield,
|
||||
UserPen,
|
||||
@@ -20,17 +19,27 @@ import {
|
||||
FolderOpen,
|
||||
Tags,
|
||||
HardDrive,
|
||||
Wrench,
|
||||
BookUser,
|
||||
KeyRound,
|
||||
PanelLeftClose,
|
||||
Bell,
|
||||
Puzzle,
|
||||
LayoutGrid,
|
||||
BookOpen,
|
||||
PenLine,
|
||||
EyeOff,
|
||||
Languages,
|
||||
Info,
|
||||
Bug,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AppearanceSettings } from '@/components/settings/appearance-settings';
|
||||
import { EmailSettings } from '@/components/settings/email-settings';
|
||||
import { LayoutSettings } from '@/components/settings/layout-settings';
|
||||
import { LanguageSettings } from '@/components/settings/language-settings';
|
||||
import { ReadingSettings } from '@/components/settings/reading-settings';
|
||||
import { ComposingSettings } from '@/components/settings/composing-settings';
|
||||
import { ContentSendersSettings } from '@/components/settings/content-senders-settings';
|
||||
import { AccountSettings } from '@/components/settings/account-settings';
|
||||
import { IdentitySettings } from '@/components/settings/identity-settings';
|
||||
import { VacationSettings } from '@/components/settings/vacation-settings';
|
||||
@@ -39,7 +48,8 @@ import { CalendarManagementSettings } from '@/components/settings/calendar-manag
|
||||
import { AddressBookManagementSettings } from '@/components/settings/address-book-management-settings';
|
||||
import { FilterSettings } from '@/components/settings/filter-settings';
|
||||
import { TemplateSettings } from '@/components/settings/template-settings';
|
||||
import { AdvancedSettings } from '@/components/settings/advanced-settings';
|
||||
import { AboutDataSettings } from '@/components/settings/about-data-settings';
|
||||
import { DebugSettings } from '@/components/settings/debug-settings';
|
||||
import { FolderSettings } from '@/components/settings/folder-settings';
|
||||
import { KeywordSettings } from '@/components/settings/keyword-settings';
|
||||
import { AccountSecuritySettings } from '@/components/settings/account-security-settings';
|
||||
@@ -62,8 +72,33 @@ import { useConfig } from '@/hooks/use-config';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type Tab = 'appearance' | 'email' | 'notifications' | 'account' | 'security' | 'identities' | 'encryption' | 'vacation' | 'calendar' | 'contacts' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'sidebar_apps' | 'themes' | 'plugins' | 'advanced';
|
||||
type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system';
|
||||
type Tab =
|
||||
| 'account'
|
||||
| 'language'
|
||||
| 'notifications'
|
||||
| 'appearance'
|
||||
| 'layout'
|
||||
| 'reading'
|
||||
| 'composing'
|
||||
| 'identities'
|
||||
| 'vacation'
|
||||
| 'filters'
|
||||
| 'templates'
|
||||
| 'folders'
|
||||
| 'keywords'
|
||||
| 'security'
|
||||
| 'encryption'
|
||||
| 'content_senders'
|
||||
| 'calendar'
|
||||
| 'contacts'
|
||||
| 'files'
|
||||
| 'sidebar_apps'
|
||||
| 'about_data'
|
||||
| 'themes'
|
||||
| 'plugins'
|
||||
| 'debug';
|
||||
|
||||
type TabGroup = 'general' | 'appearance' | 'mail' | 'privacy' | 'apps' | 'advanced';
|
||||
|
||||
interface TabDef {
|
||||
id: Tab;
|
||||
@@ -74,28 +109,54 @@ interface TabDef {
|
||||
}
|
||||
|
||||
const tabIcons: Record<Tab, LucideIcon> = {
|
||||
appearance: Palette,
|
||||
email: Mail,
|
||||
notifications: Bell,
|
||||
account: User,
|
||||
security: Shield,
|
||||
language: Languages,
|
||||
notifications: Bell,
|
||||
appearance: Palette,
|
||||
layout: LayoutGrid,
|
||||
reading: BookOpen,
|
||||
composing: PenLine,
|
||||
identities: UserPen,
|
||||
encryption: KeyRound,
|
||||
vacation: PalmtreeIcon,
|
||||
calendar: Calendar,
|
||||
contacts: BookUser,
|
||||
filters: Filter,
|
||||
templates: FileText,
|
||||
folders: FolderOpen,
|
||||
keywords: Tags,
|
||||
security: Shield,
|
||||
encryption: KeyRound,
|
||||
content_senders: EyeOff,
|
||||
calendar: Calendar,
|
||||
contacts: BookUser,
|
||||
files: HardDrive,
|
||||
sidebar_apps: PanelLeftClose,
|
||||
about_data: Info,
|
||||
themes: Palette,
|
||||
plugins: Puzzle,
|
||||
advanced: Wrench,
|
||||
debug: Bug,
|
||||
};
|
||||
|
||||
const tabGroupOrder: TabGroup[] = ['general', 'account', 'organization', 'apps', 'system'];
|
||||
const tabGroupOrder: TabGroup[] = ['general', 'appearance', 'mail', 'privacy', 'apps', 'advanced'];
|
||||
|
||||
// Map legacy tab IDs to current ones; runs once on read of localStorage.
|
||||
const LEGACY_TAB_MAP: Record<string, Tab> = {
|
||||
email: 'reading',
|
||||
advanced: 'about_data',
|
||||
};
|
||||
|
||||
function readPersistedTab(): Tab {
|
||||
try {
|
||||
const saved = localStorage.getItem('settings-active-tab');
|
||||
if (!saved) return 'appearance';
|
||||
if (saved in LEGACY_TAB_MAP) {
|
||||
const migrated = LEGACY_TAB_MAP[saved];
|
||||
try { localStorage.setItem('settings-active-tab', migrated); } catch { /* ignore */ }
|
||||
return migrated;
|
||||
}
|
||||
return saved as Tab;
|
||||
} catch {
|
||||
return 'appearance';
|
||||
}
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
@@ -107,13 +168,7 @@ export default function SettingsPage() {
|
||||
const { quota, isPushConnected } = useEmailStore();
|
||||
const { stalwartFeaturesEnabled } = useConfig();
|
||||
const { isFeatureEnabled } = usePolicyStore();
|
||||
const [activeTab, setActiveTab] = useState<Tab>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('settings-active-tab');
|
||||
if (saved) return saved as Tab;
|
||||
} catch { /* ignore */ }
|
||||
return 'appearance';
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<Tab>(readPersistedTab);
|
||||
const [mobileShowContent, setMobileShowContent] = useState(false);
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
@@ -124,21 +179,20 @@ export default function SettingsPage() {
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const dragStartWidth = useRef(256);
|
||||
|
||||
// Check auth on mount
|
||||
useEffect(() => {
|
||||
checkAuth().finally(() => {
|
||||
setInitialCheckDone(true);
|
||||
});
|
||||
}, [checkAuth]);
|
||||
|
||||
// Listen for tab change events from child components
|
||||
// Listen for tab change events from child components (with legacy migration)
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const tab = (e as CustomEvent).detail as Tab;
|
||||
if (tab) {
|
||||
setActiveTab(tab);
|
||||
try { localStorage.setItem('settings-active-tab', tab); } catch { /* ignore */ }
|
||||
}
|
||||
const raw = (e as CustomEvent).detail as string;
|
||||
if (!raw) return;
|
||||
const tab = (LEGACY_TAB_MAP[raw] ?? raw) as Tab;
|
||||
setActiveTab(tab);
|
||||
try { localStorage.setItem('settings-active-tab', tab); } catch { /* ignore */ }
|
||||
};
|
||||
window.addEventListener('settings-tab-change', handler);
|
||||
return () => window.removeEventListener('settings-tab-change', handler);
|
||||
@@ -161,25 +215,41 @@ export default function SettingsPage() {
|
||||
const supportsFiles = client?.supportsFiles() ?? false;
|
||||
|
||||
const tabs: TabDef[] = [
|
||||
{ id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'general' },
|
||||
{ id: 'email', label: t('tabs.email'), icon: tabIcons.email, group: 'general' },
|
||||
// General
|
||||
{ id: 'account', label: t('tabs.account'), icon: tabIcons.account, group: 'general' },
|
||||
{ id: 'language', label: t('tabs.language'), icon: tabIcons.language, group: 'general' },
|
||||
{ id: 'notifications', label: t('tabs.notifications'), icon: tabIcons.notifications, group: 'general' },
|
||||
{ id: 'account', label: t('tabs.account'), icon: tabIcons.account, group: 'account' },
|
||||
...(stalwartFeaturesEnabled ? [{ id: 'security' as Tab, label: t('tabs.security'), icon: tabIcons.security, group: 'account' as TabGroup }] : []),
|
||||
{ id: 'identities', label: t('tabs.identities'), icon: tabIcons.identities, group: 'account' },
|
||||
...(isFeatureEnabled('smimeEnabled') ? [{ id: 'encryption' as Tab, label: t('tabs.encryption'), icon: tabIcons.encryption, group: 'account' as TabGroup }] : []),
|
||||
...(supportsVacation ? [{ id: 'vacation' as Tab, label: t('tabs.vacation'), icon: tabIcons.vacation, group: 'account' as TabGroup }] : []),
|
||||
...(supportsSieve ? [{ id: 'filters' as Tab, label: t('tabs.filters'), icon: tabIcons.filters, group: 'organization' as TabGroup }] : []),
|
||||
...(isFeatureEnabled('templatesEnabled') ? [{ id: 'templates' as Tab, label: t('tabs.templates'), icon: tabIcons.templates, group: 'organization' as TabGroup }] : []),
|
||||
{ id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'organization' },
|
||||
...(isFeatureEnabled('customKeywordsEnabled') ? [{ id: 'keywords' as Tab, label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'organization' as TabGroup }] : []),
|
||||
|
||||
// Appearance
|
||||
{ id: 'appearance', label: t('tabs.appearance'), icon: tabIcons.appearance, group: 'appearance' },
|
||||
{ id: 'layout', label: t('tabs.layout'), icon: tabIcons.layout, group: 'appearance' },
|
||||
|
||||
// Mail
|
||||
{ id: 'reading', label: t('tabs.reading'), icon: tabIcons.reading, group: 'mail' },
|
||||
{ id: 'composing', label: t('tabs.composing'), icon: tabIcons.composing, group: 'mail' },
|
||||
{ id: 'identities', label: t('tabs.identities'), icon: tabIcons.identities, group: 'mail' },
|
||||
...(supportsVacation ? [{ id: 'vacation' as Tab, label: t('tabs.vacation'), icon: tabIcons.vacation, group: 'mail' as TabGroup }] : []),
|
||||
...(supportsSieve ? [{ id: 'filters' as Tab, label: t('tabs.filters'), icon: tabIcons.filters, group: 'mail' as TabGroup }] : []),
|
||||
...(isFeatureEnabled('templatesEnabled') ? [{ id: 'templates' as Tab, label: t('tabs.templates'), icon: tabIcons.templates, group: 'mail' as TabGroup }] : []),
|
||||
{ id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'mail' },
|
||||
...(isFeatureEnabled('customKeywordsEnabled') ? [{ id: 'keywords' as Tab, label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'mail' as TabGroup }] : []),
|
||||
|
||||
// Privacy & Security
|
||||
...(stalwartFeaturesEnabled ? [{ id: 'security' as Tab, label: t('tabs.security'), icon: tabIcons.security, group: 'privacy' as TabGroup }] : []),
|
||||
...(isFeatureEnabled('smimeEnabled') ? [{ id: 'encryption' as Tab, label: t('tabs.encryption'), icon: tabIcons.encryption, group: 'privacy' as TabGroup }] : []),
|
||||
{ id: 'content_senders', label: t('tabs.content_senders'), icon: tabIcons.content_senders, group: 'privacy' },
|
||||
|
||||
// Apps
|
||||
...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []),
|
||||
{ id: 'contacts', label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' },
|
||||
...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
|
||||
...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []),
|
||||
...(isFeatureEnabled('themesEnabled') ? [{ id: 'themes' as Tab, label: 'Themes', icon: tabIcons.themes, group: 'system' as TabGroup, experimental: true }] : []),
|
||||
...(isFeatureEnabled('pluginsEnabled') ? [{ id: 'plugins' as Tab, label: 'Plugins', icon: tabIcons.plugins, group: 'system' as TabGroup, experimental: true }] : []),
|
||||
{ id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' },
|
||||
|
||||
// Advanced
|
||||
{ id: 'about_data', label: t('tabs.about_data'), icon: tabIcons.about_data, group: 'advanced' },
|
||||
...(isFeatureEnabled('themesEnabled') ? [{ id: 'themes' as Tab, label: 'Themes', icon: tabIcons.themes, group: 'advanced' as TabGroup, experimental: true }] : []),
|
||||
...(isFeatureEnabled('pluginsEnabled') ? [{ id: 'plugins' as Tab, label: 'Plugins', icon: tabIcons.plugins, group: 'advanced' as TabGroup, experimental: true }] : []),
|
||||
...(isFeatureEnabled('debugModeEnabled') ? [{ id: 'debug' as Tab, label: t('tabs.debug'), icon: tabIcons.debug, group: 'advanced' as TabGroup }] : []),
|
||||
];
|
||||
|
||||
// Group tabs by category
|
||||
@@ -191,6 +261,10 @@ export default function SettingsPage() {
|
||||
}))
|
||||
.filter((g) => g.items.length > 0);
|
||||
|
||||
// If active tab is not in the visible list (e.g., feature disabled), fall back.
|
||||
const isActiveVisible = tabs.some((tab) => tab.id === activeTab);
|
||||
const effectiveActiveTab: Tab = isActiveVisible ? activeTab : 'appearance';
|
||||
|
||||
const handleTabSelect = (tabId: Tab) => {
|
||||
setActiveTab(tabId);
|
||||
try { localStorage.setItem('settings-active-tab', tabId); } catch { /* ignore */ }
|
||||
@@ -199,39 +273,42 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const activeTabLabel = tabs.find((tab) => tab.id === activeTab)?.label ?? '';
|
||||
const activeTabLabel = tabs.find((tab) => tab.id === effectiveActiveTab)?.label ?? '';
|
||||
|
||||
const renderTabContent = () => (
|
||||
<>
|
||||
{activeTab === 'appearance' && <AppearanceSettings />}
|
||||
{activeTab === 'email' && <EmailSettings />}
|
||||
{activeTab === 'notifications' && <NotificationSettings />}
|
||||
{activeTab === 'account' && <AccountSettings />}
|
||||
{activeTab === 'security' && <AccountSecuritySettings />}
|
||||
{activeTab === 'identities' && <IdentitySettings />}
|
||||
{activeTab === 'encryption' && <SmimeSettings />}
|
||||
{activeTab === 'vacation' && <VacationSettings />}
|
||||
{activeTab === 'calendar' && <><CalendarSettings /><div className="mt-8"><CalendarManagementSettings /></div></>}
|
||||
{activeTab === 'contacts' && <><ContactsSettings /><div className="mt-8"><AddressBookManagementSettings /></div></>}
|
||||
{activeTab === 'filters' && <FilterSettings />}
|
||||
{activeTab === 'templates' && <TemplateSettings />}
|
||||
{activeTab === 'folders' && <FolderSettings />}
|
||||
{activeTab === 'keywords' && <KeywordSettings />}
|
||||
{activeTab === 'files' && <FilesSettingsComponent />}
|
||||
{activeTab === 'sidebar_apps' && <SidebarAppsSettings />}
|
||||
{activeTab === 'themes' && <ThemesSettings />}
|
||||
{activeTab === 'plugins' && <PluginsSettings />}
|
||||
{activeTab === 'advanced' && <AdvancedSettings />}
|
||||
{effectiveActiveTab === 'account' && <AccountSettings />}
|
||||
{effectiveActiveTab === 'language' && <LanguageSettings />}
|
||||
{effectiveActiveTab === 'notifications' && <NotificationSettings />}
|
||||
{effectiveActiveTab === 'appearance' && <AppearanceSettings />}
|
||||
{effectiveActiveTab === 'layout' && <LayoutSettings />}
|
||||
{effectiveActiveTab === 'reading' && <ReadingSettings />}
|
||||
{effectiveActiveTab === 'composing' && <ComposingSettings />}
|
||||
{effectiveActiveTab === 'identities' && <IdentitySettings />}
|
||||
{effectiveActiveTab === 'vacation' && <VacationSettings />}
|
||||
{effectiveActiveTab === 'filters' && <FilterSettings />}
|
||||
{effectiveActiveTab === 'templates' && <TemplateSettings />}
|
||||
{effectiveActiveTab === 'folders' && <FolderSettings />}
|
||||
{effectiveActiveTab === 'keywords' && <KeywordSettings />}
|
||||
{effectiveActiveTab === 'security' && <AccountSecuritySettings />}
|
||||
{effectiveActiveTab === 'encryption' && <SmimeSettings />}
|
||||
{effectiveActiveTab === 'content_senders' && <ContentSendersSettings />}
|
||||
{effectiveActiveTab === 'calendar' && <><CalendarSettings /><div className="mt-8"><CalendarManagementSettings /></div></>}
|
||||
{effectiveActiveTab === 'contacts' && <><ContactsSettings /><div className="mt-8"><AddressBookManagementSettings /></div></>}
|
||||
{effectiveActiveTab === 'files' && <FilesSettingsComponent />}
|
||||
{effectiveActiveTab === 'sidebar_apps' && <SidebarAppsSettings />}
|
||||
{effectiveActiveTab === 'about_data' && <AboutDataSettings />}
|
||||
{effectiveActiveTab === 'themes' && <ThemesSettings />}
|
||||
{effectiveActiveTab === 'plugins' && <PluginsSettings />}
|
||||
{effectiveActiveTab === 'debug' && <DebugSettings />}
|
||||
</>
|
||||
);
|
||||
|
||||
// Mobile layout
|
||||
if (!isDesktop) {
|
||||
// Mobile: show content view
|
||||
if (mobileShowContent) {
|
||||
return (
|
||||
<div className="flex flex-col h-dvh bg-background">
|
||||
{/* Mobile content header */}
|
||||
<div className="flex items-center gap-2 px-4 h-14 border-b border-border bg-background shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -244,14 +321,12 @@ export default function SettingsPage() {
|
||||
<h1 className="font-semibold text-lg truncate">{activeTabLabel}</h1>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="bg-card border border-border rounded-lg p-4">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
@@ -264,10 +339,8 @@ export default function SettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile: show tab list
|
||||
return (
|
||||
<div className="flex flex-col h-dvh bg-background">
|
||||
{/* Mobile header */}
|
||||
<div className="flex items-center gap-2 px-4 h-14 border-b border-border bg-background shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -283,7 +356,6 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab list */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="py-2">
|
||||
{groupedTabs.map((group, groupIndex) => (
|
||||
@@ -319,7 +391,6 @@ export default function SettingsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="border-t border-border px-5 py-3">
|
||||
<button
|
||||
onClick={logout}
|
||||
@@ -331,7 +402,6 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<NavigationRail
|
||||
orientation="horizontal"
|
||||
onManageApps={handleManageApps}
|
||||
@@ -347,7 +417,6 @@ export default function SettingsPage() {
|
||||
// Desktop layout
|
||||
return (
|
||||
<div className="flex h-dvh bg-background">
|
||||
{/* Navigation Rail */}
|
||||
<div className="w-14 bg-secondary flex flex-col flex-shrink-0" style={{ borderRight: '1px solid rgba(128, 128, 128, 0.3)' }}>
|
||||
<NavigationRail
|
||||
collapsed
|
||||
@@ -366,7 +435,6 @@ export default function SettingsPage() {
|
||||
)}
|
||||
{!inlineApp && (
|
||||
<>
|
||||
{/* Settings Sidebar */}
|
||||
<div
|
||||
className={cn(
|
||||
"border-r border-border bg-secondary flex flex-col",
|
||||
@@ -374,7 +442,6 @@ export default function SettingsPage() {
|
||||
)}
|
||||
style={{ width: `${settingsSidebarWidth}px` }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b border-border">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -387,7 +454,6 @@ export default function SettingsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex-1 overflow-y-auto py-2" data-tour="settings-tabs">
|
||||
<div className="px-2 space-y-0.5">
|
||||
{groupedTabs.map((group, groupIndex) => (
|
||||
@@ -406,14 +472,14 @@ export default function SettingsPage() {
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
'w-full text-left px-3 py-2 rounded-md text-sm transition-colors duration-150 flex items-center gap-2.5',
|
||||
activeTab === tab.id
|
||||
effectiveActiveTab === tab.id
|
||||
? 'bg-accent text-accent-foreground font-medium'
|
||||
: 'hover:bg-muted text-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn(
|
||||
'w-4 h-4 shrink-0',
|
||||
activeTab === tab.id ? 'text-accent-foreground' : 'text-muted-foreground'
|
||||
effectiveActiveTab === tab.id ? 'text-accent-foreground' : 'text-muted-foreground'
|
||||
)} />
|
||||
{tab.label}
|
||||
{tab.experimental && (
|
||||
@@ -430,7 +496,6 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar resize handle */}
|
||||
<ResizeHandle
|
||||
onResizeStart={() => { dragStartWidth.current = settingsSidebarWidth; setIsResizing(true); }}
|
||||
onResize={(delta) => setSettingsSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))}
|
||||
@@ -441,10 +506,8 @@ export default function SettingsPage() {
|
||||
onDoubleClick={() => { setSettingsSidebarWidth(256); localStorage.setItem('settings-sidebar-width', '256'); }}
|
||||
/>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto p-8">
|
||||
{/* Page Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2.5 mb-2">
|
||||
<SettingsIcon className="w-6 h-6 text-muted-foreground" />
|
||||
@@ -452,7 +515,6 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Tab Content */}
|
||||
<div className="bg-card border border-border rounded-lg p-6">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
|
||||
+42
-84
@@ -7,22 +7,21 @@ import { useConfig } from '@/hooks/use-config';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { ALL_DEBUG_CATEGORIES } from '@/stores/settings-store';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { SpamSiegeGame } from './spam-siege-game';
|
||||
|
||||
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0";
|
||||
const GIT_COMMIT = process.env.NEXT_PUBLIC_GIT_COMMIT || "unknown";
|
||||
|
||||
export function AdvancedSettings() {
|
||||
export function AboutDataSettings() {
|
||||
const t = useTranslations('settings.advanced');
|
||||
const tCommon = useTranslations('common');
|
||||
const { debugMode, debugCategories, senderFavicons, settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } =
|
||||
const { settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } =
|
||||
useSettingsStore();
|
||||
const { settingsSyncEnabled } = useConfig();
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
||||
const { isFeatureEnabled } = usePolicyStore();
|
||||
const [showGame, setShowGame] = useState(false);
|
||||
const logoClickCount = useRef(0);
|
||||
const logoClickTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -86,7 +85,6 @@ export function AdvancedSettings() {
|
||||
return (
|
||||
<>
|
||||
{showGame && <SpamSiegeGame onClose={() => setShowGame(false)} />}
|
||||
{/* About */}
|
||||
<div className="rounded-lg border border-border bg-card p-5 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={handleLogoClick} className="flex items-center gap-4 flex-1 text-left focus:outline-none group/about cursor-pointer" aria-label="About">
|
||||
@@ -122,88 +120,48 @@ export function AdvancedSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Debug Mode */}
|
||||
{!isSettingHidden('debugMode') && isFeatureEnabled('debugModeEnabled') && (
|
||||
<SettingItem label={t('debug_mode.label')} description={t('debug_mode.description')} locked={isSettingLocked('debugMode')}>
|
||||
<ToggleSwitch checked={debugMode} onChange={(checked) => updateSetting('debugMode', checked)} />
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{settingsSyncEnabled && (
|
||||
<SettingItem label={t('settings_sync.label')} description={t('settings_sync.description')}>
|
||||
<ToggleSwitch checked={!settingsSyncDisabled} onChange={(checked) => updateSetting('settingsSyncDisabled', !checked)} />
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Debug Categories */}
|
||||
{debugMode && !isSettingHidden('debugMode') && isFeatureEnabled('debugModeEnabled') && (
|
||||
<div className="ml-4 border-l-2 border-muted pl-4 space-y-1">
|
||||
<p className="text-xs text-muted-foreground mb-2">{t('debug_categories.description')}</p>
|
||||
{ALL_DEBUG_CATEGORIES.map((cat) => (
|
||||
<SettingItem
|
||||
key={cat.id}
|
||||
label={t(`debug_categories.${cat.labelKey}`)}
|
||||
description={t(`debug_categories.${cat.labelKey}_description`)}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={debugCategories?.[cat.id] !== false}
|
||||
onChange={(checked) => {
|
||||
updateSetting('debugCategories', {
|
||||
...debugCategories,
|
||||
[cat.id]: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Settings Sync */}
|
||||
{settingsSyncEnabled && (
|
||||
<SettingItem label={t('settings_sync.label')} description={t('settings_sync.description')}>
|
||||
<ToggleSwitch checked={!settingsSyncDisabled} onChange={(checked) => updateSetting('settingsSyncDisabled', !checked)} />
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Sender Favicons (Experimental) */}
|
||||
<SettingItem label={t('sender_favicons.label')} description={t('sender_favicons.description')}>
|
||||
<ToggleSwitch checked={senderFavicons} onChange={(checked) => updateSetting('senderFavicons', checked)} />
|
||||
</SettingItem>
|
||||
|
||||
{/* Export Settings */}
|
||||
{isFeatureEnabled('settingsExportEnabled') && (
|
||||
<SettingItem label={t('export_settings.label')} description={t('export_settings.description')}>
|
||||
<Button variant="outline" size="sm" onClick={handleExport}>
|
||||
{t('export_settings.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Import Settings */}
|
||||
{isFeatureEnabled('settingsExportEnabled') && (
|
||||
<SettingItem label={t('import_settings.label')} description={t('import_settings.description')}>
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={handleImport}>
|
||||
{t('import_settings.button')}
|
||||
{isFeatureEnabled('settingsExportEnabled') && (
|
||||
<SettingItem label={t('export_settings.label')} description={t('export_settings.description')}>
|
||||
<Button variant="outline" size="sm" onClick={handleExport}>
|
||||
{t('export_settings.button')}
|
||||
</Button>
|
||||
</>
|
||||
</SettingItem>
|
||||
)}
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Reset Settings */}
|
||||
<SettingItem label={t('reset_settings.label')} description={t('reset_settings.description')}>
|
||||
<Button
|
||||
variant={showResetConfirm ? 'destructive' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
>
|
||||
{showResetConfirm ? tCommon('yes') : t('reset_settings.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
{isFeatureEnabled('settingsExportEnabled') && (
|
||||
<SettingItem label={t('import_settings.label')} description={t('import_settings.description')}>
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button variant="outline" size="sm" onClick={handleImport}>
|
||||
{t('import_settings.button')}
|
||||
</Button>
|
||||
</>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t('reset_settings.label')} description={t('reset_settings.description')}>
|
||||
<Button
|
||||
variant={showResetConfirm ? 'destructive' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
>
|
||||
{showResetConfirm ? tCommon('yes') : t('reset_settings.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,15 +2,13 @@
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
import { useSettingsStore, type ToolbarPosition, type Density } from '@/stores/settings-store';
|
||||
import { LanguageSwitcher } from '@/components/ui/language-switcher';
|
||||
import { useSettingsStore, type Density } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTour } from '@/components/tour/tour-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { PlayCircle } from 'lucide-react';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { useAccountStore } from '@/stores/account-store';
|
||||
|
||||
const DENSITY_PREVIEW: Record<Density, { py: string; gap: string; showAvatar: boolean; showPreview: boolean }> = {
|
||||
'extra-compact': { py: 'py-0.5', gap: 'gap-1.5', showAvatar: false, showPreview: false },
|
||||
@@ -66,16 +64,15 @@ function DensityPreview({ density }: { density: Density }) {
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const t = useTranslations('settings.appearance');
|
||||
const tAdvanced = useTranslations('settings.advanced');
|
||||
const tTour = useTranslations('tour');
|
||||
const { theme, setTheme } = useThemeStore();
|
||||
const { fontSize, density, animationsEnabled, toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, updateSetting } = useSettingsStore();
|
||||
const { fontSize, density, animationsEnabled, senderFavicons, updateSetting } = useSettingsStore();
|
||||
const { startTour, resetTourCompletion } = useTour();
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
const accounts = useAccountStore(s => s.accounts);
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Theme */}
|
||||
<SettingItem label={t('theme.label')} description={t('theme.description')}>
|
||||
<RadioGroup
|
||||
value={theme}
|
||||
@@ -88,12 +85,6 @@ export function AppearanceSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Language */}
|
||||
<SettingItem label={t('language.label')} description={t('language.description')}>
|
||||
<LanguageSwitcher />
|
||||
</SettingItem>
|
||||
|
||||
{/* Font Size */}
|
||||
{!isSettingHidden('fontSize') && (
|
||||
<SettingItem label={t('font_size.label')} description={t('font_size.description')} locked={isSettingLocked('fontSize')}>
|
||||
<RadioGroup
|
||||
@@ -108,7 +99,6 @@ export function AppearanceSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Density */}
|
||||
{!isSettingHidden('density') && (
|
||||
<SettingItem label={t('list_density.label')} description={t('list_density.description')} locked={isSettingLocked('density')}>
|
||||
<RadioGroup
|
||||
@@ -127,64 +117,6 @@ export function AppearanceSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Toolbar Position */}
|
||||
<SettingItem label={t('toolbar_position.label')} description={t('toolbar_position.description')}>
|
||||
<RadioGroup
|
||||
value={toolbarPosition}
|
||||
onChange={(value) => updateSetting('toolbarPosition', value as ToolbarPosition)}
|
||||
options={[
|
||||
{ value: 'top', label: t('toolbar_position.top') },
|
||||
{ value: 'below-subject', label: t('toolbar_position.below_subject') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Toolbar Labels */}
|
||||
<SettingItem label={t('toolbar_labels.label')} description={t('toolbar_labels.description')}>
|
||||
<ToggleSwitch
|
||||
checked={showToolbarLabels}
|
||||
onChange={(checked) => updateSetting('showToolbarLabels', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Hide Account Switcher */}
|
||||
<SettingItem label={t('hide_account_switcher.label')} description={t('hide_account_switcher.description')}>
|
||||
<ToggleSwitch
|
||||
checked={hideAccountSwitcher}
|
||||
onChange={(checked) => updateSetting('hideAccountSwitcher', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Show Rail Account List */}
|
||||
<SettingItem label={t('show_rail_account_list.label')} description={t('show_rail_account_list.description')}>
|
||||
<ToggleSwitch
|
||||
checked={showRailAccountList}
|
||||
onChange={(checked) => updateSetting('showRailAccountList', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Colorful Sidebar Icons */}
|
||||
<SettingItem label={t('colorful_sidebar_icons.label')} description={t('colorful_sidebar_icons.description')}>
|
||||
<ToggleSwitch
|
||||
checked={colorfulSidebarIcons}
|
||||
onChange={(checked) => updateSetting('colorfulSidebarIcons', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Unified Mailbox */}
|
||||
{accounts.length > 1 && (
|
||||
<SettingItem
|
||||
label={t('unified_mailbox.label')}
|
||||
description={t('unified_mailbox.description')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={enableUnifiedMailbox}
|
||||
onChange={(v) => updateSetting('enableUnifiedMailbox', v)}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Animations */}
|
||||
{!isSettingHidden('animationsEnabled') && (
|
||||
<SettingItem label={t('animations.label')} description={t('animations.description')} locked={isSettingLocked('animationsEnabled')}>
|
||||
<ToggleSwitch
|
||||
@@ -194,7 +126,10 @@ export function AppearanceSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Restart Tour */}
|
||||
<SettingItem label={tAdvanced('sender_favicons.label')} description={tAdvanced('sender_favicons.description')}>
|
||||
<ToggleSwitch checked={senderFavicons} onChange={(checked) => updateSetting('senderFavicons', checked)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={tTour('restart_title')} description={tTour('restart_desc')}>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import { Mail, X } from 'lucide-react';
|
||||
|
||||
export function ComposingSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const { appName } = useConfig();
|
||||
const [defaultMailStatus, setDefaultMailStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [newKeyword, setNewKeyword] = useState('');
|
||||
|
||||
const {
|
||||
autoSelectReplyIdentity,
|
||||
attachmentReminderEnabled,
|
||||
attachmentReminderKeywords,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
|
||||
const handleSetDefaultMailProgram = useCallback(() => {
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator.registerProtocolHandler) {
|
||||
navigator.registerProtocolHandler('mailto', `${window.location.origin}/compose?mailto=%s`);
|
||||
setDefaultMailStatus('success');
|
||||
}
|
||||
} catch {
|
||||
setDefaultMailStatus('error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
<SettingItem label={t('auto_select_reply_identity.label')} description={t('auto_select_reply_identity.description')}>
|
||||
<ToggleSwitch
|
||||
checked={autoSelectReplyIdentity}
|
||||
onChange={(checked) => updateSetting('autoSelectReplyIdentity', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('attachment_reminder.label')} description={t('attachment_reminder.description')}>
|
||||
<ToggleSwitch
|
||||
checked={attachmentReminderEnabled}
|
||||
onChange={(checked) => updateSetting('attachmentReminderEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
{attachmentReminderEnabled && (
|
||||
<div className="py-3 border-b border-border space-y-2">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{t('attachment_reminder.keywords_label')}</label>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('attachment_reminder.keywords_description')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{attachmentReminderKeywords.map((kw) => (
|
||||
<span key={kw} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-muted text-foreground">
|
||||
{kw}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('attachment_reminder.remove')}
|
||||
onClick={() => updateSetting('attachmentReminderKeywords', attachmentReminderKeywords.filter(k => k !== kw))}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<form
|
||||
className="flex gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const trimmed = newKeyword.trim().toLowerCase();
|
||||
if (trimmed && !attachmentReminderKeywords.includes(trimmed)) {
|
||||
updateSetting('attachmentReminderKeywords', [...attachmentReminderKeywords, trimmed]);
|
||||
}
|
||||
setNewKeyword('');
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
placeholder={t('attachment_reminder.add_placeholder')}
|
||||
className="flex-1 min-w-0 px-2 py-1 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newKeyword.trim()}
|
||||
className="px-3 py-1 text-sm bg-muted hover:bg-accent rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t('attachment_reminder.add')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SettingItem label={t('default_mail_program.label')} description={t('default_mail_program.description', { appName: appName || 'Bulwark' })}>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
onClick={handleSetDefaultMailProgram}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
<span className="text-sm text-foreground">{t('default_mail_program.button')}</span>
|
||||
</button>
|
||||
{defaultMailStatus === 'success' && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">{t('default_mail_program.success')}</p>
|
||||
)}
|
||||
{defaultMailStatus === 'error' && (
|
||||
<p className="text-xs text-destructive">{t('default_mail_program.error')}</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { useContactStore } from '@/stores/contact-store';
|
||||
|
||||
export function ContentSendersSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const [showTrustedModal, setShowTrustedModal] = useState(false);
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
|
||||
const {
|
||||
externalContentPolicy,
|
||||
emailAlwaysLightMode,
|
||||
trustedSenders,
|
||||
trustedSendersAddressBook,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
const { trustedSenderEmails } = useContactStore();
|
||||
|
||||
const getTrustedSendersCount = () => {
|
||||
const count = trustedSendersAddressBook ? trustedSenderEmails.length : trustedSenders.length;
|
||||
if (count === 0) return t('trusted_senders.count_zero');
|
||||
if (count === 1) return t('trusted_senders.count_one');
|
||||
return t('trusted_senders.count_other', { count });
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{!isSettingHidden('externalContentPolicy') && (
|
||||
<SettingItem label={t('external_content.label')} description={t('external_content.description')} locked={isSettingLocked('externalContentPolicy')}>
|
||||
<Select
|
||||
value={externalContentPolicy}
|
||||
onChange={(value) =>
|
||||
updateSetting('externalContentPolicy', value as 'ask' | 'block' | 'allow')
|
||||
}
|
||||
options={[
|
||||
{ value: 'ask', label: t('external_content.ask') },
|
||||
{ value: 'block', label: t('external_content.block') },
|
||||
{ value: 'allow', label: t('external_content.allow') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t('always_light_mode.label')} description={t('always_light_mode.description')}>
|
||||
<ToggleSwitch
|
||||
checked={emailAlwaysLightMode}
|
||||
onChange={(checked) => updateSetting('emailAlwaysLightMode', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('trusted_senders.label')} description={t('trusted_senders.description')}>
|
||||
<button
|
||||
onClick={() => setShowTrustedModal(true)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors"
|
||||
>
|
||||
<span className="text-sm text-foreground">{getTrustedSendersCount()}</span>
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('trusted_senders.use_address_book_label')} description={t('trusted_senders.use_address_book_description')}>
|
||||
<ToggleSwitch
|
||||
checked={trustedSendersAddressBook}
|
||||
onChange={(checked) => updateSetting('trustedSendersAddressBook', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<TrustedSendersModal
|
||||
isOpen={showTrustedModal}
|
||||
onClose={() => setShowTrustedModal(false)}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore, ALL_DEBUG_CATEGORIES } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
|
||||
export function DebugSettings() {
|
||||
const t = useTranslations('settings.advanced');
|
||||
const { debugMode, debugCategories, updateSetting } = useSettingsStore();
|
||||
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
||||
|
||||
if (isSettingHidden('debugMode') || !isFeatureEnabled('debugModeEnabled')) {
|
||||
return (
|
||||
<SettingsSection title={t('debug_mode.label')} description={t('debug_mode.description')}>
|
||||
<p className="text-sm text-muted-foreground py-2">{t('debug_mode.description')}</p>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('debug_mode.label')} description={t('debug_mode.description')}>
|
||||
<SettingItem label={t('debug_mode.label')} description={t('debug_mode.description')} locked={isSettingLocked('debugMode')}>
|
||||
<ToggleSwitch checked={debugMode} onChange={(checked) => updateSetting('debugMode', checked)} />
|
||||
</SettingItem>
|
||||
|
||||
{debugMode && (
|
||||
<div className="ml-4 border-l-2 border-muted pl-4 space-y-1">
|
||||
<p className="text-xs text-muted-foreground mb-2">{t('debug_categories.description')}</p>
|
||||
{ALL_DEBUG_CATEGORIES.map((cat) => (
|
||||
<SettingItem
|
||||
key={cat.id}
|
||||
label={t(`debug_categories.${cat.labelKey}`)}
|
||||
description={t(`debug_categories.${cat.labelKey}_description`)}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={debugCategories?.[cat.id] !== false}
|
||||
onChange={(checked) => {
|
||||
updateSetting('debugCategories', {
|
||||
...debugCategories,
|
||||
[cat.id]: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { LanguageSwitcher } from '@/components/ui/language-switcher';
|
||||
import { SettingsSection, SettingItem } from './settings-section';
|
||||
|
||||
export function LanguageSettings() {
|
||||
const t = useTranslations('settings.appearance');
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('language.label')} description={t('language.description')}>
|
||||
<SettingItem label={t('language.label')} description={t('language.description')}>
|
||||
<LanguageSwitcher />
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore, type ToolbarPosition, type MailLayout } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { useAccountStore } from '@/stores/account-store';
|
||||
|
||||
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
||||
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
|
||||
{ sender: 'Nadia', subject: 'Design sync', preview: 'Pushed updated mocks and notes.', selected: true },
|
||||
{ sender: 'Billing', subject: 'Invoice 1042', preview: 'Your receipt is attached.', selected: false },
|
||||
];
|
||||
|
||||
function MailLayoutPreview({
|
||||
value,
|
||||
t,
|
||||
}: {
|
||||
value: MailLayout;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const isSplit = value === 'split';
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-xl border border-border bg-background p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">{t(`mail_layout.${value}`)}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{t(`mail_layout.${value}_description`)}</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-border bg-muted/20">
|
||||
<div className="flex h-28">
|
||||
<div className="w-11 border-r border-border bg-muted/40" />
|
||||
|
||||
{isSplit ? (
|
||||
<>
|
||||
<div className="w-28 border-r border-border bg-background">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
|
||||
<div
|
||||
key={row.subject}
|
||||
className={cn(
|
||||
'border-b border-border px-2 py-1.5 text-[10px] last:border-b-0',
|
||||
row.selected && 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className="truncate font-medium text-foreground">{row.sender}</div>
|
||||
<div className="truncate text-muted-foreground">{row.subject}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 bg-background px-3 py-2">
|
||||
<div className="h-2.5 w-20 rounded bg-foreground/10" />
|
||||
<div className="mt-2 h-2 w-full rounded bg-foreground/10" />
|
||||
<div className="mt-1.5 h-2 w-5/6 rounded bg-foreground/10" />
|
||||
<div className="mt-1.5 h-2 w-2/3 rounded bg-foreground/10" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 bg-background px-2 py-2">
|
||||
<div className="space-y-1.5">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
|
||||
<div
|
||||
key={row.subject}
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1 text-[10px]',
|
||||
row.selected ? 'bg-primary/10' : 'bg-muted/20'
|
||||
)}
|
||||
>
|
||||
<div className="truncate text-foreground">
|
||||
<span className="font-medium">{row.sender}</span>
|
||||
<span className="mx-1.5 text-muted-foreground">{row.subject}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutSettings() {
|
||||
const t = useTranslations('settings.appearance');
|
||||
const tEmail = useTranslations('settings.email_behavior');
|
||||
const { toolbarPosition, showToolbarLabels, hideAccountSwitcher, showRailAccountList, enableUnifiedMailbox, colorfulSidebarIcons, mailLayout, updateSetting } = useSettingsStore();
|
||||
const { isSettingLocked, isSettingHidden } = usePolicyStore();
|
||||
const accounts = useAccountStore(s => s.accounts);
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{!isSettingHidden('mailLayout') && (
|
||||
<SettingItem label={tEmail('mail_layout.label')} description={tEmail('mail_layout.description')} locked={isSettingLocked('mailLayout')}>
|
||||
<div className="w-[22rem] max-w-full">
|
||||
<RadioGroup
|
||||
value={mailLayout}
|
||||
onChange={(value) => updateSetting('mailLayout', value as MailLayout)}
|
||||
options={[
|
||||
{ value: 'split', label: tEmail('mail_layout.split') },
|
||||
{ value: 'focus', label: tEmail('mail_layout.focus') },
|
||||
]}
|
||||
/>
|
||||
<MailLayoutPreview value={mailLayout} t={tEmail} />
|
||||
</div>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem label={t('toolbar_position.label')} description={t('toolbar_position.description')}>
|
||||
<RadioGroup
|
||||
value={toolbarPosition}
|
||||
onChange={(value) => updateSetting('toolbarPosition', value as ToolbarPosition)}
|
||||
options={[
|
||||
{ value: 'top', label: t('toolbar_position.top') },
|
||||
{ value: 'below-subject', label: t('toolbar_position.below_subject') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('toolbar_labels.label')} description={t('toolbar_labels.description')}>
|
||||
<ToggleSwitch
|
||||
checked={showToolbarLabels}
|
||||
onChange={(checked) => updateSetting('showToolbarLabels', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('hide_account_switcher.label')} description={t('hide_account_switcher.description')}>
|
||||
<ToggleSwitch
|
||||
checked={hideAccountSwitcher}
|
||||
onChange={(checked) => updateSetting('hideAccountSwitcher', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('show_rail_account_list.label')} description={t('show_rail_account_list.description')}>
|
||||
<ToggleSwitch
|
||||
checked={showRailAccountList}
|
||||
onChange={(checked) => updateSetting('showRailAccountList', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('colorful_sidebar_icons.label')} description={t('colorful_sidebar_icons.description')}>
|
||||
<ToggleSwitch
|
||||
checked={colorfulSidebarIcons}
|
||||
onChange={(checked) => updateSetting('colorfulSidebarIcons', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{accounts.length > 1 && (
|
||||
<SettingItem
|
||||
label={t('unified_mailbox.label')}
|
||||
description={t('unified_mailbox.description')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={enableUnifiedMailbox}
|
||||
onChange={(v) => updateSetting('enableUnifiedMailbox', v)}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import type { ArchiveMode, HoverAction, MailLayout } from '@/stores/settings-store';
|
||||
import type { ArchiveMode, HoverAction } from '@/stores/settings-store';
|
||||
import { ALL_HOVER_ACTIONS } from '@/stores/settings-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useEmailStore } from '@/stores/email-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { RadioGroup, SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { TrustedSendersModal } from '@/components/trusted-senders-modal';
|
||||
import { ChevronRight, AlertTriangle, FolderSync, Loader2, Mail, X } from 'lucide-react';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { AlertTriangle, FolderSync, Loader2 } from 'lucide-react';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { useContactStore } from '@/stores/contact-store';
|
||||
|
||||
const MAIL_LAYOUT_PREVIEW_ROWS = [
|
||||
{ sender: 'Alice', subject: 'Quarterly roadmap', preview: 'The draft is ready for review.', selected: false },
|
||||
{ sender: 'Nadia', subject: 'Design sync', preview: 'Pushed updated mocks and notes.', selected: true },
|
||||
{ sender: 'Billing', subject: 'Invoice 1042', preview: 'Your receipt is attached.', selected: false },
|
||||
];
|
||||
|
||||
function MailLayoutPreview({
|
||||
value,
|
||||
t,
|
||||
}: {
|
||||
value: MailLayout;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const isSplit = value === 'split';
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-xl border border-border bg-background p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">{t(`mail_layout.${value}`)}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{t(`mail_layout.${value}_description`)}</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 overflow-hidden rounded-lg border border-border bg-muted/20">
|
||||
<div className="flex h-28">
|
||||
<div className="w-11 border-r border-border bg-muted/40" />
|
||||
|
||||
{isSplit ? (
|
||||
<>
|
||||
<div className="w-28 border-r border-border bg-background">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
|
||||
<div
|
||||
key={row.subject}
|
||||
className={cn(
|
||||
'border-b border-border px-2 py-1.5 text-[10px] last:border-b-0',
|
||||
row.selected && 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className="truncate font-medium text-foreground">{row.sender}</div>
|
||||
<div className="truncate text-muted-foreground">{row.subject}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 bg-background px-3 py-2">
|
||||
<div className="h-2.5 w-20 rounded bg-foreground/10" />
|
||||
<div className="mt-2 h-2 w-full rounded bg-foreground/10" />
|
||||
<div className="mt-1.5 h-2 w-5/6 rounded bg-foreground/10" />
|
||||
<div className="mt-1.5 h-2 w-2/3 rounded bg-foreground/10" />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 bg-background px-2 py-2">
|
||||
<div className="space-y-1.5">
|
||||
{MAIL_LAYOUT_PREVIEW_ROWS.map((row) => (
|
||||
<div
|
||||
key={row.subject}
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1 text-[10px]',
|
||||
row.selected ? 'bg-primary/10' : 'bg-muted/20'
|
||||
)}
|
||||
>
|
||||
<div className="truncate text-foreground">
|
||||
<span className="font-medium">{row.sender}</span>
|
||||
<span className="mx-1.5 text-muted-foreground">{row.subject}</span>
|
||||
<span className="text-muted-foreground/80">{row.preview}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmailSettings() {
|
||||
export function ReadingSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
const { appName } = useConfig();
|
||||
const [showTrustedModal, setShowTrustedModal] = useState(false);
|
||||
const [isReorganizing, setIsReorganizing] = useState(false);
|
||||
const [reorganizeResult, setReorganizeResult] = useState<string | null>(null);
|
||||
const [defaultMailStatus, setDefaultMailStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
|
||||
|
||||
const handleSetDefaultMailProgram = useCallback(() => {
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator.registerProtocolHandler) {
|
||||
navigator.registerProtocolHandler('mailto', `${window.location.origin}/compose?mailto=%s`);
|
||||
setDefaultMailStatus('success');
|
||||
}
|
||||
} catch {
|
||||
setDefaultMailStatus('error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [newKeyword, setNewKeyword] = useState('');
|
||||
|
||||
const {
|
||||
markAsReadDelay,
|
||||
deleteAction,
|
||||
@@ -120,33 +25,17 @@ export function EmailSettings() {
|
||||
showPreview,
|
||||
mailLayout,
|
||||
disableThreading,
|
||||
autoSelectReplyIdentity,
|
||||
plainTextMode,
|
||||
emailsPerPage,
|
||||
externalContentPolicy,
|
||||
mailAttachmentAction,
|
||||
attachmentPosition,
|
||||
emailAlwaysLightMode,
|
||||
archiveMode,
|
||||
hoverActions,
|
||||
hoverActionsMode,
|
||||
hoverActionsCorner,
|
||||
trustedSenders,
|
||||
trustedSendersAddressBook,
|
||||
attachmentReminderEnabled,
|
||||
attachmentReminderKeywords,
|
||||
hideInlineImageAttachments,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
const { trustedSenderEmails } = useContactStore();
|
||||
|
||||
// Get count label for trusted senders button
|
||||
const getTrustedSendersCount = () => {
|
||||
const count = trustedSendersAddressBook ? trustedSenderEmails.length : trustedSenders.length;
|
||||
if (count === 0) return t('trusted_senders.count_zero');
|
||||
if (count === 1) return t('trusted_senders.count_one');
|
||||
return t('trusted_senders.count_other', { count });
|
||||
};
|
||||
|
||||
const isFocusedLayout = mailLayout === 'focus';
|
||||
|
||||
@@ -163,8 +52,6 @@ export function EmailSettings() {
|
||||
|
||||
try {
|
||||
const archiveId = archiveMailbox.originalId || archiveMailbox.id;
|
||||
|
||||
// Fetch all emails in the root archive mailbox
|
||||
const emails = await client.getEmailsInMailbox(archiveId);
|
||||
let movedCount = 0;
|
||||
|
||||
@@ -173,10 +60,8 @@ export function EmailSettings() {
|
||||
const year = emailDate.getFullYear().toString();
|
||||
const month = (emailDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
|
||||
// Re-read mailboxes from store each iteration in case new ones were created
|
||||
let currentMailboxes = useEmailStore.getState().mailboxes;
|
||||
|
||||
// Find or create year subfolder
|
||||
let yearMailbox = currentMailboxes.find(
|
||||
m => m.name === year && m.parentId === archiveId
|
||||
);
|
||||
@@ -190,7 +75,6 @@ export function EmailSettings() {
|
||||
await client.moveEmail(email.id, yearMailbox.id);
|
||||
movedCount++;
|
||||
} else {
|
||||
// month mode
|
||||
const yearId = yearMailbox.originalId || yearMailbox.id;
|
||||
let monthMailbox = currentMailboxes.find(
|
||||
m => m.name === month && m.parentId === yearId
|
||||
@@ -215,7 +99,6 @@ export function EmailSettings() {
|
||||
|
||||
return (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
{/* Mark as Read */}
|
||||
{!isSettingHidden('markAsReadDelay') && (
|
||||
<SettingItem label={t('mark_read.label')} description={t('mark_read.description')} locked={isSettingLocked('markAsReadDelay')}>
|
||||
<Select
|
||||
@@ -231,7 +114,6 @@ export function EmailSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Delete Action */}
|
||||
{!isSettingHidden('deleteAction') && (
|
||||
<SettingItem label={t('delete_action.label')} description={t('delete_action.description')} locked={isSettingLocked('deleteAction')}>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -253,7 +135,6 @@ export function EmailSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Archive Mode */}
|
||||
<SettingItem label={t('archive_mode.label')} description={t('archive_mode.description')}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select
|
||||
@@ -287,7 +168,6 @@ export function EmailSettings() {
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
{/* Permanently Delete Junk */}
|
||||
<SettingItem label={t('permanently_delete_junk.label')} description={t('permanently_delete_junk.description')}>
|
||||
<ToggleSwitch
|
||||
checked={permanentlyDeleteJunk}
|
||||
@@ -295,23 +175,6 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{!isSettingHidden('mailLayout') && (
|
||||
<SettingItem label={t('mail_layout.label')} description={t('mail_layout.description')} locked={isSettingLocked('mailLayout')}>
|
||||
<div className="w-[22rem] max-w-full">
|
||||
<RadioGroup
|
||||
value={mailLayout}
|
||||
onChange={(value) => updateSetting('mailLayout', value as MailLayout)}
|
||||
options={[
|
||||
{ value: 'split', label: t('mail_layout.split') },
|
||||
{ value: 'focus', label: t('mail_layout.focus') },
|
||||
]}
|
||||
/>
|
||||
<MailLayoutPreview value={mailLayout} t={t} />
|
||||
</div>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Show Preview */}
|
||||
{!isSettingHidden('showPreview') && (
|
||||
<SettingItem
|
||||
label={t('show_preview.label')}
|
||||
@@ -322,7 +185,6 @@ export function EmailSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Disable Thread Grouping */}
|
||||
<SettingItem label={t('disable_threading.label')} description={t('disable_threading.description')}>
|
||||
<ToggleSwitch
|
||||
checked={disableThreading}
|
||||
@@ -330,7 +192,6 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Plain Text Mode */}
|
||||
<SettingItem label={t('plain_text_mode.label')} description={t('plain_text_mode.description')}>
|
||||
<ToggleSwitch
|
||||
checked={plainTextMode}
|
||||
@@ -338,71 +199,6 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('auto_select_reply_identity.label')} description={t('auto_select_reply_identity.description')}>
|
||||
<ToggleSwitch
|
||||
checked={autoSelectReplyIdentity}
|
||||
onChange={(checked) => updateSetting('autoSelectReplyIdentity', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Attachment Reminder */}
|
||||
<SettingItem label={t('attachment_reminder.label')} description={t('attachment_reminder.description')}>
|
||||
<ToggleSwitch
|
||||
checked={attachmentReminderEnabled}
|
||||
onChange={(checked) => updateSetting('attachmentReminderEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
{attachmentReminderEnabled && (
|
||||
<div className="py-3 border-b border-border space-y-2">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{t('attachment_reminder.keywords_label')}</label>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('attachment_reminder.keywords_description')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{attachmentReminderKeywords.map((kw) => (
|
||||
<span key={kw} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-muted text-foreground">
|
||||
{kw}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('attachment_reminder.remove')}
|
||||
onClick={() => updateSetting('attachmentReminderKeywords', attachmentReminderKeywords.filter(k => k !== kw))}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<form
|
||||
className="flex gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const trimmed = newKeyword.trim().toLowerCase();
|
||||
if (trimmed && !attachmentReminderKeywords.includes(trimmed)) {
|
||||
updateSetting('attachmentReminderKeywords', [...attachmentReminderKeywords, trimmed]);
|
||||
}
|
||||
setNewKeyword('');
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={newKeyword}
|
||||
onChange={(e) => setNewKeyword(e.target.value)}
|
||||
placeholder={t('attachment_reminder.add_placeholder')}
|
||||
className="flex-1 min-w-0 px-2 py-1 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newKeyword.trim()}
|
||||
className="px-3 py-1 text-sm bg-muted hover:bg-accent rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t('attachment_reminder.add')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hide inline images from attachment list */}
|
||||
<SettingItem label={t('hide_inline_image_attachments.label')} description={t('hide_inline_image_attachments.description')}>
|
||||
<ToggleSwitch
|
||||
checked={hideInlineImageAttachments}
|
||||
@@ -410,7 +206,6 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Quick Hover Actions */}
|
||||
{isFeatureEnabled('hoverActionsConfigEnabled') && (
|
||||
<div className="py-3 border-b border-border space-y-3">
|
||||
<div>
|
||||
@@ -443,7 +238,6 @@ export function EmailSettings() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Hover Actions Display Mode */}
|
||||
<div className="pt-2 space-y-2">
|
||||
<label className="text-xs font-medium text-foreground">{t('hover_actions.mode_label')}</label>
|
||||
<div className="flex gap-2">
|
||||
@@ -465,7 +259,6 @@ export function EmailSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Corner Selection (only when floating) */}
|
||||
{hoverActionsMode === 'floating' && (
|
||||
<div className="pt-1 space-y-2">
|
||||
<label className="text-xs font-medium text-foreground">{t('hover_actions.corner_label')}</label>
|
||||
@@ -513,7 +306,6 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Emails Per Page */}
|
||||
{!isSettingHidden('emailsPerPage') && (
|
||||
<SettingItem label={t('emails_per_page.label')} description={t('emails_per_page.description')} locked={isSettingLocked('emailsPerPage')}>
|
||||
<Select
|
||||
@@ -528,75 +320,6 @@ export function EmailSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Always Light Mode for Emails */}
|
||||
<SettingItem label={t('always_light_mode.label')} description={t('always_light_mode.description')}>
|
||||
<ToggleSwitch
|
||||
checked={emailAlwaysLightMode}
|
||||
onChange={(checked) => updateSetting('emailAlwaysLightMode', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* External Content */}
|
||||
{!isSettingHidden('externalContentPolicy') && (
|
||||
<SettingItem label={t('external_content.label')} description={t('external_content.description')} locked={isSettingLocked('externalContentPolicy')}>
|
||||
<Select
|
||||
value={externalContentPolicy}
|
||||
onChange={(value) =>
|
||||
updateSetting('externalContentPolicy', value as 'ask' | 'block' | 'allow')
|
||||
}
|
||||
options={[
|
||||
{ value: 'ask', label: t('external_content.ask') },
|
||||
{ value: 'block', label: t('external_content.block') },
|
||||
{ value: 'allow', label: t('external_content.allow') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{/* Default Mail Program */}
|
||||
<SettingItem label={t('default_mail_program.label')} description={t('default_mail_program.description', { appName: appName || 'Bulwark' })}>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
onClick={handleSetDefaultMailProgram}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
<span className="text-sm text-foreground">{t('default_mail_program.button')}</span>
|
||||
</button>
|
||||
{defaultMailStatus === 'success' && (
|
||||
<p className="text-xs text-green-600 dark:text-green-400">{t('default_mail_program.success')}</p>
|
||||
)}
|
||||
{defaultMailStatus === 'error' && (
|
||||
<p className="text-xs text-destructive">{t('default_mail_program.error')}</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
{/* Trusted Senders */}
|
||||
<SettingItem label={t('trusted_senders.label')} description={t('trusted_senders.description')}>
|
||||
<button
|
||||
onClick={() => setShowTrustedModal(true)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-accent rounded-md transition-colors"
|
||||
>
|
||||
<span className="text-sm text-foreground">{getTrustedSendersCount()}</span>
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</SettingItem>
|
||||
|
||||
{/* Trusted Senders - address book storage */}
|
||||
<SettingItem label={t('trusted_senders.use_address_book_label')} description={t('trusted_senders.use_address_book_description')}>
|
||||
<ToggleSwitch
|
||||
checked={trustedSendersAddressBook}
|
||||
onChange={(checked) => updateSetting('trustedSendersAddressBook', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
{/* Trusted Senders Modal */}
|
||||
<TrustedSendersModal
|
||||
isOpen={showTrustedModal}
|
||||
onClose={() => setShowTrustedModal(false)}
|
||||
/>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "Dateien",
|
||||
"contacts": "Kontakte",
|
||||
"sidebar_apps": "Sidebar-Apps",
|
||||
"notifications": "Benachrichtigungen"
|
||||
"notifications": "Benachrichtigungen",
|
||||
"layout": "Layout",
|
||||
"reading": "Lesen",
|
||||
"composing": "Verfassen",
|
||||
"content_senders": "Inhalte & Absender",
|
||||
"about_data": "Über & Daten",
|
||||
"debug": "Debug"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Allgemein",
|
||||
"account": "Konto & Identität",
|
||||
"organization": "E-Mail-Organisation",
|
||||
"apps": "Apps",
|
||||
"system": "System"
|
||||
"system": "System",
|
||||
"appearance": "Darstellung",
|
||||
"mail": "E-Mail",
|
||||
"privacy": "Datenschutz & Sicherheit",
|
||||
"advanced": "Erweitert"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Darstellung",
|
||||
|
||||
+12
-2
@@ -673,14 +673,24 @@
|
||||
"contacts": "Contacts",
|
||||
"encryption": "Encryption",
|
||||
"sidebar_apps": "Sidebar Apps",
|
||||
"notifications": "Notifications"
|
||||
"notifications": "Notifications",
|
||||
"layout": "Layout",
|
||||
"reading": "Reading",
|
||||
"composing": "Composing",
|
||||
"content_senders": "Content & Senders",
|
||||
"about_data": "About & Data",
|
||||
"debug": "Debug"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "General",
|
||||
"account": "Account & Identity",
|
||||
"organization": "Mail Organization",
|
||||
"apps": "Apps",
|
||||
"system": "System"
|
||||
"system": "System",
|
||||
"appearance": "Appearance",
|
||||
"mail": "Mail",
|
||||
"privacy": "Privacy & Security",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "Archivos",
|
||||
"contacts": "Contactos",
|
||||
"sidebar_apps": "Apps de barra lateral",
|
||||
"notifications": "Notificaciones"
|
||||
"notifications": "Notificaciones",
|
||||
"layout": "Diseño",
|
||||
"reading": "Lectura",
|
||||
"composing": "Redacción",
|
||||
"content_senders": "Contenido y remitentes",
|
||||
"about_data": "Acerca de y datos",
|
||||
"debug": "Depuración"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "General",
|
||||
"account": "Cuenta e identidad",
|
||||
"organization": "Organización del correo",
|
||||
"apps": "Aplicaciones",
|
||||
"system": "Sistema"
|
||||
"system": "Sistema",
|
||||
"appearance": "Apariencia",
|
||||
"mail": "Correo",
|
||||
"privacy": "Privacidad y seguridad",
|
||||
"advanced": "Avanzado"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Apariencia",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "Fichiers",
|
||||
"contacts": "Contacts",
|
||||
"sidebar_apps": "Apps de la barre latérale",
|
||||
"notifications": "Notifications"
|
||||
"notifications": "Notifications",
|
||||
"layout": "Mise en page",
|
||||
"reading": "Lecture",
|
||||
"composing": "Rédaction",
|
||||
"content_senders": "Contenu et expéditeurs",
|
||||
"about_data": "À propos et données",
|
||||
"debug": "Débogage"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Général",
|
||||
"account": "Compte & Identité",
|
||||
"organization": "Organisation des e-mails",
|
||||
"apps": "Applications",
|
||||
"system": "Système"
|
||||
"system": "Système",
|
||||
"appearance": "Apparence",
|
||||
"mail": "Courrier",
|
||||
"privacy": "Confidentialité et sécurité",
|
||||
"advanced": "Avancé"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Apparence",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "File",
|
||||
"contacts": "Contatti",
|
||||
"sidebar_apps": "App nella barra laterale",
|
||||
"notifications": "Notifiche"
|
||||
"notifications": "Notifiche",
|
||||
"layout": "Layout",
|
||||
"reading": "Lettura",
|
||||
"composing": "Composizione",
|
||||
"content_senders": "Contenuto e mittenti",
|
||||
"about_data": "Informazioni e dati",
|
||||
"debug": "Debug"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Generale",
|
||||
"account": "Account e identità",
|
||||
"organization": "Organizzazione e-mail",
|
||||
"apps": "Applicazioni",
|
||||
"system": "Sistema"
|
||||
"system": "Sistema",
|
||||
"appearance": "Aspetto",
|
||||
"mail": "Posta",
|
||||
"privacy": "Privacy e sicurezza",
|
||||
"advanced": "Avanzate"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Aspetto",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "ファイル",
|
||||
"contacts": "連絡先",
|
||||
"sidebar_apps": "サイドバーアプリ",
|
||||
"notifications": "通知"
|
||||
"notifications": "通知",
|
||||
"layout": "レイアウト",
|
||||
"reading": "閲覧",
|
||||
"composing": "作成",
|
||||
"content_senders": "コンテンツと送信者",
|
||||
"about_data": "情報とデータ",
|
||||
"debug": "デバッグ"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "一般",
|
||||
"account": "アカウントと身元",
|
||||
"organization": "メール整理",
|
||||
"apps": "アプリ",
|
||||
"system": "システム"
|
||||
"system": "システム",
|
||||
"appearance": "外観",
|
||||
"mail": "メール",
|
||||
"privacy": "プライバシーとセキュリティ",
|
||||
"advanced": "詳細"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "外観",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"contacts": "연락처",
|
||||
"encryption": "암호화",
|
||||
"sidebar_apps": "사이드바 앱",
|
||||
"notifications": "알림"
|
||||
"notifications": "알림",
|
||||
"layout": "레이아웃",
|
||||
"reading": "읽기",
|
||||
"composing": "작성",
|
||||
"content_senders": "콘텐츠 및 발신자",
|
||||
"about_data": "정보 및 데이터",
|
||||
"debug": "디버그"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "일반",
|
||||
"account": "계정 및 인증",
|
||||
"organization": "메일 정리",
|
||||
"apps": "앱",
|
||||
"system": "시스템"
|
||||
"system": "시스템",
|
||||
"appearance": "모양",
|
||||
"mail": "메일",
|
||||
"privacy": "개인정보 및 보안",
|
||||
"advanced": "고급"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "화면 설정",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"contacts": "Kontakti",
|
||||
"encryption": "Šifrēšana",
|
||||
"sidebar_apps": "Sānu joslas lietotnes",
|
||||
"notifications": "Paziņojumi"
|
||||
"notifications": "Paziņojumi",
|
||||
"layout": "Izkārtojums",
|
||||
"reading": "Lasīšana",
|
||||
"composing": "Rakstīšana",
|
||||
"content_senders": "Saturs un sūtītāji",
|
||||
"about_data": "Par un dati",
|
||||
"debug": "Atkļūdošana"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Vispārīgi",
|
||||
"account": "Konts un identitāte",
|
||||
"organization": "Pasta organizēšana",
|
||||
"apps": "Lietotnes",
|
||||
"system": "Sistēma"
|
||||
"system": "Sistēma",
|
||||
"appearance": "Izskats",
|
||||
"mail": "Pasts",
|
||||
"privacy": "Privātums un drošība",
|
||||
"advanced": "Papildu"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Izskats",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "Bestanden",
|
||||
"contacts": "Contacten",
|
||||
"sidebar_apps": "Zijbalk-apps",
|
||||
"notifications": "Meldingen"
|
||||
"notifications": "Meldingen",
|
||||
"layout": "Indeling",
|
||||
"reading": "Lezen",
|
||||
"composing": "Opstellen",
|
||||
"content_senders": "Inhoud en afzenders",
|
||||
"about_data": "Over en gegevens",
|
||||
"debug": "Debuggen"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Algemeen",
|
||||
"account": "Account & identiteit",
|
||||
"organization": "E-mailorganisatie",
|
||||
"apps": "Apps",
|
||||
"system": "Systeem"
|
||||
"system": "Systeem",
|
||||
"appearance": "Weergave",
|
||||
"mail": "E-mail",
|
||||
"privacy": "Privacy en beveiliging",
|
||||
"advanced": "Geavanceerd"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Uiterlijk",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"contacts": "Kontakty",
|
||||
"encryption": "Szyfrowanie",
|
||||
"sidebar_apps": "Aplikacje paska bocznego",
|
||||
"notifications": "Powiadomienia"
|
||||
"notifications": "Powiadomienia",
|
||||
"layout": "Układ",
|
||||
"reading": "Czytanie",
|
||||
"composing": "Tworzenie",
|
||||
"content_senders": "Treść i nadawcy",
|
||||
"about_data": "O programie i dane",
|
||||
"debug": "Debugowanie"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Ogólne",
|
||||
"account": "Konto i tożsamość",
|
||||
"organization": "Organizacja poczty",
|
||||
"apps": "Aplikacje",
|
||||
"system": "System"
|
||||
"system": "System",
|
||||
"appearance": "Wygląd",
|
||||
"mail": "Poczta",
|
||||
"privacy": "Prywatność i bezpieczeństwo",
|
||||
"advanced": "Zaawansowane"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Wygląd",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"files": "Arquivos",
|
||||
"contacts": "Contactos",
|
||||
"sidebar_apps": "Apps da barra lateral",
|
||||
"notifications": "Notificações"
|
||||
"notifications": "Notificações",
|
||||
"layout": "Layout",
|
||||
"reading": "Leitura",
|
||||
"composing": "Composição",
|
||||
"content_senders": "Conteúdo e remetentes",
|
||||
"about_data": "Sobre e dados",
|
||||
"debug": "Depuração"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Geral",
|
||||
"account": "Conta e identidade",
|
||||
"organization": "Organização de e-mail",
|
||||
"apps": "Aplicativos",
|
||||
"system": "Sistema"
|
||||
"system": "Sistema",
|
||||
"appearance": "Aparência",
|
||||
"mail": "Correio",
|
||||
"privacy": "Privacidade e segurança",
|
||||
"advanced": "Avançado"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Aparência",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"contacts": "Контакты",
|
||||
"encryption": "Шифрование",
|
||||
"sidebar_apps": "Приложения боковой панели",
|
||||
"notifications": "Уведомления"
|
||||
"notifications": "Уведомления",
|
||||
"layout": "Макет",
|
||||
"reading": "Чтение",
|
||||
"composing": "Написание",
|
||||
"content_senders": "Содержимое и отправители",
|
||||
"about_data": "О программе и данные",
|
||||
"debug": "Отладка"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Общие",
|
||||
"account": "Аккаунт и удостоверение",
|
||||
"organization": "Организация почты",
|
||||
"apps": "Приложения",
|
||||
"system": "Система"
|
||||
"system": "Система",
|
||||
"appearance": "Внешний вид",
|
||||
"mail": "Почта",
|
||||
"privacy": "Конфиденциальность и безопасность",
|
||||
"advanced": "Дополнительно"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Внешний вид",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"contacts": "Контакти",
|
||||
"encryption": "Шифрування",
|
||||
"sidebar_apps": "Програми бічної панелі",
|
||||
"notifications": "Сповіщення"
|
||||
"notifications": "Сповіщення",
|
||||
"layout": "Макет",
|
||||
"reading": "Читання",
|
||||
"composing": "Написання",
|
||||
"content_senders": "Вміст і відправники",
|
||||
"about_data": "Про програму та дані",
|
||||
"debug": "Налагодження"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "Загальний",
|
||||
"account": "Обліковий запис і ідентифікатор",
|
||||
"organization": "Організація пошти",
|
||||
"apps": "програми",
|
||||
"system": "система"
|
||||
"system": "система",
|
||||
"appearance": "Вигляд",
|
||||
"mail": "Пошта",
|
||||
"privacy": "Конфіденційність і безпека",
|
||||
"advanced": "Додатково"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Зовнішній вигляд",
|
||||
|
||||
+12
-2
@@ -671,14 +671,24 @@
|
||||
"contacts": "联系人",
|
||||
"encryption": "加密",
|
||||
"sidebar_apps": "侧边栏应用",
|
||||
"notifications": "通知"
|
||||
"notifications": "通知",
|
||||
"layout": "布局",
|
||||
"reading": "阅读",
|
||||
"composing": "撰写",
|
||||
"content_senders": "内容和发件人",
|
||||
"about_data": "关于和数据",
|
||||
"debug": "调试"
|
||||
},
|
||||
"tab_groups": {
|
||||
"general": "通用",
|
||||
"account": "账户与身份",
|
||||
"organization": "邮件组织",
|
||||
"apps": "应用",
|
||||
"system": "系统"
|
||||
"system": "系统",
|
||||
"appearance": "外观",
|
||||
"mail": "邮件",
|
||||
"privacy": "隐私和安全",
|
||||
"advanced": "高级"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "外观",
|
||||
|
||||
Reference in New Issue
Block a user