diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx index 1800bf46..a134d50d 100644 --- a/app/[locale]/settings/page.tsx +++ b/app/[locale]/settings/page.tsx @@ -3,7 +3,26 @@ import { useState, useEffect } from 'react'; import { useRouter } from '@/i18n/navigation'; import { useTranslations } from 'next-intl'; -import { ArrowLeft, ChevronRight, LogOut, Settings as SettingsIcon } from 'lucide-react'; +import { + ArrowLeft, + ChevronRight, + LogOut, + Settings as SettingsIcon, + Palette, + Mail, + User, + Shield, + UserPen, + PalmtreeIcon, + Calendar, + Filter, + FileText, + FolderOpen, + Tags, + HardDrive, + Wrench, + 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'; @@ -27,6 +46,32 @@ import { useConfig } from '@/hooks/use-config'; import { cn } from '@/lib/utils'; type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'vacation' | 'calendar' | 'filters' | 'templates' | 'folders' | 'keywords' | 'files' | 'advanced'; +type TabGroup = 'general' | 'account' | 'organization' | 'apps' | 'system'; + +interface TabDef { + id: Tab; + label: string; + icon: LucideIcon; + group: TabGroup; +} + +const tabIcons: Record = { + appearance: Palette, + email: Mail, + account: User, + security: Shield, + identities: UserPen, + vacation: PalmtreeIcon, + calendar: Calendar, + filters: Filter, + templates: FileText, + folders: FolderOpen, + keywords: Tags, + files: HardDrive, + advanced: Wrench, +}; + +const tabGroupOrder: TabGroup[] = ['general', 'account', 'organization', 'apps', 'system']; export default function SettingsPage() { const router = useRouter(); @@ -69,22 +114,31 @@ export default function SettingsPage() { const supportsSieve = client?.supportsSieve() ?? false; const supportsFiles = client?.supportsFiles() ?? false; - const tabs: { id: Tab; label: string }[] = [ - { id: 'appearance', label: t('tabs.appearance') }, - { id: 'email', label: t('tabs.email') }, - { id: 'account', label: t('tabs.account') }, - ...(stalwartFeaturesEnabled ? [{ id: 'security' as Tab, label: t('tabs.security') }] : []), - { id: 'identities', label: t('tabs.identities') }, - ...(supportsVacation ? [{ id: 'vacation' as Tab, label: t('tabs.vacation') }] : []), - ...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar') }] : []), - ...(supportsSieve ? [{ id: 'filters' as Tab, label: t('tabs.filters') }] : []), - { id: 'templates', label: t('tabs.templates') }, - { id: 'folders', label: t('tabs.folders') }, - { id: 'keywords', label: t('tabs.keywords') }, - ...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files') }] : []), - { id: 'advanced', label: t('tabs.advanced') }, + 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' }, + { 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' }, + ...(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 }] : []), + { id: 'templates', label: t('tabs.templates'), icon: tabIcons.templates, group: 'organization' }, + { id: 'folders', label: t('tabs.folders'), icon: tabIcons.folders, group: 'organization' }, + { id: 'keywords', label: t('tabs.keywords'), icon: tabIcons.keywords, group: 'organization' }, + ...(supportsCalendar ? [{ id: 'calendar' as Tab, label: t('tabs.calendar'), icon: tabIcons.calendar, group: 'apps' as TabGroup }] : []), + ...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []), + { id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' }, ]; + // Group tabs by category + const groupedTabs = tabGroupOrder + .map((group) => ({ + group, + label: t(`tab_groups.${group}`), + items: tabs.filter((tab) => tab.group === group), + })) + .filter((g) => g.items.length > 0); + const handleTabSelect = (tabId: Tab) => { setActiveTab(tabId); try { localStorage.setItem('settings-active-tab', tabId); } catch { /* ignore */ } @@ -167,15 +221,31 @@ export default function SettingsPage() { {/* Tab list */}
- {tabs.map((tab) => ( - + {groupedTabs.map((group, groupIndex) => ( +
+ {groupIndex > 0 &&
} +
+ + {group.label} + +
+ {group.items.map((tab) => { + const Icon = tab.icon; + return ( + + ); + })} +
))}
@@ -227,20 +297,37 @@ export default function SettingsPage() { {/* Tabs */}
-
- {tabs.map((tab) => ( - +
+ {groupedTabs.map((group, groupIndex) => ( +
+ {groupIndex > 0 &&
} +
+ + {group.label} + +
+ {group.items.map((tab) => { + const Icon = tab.icon; + return ( + + ); + })} +
))}
diff --git a/components/settings/files-settings.tsx b/components/settings/files-settings.tsx index 1a73af2d..fce66480 100644 --- a/components/settings/files-settings.tsx +++ b/components/settings/files-settings.tsx @@ -1,9 +1,173 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { useTranslations } from "next-intl"; +import { Folder, FolderOpen, FileText, FileCode, ImageIcon, FileAudio, File, Home, ChevronRight, ChevronDown } from "lucide-react"; import { SettingsSection, SettingItem, ToggleSwitch, RadioGroup } from "./settings-section"; import { loadFilesSettings, saveFilesSettings, type FilesSettings, type FolderLayout } from "@/components/files/files-settings-dialog"; +import { cn } from "@/lib/utils"; + +interface SampleFile { + name: string; + isFolder: boolean; + size: number; + modified: string; + hidden?: boolean; + thumbnailUrl?: string; +} + +const SAMPLE_FILES: SampleFile[] = [ + { name: "Documents", isFolder: true, size: 0, modified: "2026-03-10" }, + { name: "Photos", isFolder: true, size: 0, modified: "2026-03-14" }, + { name: "report.pdf", isFolder: false, size: 245000, modified: "2026-03-15" }, + { name: "notes.md", isFolder: false, size: 1200, modified: "2026-03-12" }, + { name: "vacation.jpg", isFolder: false, size: 3400000, modified: "2026-03-08", thumbnailUrl: "/branding/Bulwark_Logo_Color.png" }, + { name: "song.mp3", isFolder: false, size: 5200000, modified: "2026-03-01" }, + { name: ".config", isFolder: false, size: 340, modified: "2026-02-20", hidden: true }, +]; + +function getPreviewIcon(file: SampleFile, colored: boolean, size: "sm" | "lg") { + const cls = size === "sm" ? "w-4 h-4 flex-shrink-0" : "w-8 h-8 flex-shrink-0"; + + if (file.isFolder) { + return ; + } + + const ext = file.name.split(".").pop()?.toLowerCase(); + switch (ext) { + case "jpg": case "png": case "gif": + return ; + case "mp3": case "wav": + return ; + case "pdf": + return ; + case "md": case "json": case "js": case "ts": + return ; + default: + return ; + } +} + +function formatSize(bytes: number): string { + if (bytes === 0) return "—"; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function FilesSettingsPreview({ settings }: { settings: FilesSettings }) { + const sortedFiles = useMemo(() => { + let files = SAMPLE_FILES.filter((f) => { + if (!settings.showHiddenFiles && f.hidden) return false; + if (settings.folderLayout === "sidebar" && f.isFolder) return false; + return true; + }); + + files.sort((a, b) => { + // Folders first + if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1; + + let cmp = 0; + switch (settings.defaultSortKey) { + case "name": cmp = a.name.localeCompare(b.name); break; + case "size": cmp = a.size - b.size; break; + case "modified": cmp = a.modified.localeCompare(b.modified); break; + } + return settings.defaultSortDir === "desc" ? -cmp : cmp; + }); + + return files; + }, [settings.showHiddenFiles, settings.folderLayout, settings.defaultSortKey, settings.defaultSortDir]); + + const listView = ( +
+
+ Name + Size + Modified +
+ {sortedFiles.map((file) => ( +
+ {settings.showThumbnails && file.thumbnailUrl ? ( + + ) : settings.showIcons ? ( + getPreviewIcon(file, settings.coloredIcons, "sm") + ) : null} + + {file.name} + + + {formatSize(file.size)} + + + {file.modified.slice(5)} + +
+ ))} +
+ ); + + const gridView = ( +
+
+ {sortedFiles.map((file) => ( +
+ {settings.showThumbnails && file.thumbnailUrl ? ( + + ) : settings.showIcons ? ( + getPreviewIcon(file, settings.coloredIcons, "lg") + ) : ( +
+ )} + + {file.name} + +
+ ))} +
+
+ ); + + const sidebar = settings.folderLayout === "sidebar" && ( +
+
+ + Files +
+
+ + + Documents +
+
+ + + Photos +
+
+ ); + + return ( +
+
+ {sidebar} + {settings.defaultViewMode === "grid" ? gridView : listView} +
+
+ ); +} export function FilesSettingsComponent() { const t = useTranslations("settings.files"); @@ -29,8 +193,14 @@ export function FilesSettingsComponent() { }, []); return ( -
- +
+
+

{t("preview.label")}

+ +
+ +
+ +
); } diff --git a/locales/de/common.json b/locales/de/common.json index 85de0385..ceea2694 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "Allgemein", - "account": "Konto", - "organization": "Organisation" + "account": "Konto & Identität", + "organization": "E-Mail-Organisation", + "apps": "Apps", + "system": "System" }, "appearance": { "title": "Darstellung", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "Versteckte Dateien anzeigen", "description": "Dateien und Ordner anzeigen, die mit einem Punkt beginnen" + }, + "preview": { + "label": "Vorschau" } } }, diff --git a/locales/en/common.json b/locales/en/common.json index f737125a..c9f9e686 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "General", - "account": "Account", - "organization": "Organization" + "account": "Account & Identity", + "organization": "Mail Organization", + "apps": "Apps", + "system": "System" }, "appearance": { "title": "Appearance", @@ -1118,6 +1120,9 @@ "show_hidden": { "label": "Show Hidden Files", "description": "Display files and folders that start with a dot" + }, + "preview": { + "label": "Preview" } } }, diff --git a/locales/es/common.json b/locales/es/common.json index 5299c658..1dae539e 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "General", - "account": "Cuenta", - "organization": "Organización" + "account": "Cuenta e identidad", + "organization": "Organización del correo", + "apps": "Aplicaciones", + "system": "Sistema" }, "appearance": { "title": "Apariencia", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "Mostrar archivos ocultos", "description": "Mostrar archivos y carpetas que comienzan con un punto" + }, + "preview": { + "label": "Vista previa" } } }, diff --git a/locales/fr/common.json b/locales/fr/common.json index 59671b52..b20d68c5 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "Général", - "account": "Compte", - "organization": "Organisation" + "account": "Compte & Identité", + "organization": "Organisation des e-mails", + "apps": "Applications", + "system": "Système" }, "appearance": { "title": "Apparence", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "Afficher les fichiers cachés", "description": "Afficher les fichiers et dossiers commençant par un point" + }, + "preview": { + "label": "Aperçu" } } }, diff --git a/locales/it/common.json b/locales/it/common.json index e40b403b..398ba163 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "Generale", - "account": "Account", - "organization": "Organizzazione" + "account": "Account e identità", + "organization": "Organizzazione e-mail", + "apps": "Applicazioni", + "system": "Sistema" }, "appearance": { "title": "Aspetto", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "Mostra file nascosti", "description": "Visualizza file e cartelle che iniziano con un punto" + }, + "preview": { + "label": "Anteprima" } } }, diff --git a/locales/ja/common.json b/locales/ja/common.json index bf4744d2..25437f06 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "一般", - "account": "アカウント", - "organization": "整理" + "account": "アカウントと身元", + "organization": "メール整理", + "apps": "アプリ", + "system": "システム" }, "appearance": { "title": "外観", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "隠しファイルを表示", "description": "ドットで始まるファイルとフォルダーを表示します" + }, + "preview": { + "label": "プレビュー" } } }, diff --git a/locales/nl/common.json b/locales/nl/common.json index 8d6095cb..9eb29f60 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "Algemeen", - "account": "Account", - "organization": "Organisatie" + "account": "Account & identiteit", + "organization": "E-mailorganisatie", + "apps": "Apps", + "system": "Systeem" }, "appearance": { "title": "Uiterlijk", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "Verborgen bestanden tonen", "description": "Bestanden en mappen weergeven die beginnen met een punt" + }, + "preview": { + "label": "Voorbeeld" } } }, diff --git a/locales/pt/common.json b/locales/pt/common.json index 9a3e8076..c46f319e 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -504,8 +504,10 @@ }, "tab_groups": { "general": "Geral", - "account": "Conta", - "organization": "Organização" + "account": "Conta e identidade", + "organization": "Organização de e-mail", + "apps": "Aplicativos", + "system": "Sistema" }, "appearance": { "title": "Aparência", @@ -1112,6 +1114,9 @@ "show_hidden": { "label": "Mostrar arquivos ocultos", "description": "Exibir arquivos e pastas que começam com um ponto" + }, + "preview": { + "label": "Visualização" } } },