Files
mail/ui/dialogs/settings-dialog.slint
T

3816 lines
254 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AccountMailPreferencesPanel } from "../components/account-mail-preferences.slint";
import { RendererSettings } from "../renderer-settings.slint";
import { WindowChrome } from "../window-chrome.slint";
import { AppIcon } from "../app-icon.slint";
import { DefaultEmailApp } from "../default-email-app.slint";
import { SystemNotifications } from "../system-notifications.slint";
import { Updates } from "../updates.slint";
import { AppIconPicker } from "../components/app-icon-picker.slint";
import { MessageColors } from "../message-colors.slint";
import { MailPorts, MailSecurityFields } from "../components/mail-security-fields.slint";
import { OAuthBrowserHelp } from "../components/oauth-browser-help.slint";
import { HorizontalBox, ScrollView, VerticalBox } from "std-widgets.slint";
import { AccountRow, CalendarConnectionRow, MailProfileRow, ProfileMemberRow, StorageUsageRow } from "../models.slint";
import { AccountCalendars } from "../components/account-calendars.slint";
import { Avatar } from "../components/avatar.slint";
import { Button } from "../components/button.slint";
import { ConfirmationDialog } from "../components/confirmation-dialog.slint";
import { IconButton, LineEdit, RadioButton, Switch } from "../components/controls.slint";
import { CopyableDiagnostic } from "../components/copyable-diagnostic.slint";
import { FocusVisibleScope } from "../components/focus-visible-scope.slint";
import { OnScreenSurface } from "../components/on-screen-surface.slint";
import { ProviderButton } from "../components/provider-button.slint";
import { FormField, SettingsCard, SettingsLinkRow, SettingsNavItem, SettingsSelectRow, SettingsSwitchRow, ShortcutRow, ShortcutSection } from "../components/settings-components.slint";
import { Shortcuts } from "../shortcuts.slint";
import { KeyboardKeys } from "../components/kbd.slint";
import { TabsUi } from "../components/workspace-navigation.slint";
import { AccountProfilePicker, ProfileColorChoice, ProfileMemberChoice, ProfilePalette } from "../components/profile-controls.slint";
import { RuntimeColorPicker, ThemeEditor } from "../components/theme-editor.slint";
import { TypeScale } from "../typography.slint";
import { AppTheme, LayoutMetrics, SettingsIconPalette, ServiceIconPalette } from "../design-system.slint";
import { CompactPageHeader, WorkspaceSidebarSurface } from "../components/workspace-layout.slint";
import { TabSwitcherButton } from "../components/tab-overview.slint";
import { LocalMailUi } from "../local-mail.slint";
import { Numbers } from "../numbers.slint";
import { BlockSender } from "../block-sender.slint";
// Use the same focusable controls in both account-card layouts.
component AccountActions inherits HorizontalLayout {
in property <string> edit-label;
in property <bool> reconnect: false;
in property <bool> edit-enabled;
in property <bool> remove-enabled;
in property <bool> sync-enabled;
callback edit();
callback remove();
callback sync();
width: 108px;
height: 32px;
spacing: 6px;
IconButton {
icon: root.reconnect ? @image-url("../icons/lucide/outline/sync.svg") : @image-url("../icons/lucide/outline/pencil.svg");
icon-color: AppTheme.secondary;
ghost: true;
hover-background: AppTheme.muted-control-bg;
interactive: root.edit-enabled;
tooltip-text: root.edit-label;
clicked => { root.edit(); }
}
IconButton {
icon: @image-url("../icons/lucide/outline/trash.svg");
icon-color: AppTheme.destructive;
ghost: true;
hover-background: AppTheme.destructive.with-alpha(0.12);
interactive: root.remove-enabled;
tooltip-text: @tr("Remove");
clicked => { root.remove(); }
}
IconButton {
icon: @image-url("../icons/lucide/outline/sync.svg");
icon-color: AppTheme.secondary;
ghost: true;
hover-background: AppTheme.muted-control-bg;
interactive: root.sync-enabled;
tooltip-text: @tr("Sync");
clicked => { root.sync(); }
}
}
component SettingsPageTitle inherits Text {
in property <string> label;
in property <color> title-color;
height: max(28px, self.preferred-height);
text: root.label;
color: root.title-color;
font-size: TypeScale.display;
font-weight: 600;
letter-spacing: -0.6px;
vertical-alignment: center;
}
component SettingsSearch inherits Rectangle {
in-out property <string> query;
in property <color> muted-color;
in property <color> hover-background;
callback edited(string);
height: 32px;
LineEdit {
width: parent.width;
height: parent.height;
text <=> root.query;
placeholder-text: @tr("Search settings");
input-label: @tr("Search settings");
show-leading-icon: true;
leading-icon: @image-url("../icons/lucide/outline/search.svg");
leading-icon-color: root.muted-color;
trailing-content-width: root.query == "" ? 0px : 32px;
edited(value) => { root.edited(value); }
}
IconButton {
visible: root.query != "";
x: parent.width - 32px;
width: 32px;
height: 32px;
icon: @image-url("../icons/lucide/outline/x.svg");
icon-size: 14px;
icon-color: root.muted-color;
hover-background: root.hover-background;
tooltip-text: @tr("Clear settings search");
clicked => { root.query = ""; root.edited(""); }
}
}
component WorkspaceLayoutChoice inherits FocusVisibleScope {
in property <string> name;
in property <bool> minimal: false;
in property <bool> selected: false;
callback chosen;
height: 55px + layout-name.height;
min-width: 120px;
focus-on-click: true;
accessible-role: radio-button;
accessible-label: root.name;
accessible-checked: root.selected;
accessible-action-default => { root.chosen(); }
key-pressed(event) => {
if event.text == Key.Return || event.text == Key.Space { root.chosen(); return accept; }
reject
}
Rectangle {
border-radius: 8px;
border-width: root.selected || root.focus-visible ? 2px : 1px;
border-color: root.selected || root.focus-visible ? AppTheme.accent : AppTheme.border;
background: layout-hit.has-hover ? AppTheme.control-bg : AppTheme.surface-raised;
Rectangle {
x: 10px; y: 9px; width: parent.width - 20px; height: 34px;
border-radius: 4px; border-width: 1px; border-color: AppTheme.border;
background: AppTheme.page-bg; clip: true;
Rectangle { x: 0px; width: root.minimal ? 11px : 18px; height: parent.height; background: AppTheme.sidebar-tint; }
Rectangle { x: root.minimal ? 14px : 21px; y: 4px; width: root.minimal ? parent.width - 18px : 25px; height: parent.height - 8px; border-radius: 2px; background: AppTheme.surface; }
if !root.minimal : Rectangle { x: 49px; y: 4px; width: parent.width - 53px; height: parent.height - 8px; border-radius: 2px; background: AppTheme.surface; }
}
layout-name := Text {
x: 8px; y: 47px; width: parent.width - 16px; height: max(17px, self.preferred-height);
text: root.name; color: root.selected ? AppTheme.accent : AppTheme.body;
font-size: TypeScale.caption; font-weight: root.selected ? 700 : 600;
horizontal-alignment: center; vertical-alignment: center; overflow: elide;
}
layout-hit := TouchArea { clicked => { root.pointer-activated(); root.chosen(); } mouse-cursor: pointer; }
}
}
component StorageUsageCard inherits Rectangle {
in property <bool> compact: false;
in property <bool> loading: false;
in property <bool> loaded: false;
in property <string> total;
in property <string> error;
in property <[StorageUsageRow]> rows: [];
in property <color> surface;
in property <color> card-border-color;
in property <color> heading;
in property <color> body-text;
in property <color> secondary-text;
callback refresh();
height: root.loaded
? usage-rows.y + usage-rows.height + (root.error != "" ? storage-error.height + 13px : 9px)
: storage-status.y + storage-status.height + 81px;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.card-border-color;
clip: true;
accessible-role: groupbox;
accessible-label: @tr("Local storage usage");
storage-title := Text {
x: 20px;
y: 16px;
width: parent.width - 80px;
height: max(24px, self.preferred-height);
text: @tr("Local storage");
color: root.heading;
font-size: TypeScale.subtitle;
font-weight: 600;
letter-spacing: -0.3px;
vertical-alignment: center;
wrap: word-wrap;
}
IconButton {
x: parent.width - self.width - 16px;
y: 12px;
width: 36px;
height: 36px;
icon: @image-url("../icons/lucide/outline/sync.svg");
icon-color: root.secondary-text;
ghost: true;
hover-background: root.card-border-color;
interactive: !root.loading;
tooltip-text: root.loading ? @tr("Calculating…") : @tr("Refresh");
clicked => { root.refresh(); }
}
storage-status := Text {
x: 20px;
y: storage-title.y + storage-title.height + 7px;
width: parent.width - 40px;
height: max(22px, self.preferred-height);
text: root.loading && !root.loaded ? @tr("Calculating local storage…")
: root.loaded ? @tr("{} used by Flectar Mail", root.total)
: root.error == "failed" ? @tr("Storage usage could not be calculated. Try again.")
: @tr("Open Storage to calculate local usage.");
color: root.secondary-text;
font-size: TypeScale.secondary;
vertical-alignment: center;
overflow: elide;
}
usage-bar := Rectangle {
visible: root.loaded;
x: 20px;
y: storage-status.y + storage-status.height + 11px;
width: parent.width - 40px;
height: 18px;
background: root.card-border-color;
border-radius: 9px;
clip: true;
for usage in root.rows : segment := Rectangle {
private property <length> segment-left: parent.width * usage.offset;
private property <length> segment-right: min(parent.width, parent.width * (usage.offset + usage.fraction));
visible: usage.fraction > 0 && self.segment-right > self.segment-left;
x: self.segment-left;
width: max(0px, self.segment-right - self.segment-left);
height: parent.height;
background: usage.color;
}
}
// The last row reaches into the bottom padding.
usage-rows := VerticalBox {
visible: root.loaded;
x: 20px;
y: usage-bar.y + usage-bar.height + 10px;
width: parent.width - 40px;
height: self.preferred-height;
padding-bottom: 0px;
spacing: 0px;
for usage in root.rows : Rectangle {
height: usage-description.y + usage-description.height + 2px;
Rectangle {
x: 0px;
y: 8px;
width: 10px;
height: 10px;
border-radius: 5px;
background: usage.color;
}
usage-name := Text {
x: 20px;
y: 1px;
width: parent.width - 120px;
height: max(18px, self.preferred-height);
text: usage.kind == "mail" ? @tr("Mail")
: usage.kind == "attachments" ? @tr("Attachments")
: usage.kind == "files" ? @tr("Files")
: usage.kind == "databases" ? @tr("Databases")
: @tr("Other");
color: root.body-text;
font-size: TypeScale.body;
font-weight: 600;
overflow: elide;
}
usage-description := Text {
x: 20px;
y: usage-name.y + usage-name.height - 1px;
width: parent.width - 120px;
height: max(15px, self.preferred-height);
text: usage.kind == "mail" ? @tr("Downloaded messages")
: usage.kind == "attachments" ? @tr("Downloaded and draft attachments")
: usage.kind == "files" ? @tr("Offline file copies")
: usage.kind == "databases" ? @tr("Mail, calendar, contacts, and indexes")
: @tr("Settings and temporary data");
color: root.secondary-text;
font-size: TypeScale.caption;
overflow: elide;
}
Text {
x: parent.width - 96px;
width: 96px;
height: parent.height - 1px;
text: usage.size;
color: root.secondary-text;
font-size: TypeScale.secondary;
horizontal-alignment: right;
vertical-alignment: center;
}
}
}
storage-error := Text {
visible: root.error != "" && root.loaded;
x: 20px;
y: usage-rows.y + usage-rows.height + 2px;
width: parent.width - 40px;
height: max(16px, self.preferred-height);
text: root.error == "incomplete"
? @tr("Some files could not be measured, so this total may be incomplete.")
: @tr("Storage usage could not be refreshed. Try again.");
color: root.secondary-text;
font-size: TypeScale.caption;
overflow: elide;
}
}
component ServiceBadge inherits Rectangle {
in property <string> text;
in property <color> foreground;
in property <color> fill;
width: badge-label.preferred-width + 16px;
height: max(20px, badge-label.preferred-height);
border-radius: self.height / 2;
background: root.fill;
badge-label := Text {
x: 8px;
height: parent.height;
text: root.text;
color: root.foreground;
font-size: TypeScale.caption;
font-weight: 600;
vertical-alignment: center;
}
}
// Paid Flectar Base services are previewed before they can be purchased, so
// each card describes what is planned without implying anything is connected.
component ServicePreviewCard inherits Rectangle {
in property <bool> compact: false;
in property <image> icon;
in property <string> title;
in property <string> tagline;
in property <string> description;
in property <[string]> features: [];
in property <string> note;
in property <color> surface;
in property <color> card-border-color;
in property <color> heading;
in property <color> body-text;
in property <color> secondary-text;
in property <color> faint-text;
in property <color> accent;
in property <color> accent-soft;
callback learn-more();
background: root.surface;
border-radius: 10px;
border-width: 1px;
border-color: root.card-border-color;
VerticalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 10px;
HorizontalBox {
padding: 0px;
spacing: 12px;
height: max(38px, self.preferred-height);
Rectangle {
width: 38px;
height: 38px;
border-radius: 9px;
background: root.accent-soft;
Image { width: 24px; height: 24px; source: root.icon; colorize: root.accent; image-fit: contain; accessible-role: none; }
}
VerticalBox {
padding: 0px;
spacing: 1px;
horizontal-stretch: 1;
Text { height: max(20px, self.preferred-height); text: root.title; color: root.heading; font-size: TypeScale.subtitle; font-weight: 600; letter-spacing: -0.3px; vertical-alignment: center; overflow: elide; }
Text { height: max(17px, self.preferred-height); text: root.tagline; color: root.faint-text; font-size: TypeScale.caption; vertical-alignment: center; overflow: elide; }
}
if !root.compact : HorizontalBox {
padding: 0px;
spacing: 6px;
alignment: end;
cross-axis-alignment: center;
ServiceBadge { text: @tr("Paid"); foreground: root.secondary-text; fill: root.card-border-color; }
ServiceBadge { text: @tr("Coming soon"); foreground: root.accent; fill: root.accent-soft; }
}
}
if root.compact : HorizontalBox {
padding: 0px;
spacing: 6px;
alignment: start;
ServiceBadge { text: @tr("Paid"); foreground: root.secondary-text; fill: root.card-border-color; }
ServiceBadge { text: @tr("Coming soon"); foreground: root.accent; fill: root.accent-soft; }
}
Text { height: max(root.compact ? 51px : 34px, self.preferred-height); text: root.description; color: root.body-text; font-size: TypeScale.secondary; wrap: word-wrap; }
VerticalBox {
padding: 0px;
spacing: 2px;
for feature in root.features : HorizontalBox {
padding: 0px;
spacing: 9px;
height: max(root.compact ? 34px : 22px, self.preferred-height);
Image { y: 3px; width: 16px; height: 16px; source: @image-url("../icons/lucide/outline/check-circle.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
Text { horizontal-stretch: 1; text: feature; color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; vertical-alignment: root.compact ? top : center; }
}
}
Rectangle { height: 1px; background: root.card-border-color; }
if root.compact : Text { height: max(34px, self.preferred-height); text: root.note; color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
HorizontalBox {
padding: 0px;
spacing: 16px;
height: 32px;
if !root.compact : Text { horizontal-stretch: 1; text: root.note; color: root.faint-text; font-size: TypeScale.caption; vertical-alignment: center; overflow: elide; }
Button {
width: root.compact ? parent.width : 140px;
text: @tr("Learn more");
icon: @image-url("../icons/lucide/outline/link.svg");
has-icon: true;
clicked => { root.learn-more(); }
}
}
}
}
export component SettingsDialog inherits Rectangle {
in property <bool> compact: false;
in property <[AccountRow]> connected_accounts: [];
in property <[MailProfileRow]> mail_profiles: [];
// The profile editor's account checklist, from `prepare_profile_members`.
in property <[ProfileMemberRow]> profile_members: [];
// Set to open the profile editor for a new profile.
in-out property <bool> new-profile-requested: false;
in property <bool> custom_oauth_configured: false;
in property <bool> google_oauth_available: false;
in property <bool> microsoft_oauth_available: false;
in property <bool> google_oauth_bundled: false;
in property <bool> microsoft_oauth_bundled: false;
in property <bool> remote-images-available: true;
in property <bool> account-transfer-supported: true;
in property <bool> sync_in_progress: false;
in property <bool> account_setup_in_progress: false;
in property <bool> oauth_in_progress: false;
in property <bool> oauth_settings_saving: false;
in property <string> sync_status: "";
in property <string> app-version: "";
in property <string> privacy-policy-url;
in property <bool> storage_loading: false;
in property <bool> storage_loaded: false;
in property <string> storage_total: "";
in property <string> storage_error: "";
in property <[StorageUsageRow]> storage_usage: [];
in property <color> accent;
in property <color> accent-soft;
in property <color> body-text;
in property <color> border-subtle;
in property <color> faint-text;
in property <color> heading;
in property <color> muted-text;
in property <color> page-bg;
in property <color> secondary-text;
in property <color> sidebar-bg;
in property <color> surface;
in property <color> surface-raised;
in property <color> title-color;
in-out property <bool> settings_open: false;
in-out property <string> settings_tab: "General";
in-out property <string> theme_mode: "system";
in-out property <bool> show_avatars: true;
in-out property <bool> show_tabs: true;
in property <bool> spelling_available: false;
in-out property <bool> spelling_enabled: true;
in property <bool> spelling_installed: false;
in property <string> spelling_system: "";
in-out property <bool> group_mail_by_date: true;
in-out property <bool> conversation_view: true;
in-out property <bool> show_account_markers: false;
in-out property <string> workspace_layout: "default";
in-out property <string> language_mode: "system";
in-out property <string> time_format: "automatic";
in property <string> time_format_sample: "13:30";
in-out property <bool> monochrome_sidebar_icons: false;
in-out property <bool> notifications_enabled: true;
in-out property <bool> notification_sound_enabled: true;
in-out property <bool> unread_badge_enabled: true;
in-out property <string> notification_scope: "important";
in-out property <int> sync_interval_minutes: 5;
// How long a sent message waits before it leaves, so that it can be
// taken back; 0 sends it at once.
in-out property <int> undo_send_seconds: 10;
in-out property <bool> mark_read_on_open: true;
in-out property <bool> collect_outgoing_contacts: true;
in-out property <bool> collect_incoming_contacts: false;
in-out property <bool> contact_suggest_all_accounts: false;
in-out property <bool> suggest_learned_contacts: true;
in-out property <bool> remote-images-enabled: false;
in-out property <bool> close_to_tray: false;
in-out property <bool> launch_at_login: false;
in property <bool> launch_at_login_available: false;
// False where closing to the background shows no tray icon (the Flatpak):
// opening the app again brings its window back.
in property <bool> tray_supported: true;
in-out property <string> account_email: "";
in-out property <string> account_username: "";
in-out property <string> account_password: "";
in-out property <string> account_protocol: "imap";
in-out property <string> jmap_url: "";
in-out property <bool> connect_calendar_on_add: true;
// The CalDAV form adds a connection to this account, or changes the
// connection with this id when it is positive.
in-out property <int> caldav_account_id: -1;
in-out property <int> caldav_connection_id: -1;
in-out property <string> caldav_url: "";
in-out property <string> caldav_username: "";
in-out property <string> caldav_password: "";
in property <bool> caldav_connecting: false;
private property <string> caldav-account-email: "";
in-out property <int> carddav_account_id: -1;
in-out property <string> carddav_url: "";
in-out property <string> carddav_username: "";
in-out property <string> carddav_password: "";
in-out property <bool> carddav_manage_existing: false;
in-out property <string> incoming_security: "tls";
in-out property <string> smtp_security: "tls";
in-out property <string> trusted_certificate_pem;
in property <string> oauth_authorization_url;
in property <bool> oauth_browser_error;
callback retry_oauth_browser();
callback copy_oauth_link();
callback submit_oauth_redirect(string);
callback import_mail_certificate();
in-out property <string> incoming_host: "";
in-out property <string> incoming_port: "993";
in-out property <string> smtp_host: "";
in-out property <string> smtp_port: "465";
in-out property <string> google_client_id: "";
in-out property <string> google_client_secret: "";
in-out property <string> ms_client_id: "";
// The download folder as shown: its host path inside Flatpak. Empty is the default.
in-out property <string> download_directory: "";
// Only desktop hosts pick a filesystem directory; mobile hosts export
// through their own document provider and hide the setting.
in-out property <bool> download_directory_available: false;
in-out property <bool> custom_oauth_open: false;
in-out property <bool> account_form_open: false;
private property <bool> account_editing: false;
private property <bool> show_account_status: false;
in-out property <bool> delete_data_confirmation: false;
in-out property <bool> delete_account_confirmation: false;
in-out property <int> delete_account_id: -1;
in-out property <string> delete_account_name: "";
// The account being removed keeps its mail only on this device (POP3).
private property <bool> delete_account_device_mail: false;
private property <bool> delete_local_mail_confirmation: false;
// Some account keeps its mail only on this device (POP3).
in property <bool> has_device_mail: false;
in property <bool> mail_export_running: false;
in property <string> mail_export_status: "";
private property <bool> compact-nav-open: false;
private property <bool> clear-contact-suggestions-confirming: false;
private property <string> settings-search-query: "";
private property <bool> profile-editor-open: false;
private property <string> profile-editor-id: "";
private property <string> profile-editor-name: "";
private property <color> profile-editor-color: #3b82f6;
private property <bool> profile-editor-custom-color: false;
private property <bool> delete-profile-confirmation: false;
private property <string> delete-profile-id: "";
private property <string> delete-profile-name: "";
private property <bool> remove-calendar-confirmation: false;
private property <int> remove-calendar-id: -1;
private property <string> remove-calendar-name: "";
private property <bool> account-profile-picker-open: false;
private property <int> account-profile-picker-id: -1;
// The picker shows the account's live row, which changes as choices apply.
private property <int> account-profile-picker-index: -1;
private property <AccountRow> account-profile-picker-account: root.connected_accounts[root.account-profile-picker-index];
private property <string> profile-editor-error: "";
callback save_theme(string);
callback save_theme_preset(string);
callback save_custom_theme(color, color, color, color, color, color, color, color, color, color) -> bool;
callback save_avatar_visibility(bool);
callback save_show_tabs(bool);
callback save_spelling_enabled(bool);
callback save_group_mail_by_date(bool);
callback save_conversation_view(bool);
callback choose_download_directory();
callback clear_download_directory();
// Save a profile's name and color, and make the checked accounts of
// `profile_members` its accounts. Returns the profile's id, or empty.
callback save_mail_profile(string, string, color) -> string;
// Fill `profile_members` for this profile, or a new one, with one
// account checked from the start (-1 for none).
callback prepare_profile_members(string, int);
callback assign_account_profile(int, string);
callback save_account_color(int, color, bool);
callback delete_mail_profile(string);
callback save_account_markers_visibility(bool);
callback save_workspace_layout(string);
callback save_language(string);
callback save_time_format(string);
callback save_sidebar_icon_style(bool);
callback save_notification_settings(bool, bool, string);
callback save_unread_badge(bool);
callback save_sync_interval(int);
callback save_undo_send(int);
callback save_mark_read_on_open(bool);
callback save_contact_discovery_settings(bool, bool, bool, bool);
callback clear_contact_suggestions() -> bool;
callback set_close_to_tray(bool);
callback set_launch_at_login(bool);
callback sync_mail();
callback sync_account(int);
callback delete_account(int);
callback add_password_account(string, string, string, string, string, string, string, string, string);
callback suggest_account_settings(string);
callback start_oauth(string);
callback reauth_account(int);
callback cancel_oauth();
callback save_oauth_app(string, string, string);
callback set-remote-images(bool);
pure callback make_account_transfer(string) -> data-transfer;
pure callback read_account_transfer(data-transfer) -> string;
callback reorder_account(string, int, bool);
callback save_account_mail_history(int, string);
callback save_account_sent_copy_policy(int, string);
callback save_account_keep_on_server(int, string);
callback connect_account_calendar(int);
// (account, connection or -1 for a new one, url, username, password)
callback connect_caldav(int, int, string, string, string);
callback set_calendar_connection_enabled(int, bool);
callback remove_calendar_connection(int);
callback connect_carddav(int, string, string, string);
callback set_account_carddav_enabled(int, bool);
callback disconnect_carddav(int);
callback export_account_backup();
callback import_accounts();
callback export_database_snapshot();
// Export the mail of one account, or of every POP3 account for -1.
callback export_device_mail(int);
callback delete_all_data();
callback load_storage_usage();
pure callback settings-search-matches(string, string) -> bool;
callback open-link(string);
// Flectar Base services are previewed in the Services page until the
// base.flectar.com integration ships; enabling this exposes sign-in.
in property <bool> flectar-services-available: false;
callback start_flectar_sign_in();
private property <[string]> sync-interval-options: [
@tr("Every minute"),
@tr("Every 5 minutes"),
@tr("Every 15 minutes"),
];
private property <[string]> undo-send-options: [
@tr("Off"),
@tr("5 seconds"),
@tr("10 seconds"),
@tr("20 seconds"),
@tr("30 seconds"),
];
private property <int> undo-send-index: root.undo_send_seconds <= 0 ? 0
: root.undo_send_seconds <= 5 ? 1
: root.undo_send_seconds <= 10 ? 2
: root.undo_send_seconds <= 20 ? 3 : 4;
private property <[string]> notification-options: [
@tr("Off"),
@tr("Important mail"),
@tr("All new mail"),
];
private property <[string]> mail-history-options: [
@tr("1 month"),
@tr("3 months"),
@tr("6 months · Recommended"),
@tr("1 year"),
@tr("All mail"),
];
private property <[string]> keep-on-server-options: [
@tr("Always"),
@tr("Until deleted here"),
@tr("7 days"),
@tr("14 days"),
@tr("30 days"),
@tr("Until downloaded"),
];
private property <[string]> language-options: [
@tr("System language"),
@tr("English"),
@tr("Spanish"),
@tr("Turkish"),
@tr("Simplified Chinese"),
@tr("Russian"),
];
private property <[string]> time-format-options: [
@tr("Automatic"),
@tr("12-hour"),
@tr("24-hour"),
];
private property <int> sync-interval-index: root.sync_interval_minutes <= 1 ? 0
: root.sync_interval_minutes <= 5 ? 1 : 2;
private property <int> notification-index: !root.notifications_enabled ? 0
: root.notification_scope == "all" ? 2 : 1;
// On here but kept from showing by the system: turned off in its settings,
// refused for this copy of the app (macOS, as for a copy in a temporary
// folder), or no notification service running.
private property <bool> notifications-blocked: root.notifications_enabled
&& (SystemNotifications.availability == "denied" || SystemNotifications.availability == "refused"
|| SystemNotifications.availability == "unavailable");
private property <string> notifications-blocked-text: SystemNotifications.availability == "denied"
? @tr("Notifications from Flectar Mail are turned off in your system settings.")
: SystemNotifications.availability == "refused"
? @tr("macOS doesn't allow notifications from Flectar Mail where it is now. Move it to the Applications folder, then open it again.")
: @tr("This system has no notification service running, so notifications can't be shown.");
private property <bool> notifications-settings-offered: SystemNotifications.availability == "denied"
&& SystemNotifications.settings-openable;
// A phone checks for new mail in the background when its system lets
// it; desktops follow the accounts while the app runs.
private property <string> notifications-title: SystemNotifications.background-checks == ""
? @tr("Desktop notifications") : @tr("New mail notifications");
// When the system checks for new mail in the background, for what
// notifications and the app icon's count show.
// What the last check for a new version found (src/updates.rs).
private property <string> update-status: Updates.version != ""
? @tr("Flectar Mail {} is out.", Updates.version)
: Updates.checking ? @tr("Checking for a new version…")
: Updates.failed ? @tr("GitHub couldn't be reached. Flectar Mail tries again later.")
: Updates.current ? @tr("Flectar Mail is up to date.")
: @tr("Flectar Mail checks soon after it starts.");
private property <bool> background-checks-noted: SystemNotifications.background-checks != ""
&& (root.notifications_enabled || root.unread_badge_enabled) && !root.notifications-blocked;
private property <string> background-checks-text: AppIcon.platform == "android"
? (SystemNotifications.background-checks == "off"
? @tr("Android restricts Flectar Mail in the background, so new mail shows up only while the app is open.")
: SystemNotifications.background-checks == "low-power"
? @tr("Battery Saver pauses checking for new mail while Flectar Mail is in the background.")
: @tr("Flectar Mail checks for new mail about every 15 minutes in the background, and less often while your phone is idle."))
: SystemNotifications.background-checks == "off"
? @tr("Background App Refresh is off for Flectar Mail, so new mail shows up only while the app is open.")
: SystemNotifications.background-checks == "low-power"
? @tr("Low Power Mode pauses checking for new mail while Flectar Mail is in the background.")
: @tr("iOS decides how often Flectar Mail checks for new mail in the background, so notifications can arrive late.");
private property <int> language-index: root.language_mode == "en" ? 1
: root.language_mode == "es" ? 2
: root.language_mode == "tr" ? 3
: root.language_mode == "zh_Hans" ? 4
: root.language_mode == "ru" ? 5 : 0;
private property <int> time-format-index: root.time_format == "12h" ? 1
: root.time_format == "24h" ? 2 : 0;
pure function settings-icon-color(section: string) -> color {
if root.monochrome_sidebar_icons {
return root.settings_tab == section ? root.accent : root.muted-text;
}
return section == "General" ? SettingsIconPalette.general
: section == "Accounts" ? SettingsIconPalette.accounts
: section == "Contacts" ? SettingsIconPalette.accounts
: section == "Notifications" ? SettingsIconPalette.notifications
: section == "Shortcuts" ? SettingsIconPalette.shortcuts
: section == "Data" ? SettingsIconPalette.storage
: section == "Services" ? SettingsIconPalette.services
: SettingsIconPalette.about;
}
pure function settings-section-visible(section: string) -> bool {
// No notifications on this platform: nothing to set.
if section == "Notifications" && SystemNotifications.availability == "" { return false; }
if root.settings-search-query == "" { return true; }
let terms = section == "General"
? @tr("general theme appearance palette app icon dock taskbar launcher language time clock 12-hour 24-hour workspace layout window frameless title bar renderer privacy remote images sync group messages date inbox tray background startup sign in login download location folder attachments save tabs conversations threads") + " " + @tr("sending undo send delay take back")
+ " " + @tr("spelling spell check dictionary misspelled words")
: section == "Accounts"
? @tr("accounts profiles personal work colors providers gmail outlook imap pop3 jmap caldav carddav mail history server oauth security signatures")
: section == "Contacts"
? @tr("contacts people suggestions autocomplete recipients senders address book privacy clear")
: section == "Notifications"
? @tr("notifications alerts sounds desktop new mail unread count badge app icon dock taskbar background app refresh low power mode battery saver")
: section == "Shortcuts"
? @tr("shortcuts keyboard keys compose search archive spam junk trash delete navigation messages reading find zoom caret tabs")
: section == "Data"
? @tr("storage data usage mail attachments files databases backup snapshot import export delete")
: section == "Services"
? @tr("services flectar base account sign in subscription paid sync devices business email custom domain mailbox")
: @tr("about version credits contributors license flectar") + " " + @tr("Privacy policy")
+ (Updates.offered ? " " + @tr("updates new version release download check") : "");
return root.settings-search-matches(terms, root.settings-search-query);
}
private property <bool> search-has-results: root.settings-section-visible("General")
|| root.settings-section-visible("Accounts") || root.settings-section-visible("Contacts")
|| root.settings-section-visible("Notifications")
|| root.settings-section-visible("Shortcuts") || root.settings-section-visible("Data")
|| root.settings-section-visible("Services") || root.settings-section-visible("About");
init => {
if root.new-profile-requested {
root.new-profile-requested = false;
root.open-new-profile(-1);
}
}
changed new-profile-requested => {
if root.new-profile-requested {
root.new-profile-requested = false;
root.open-new-profile(-1);
}
}
function select-undo-send(index: int) {
root.undo_send_seconds = index == 0 ? 0 : index == 1 ? 5 : index == 2 ? 10 : index == 3 ? 20 : 30;
root.save_undo_send(root.undo_send_seconds);
}
function select-sync-interval(index: int) {
root.sync_interval_minutes = index == 0 ? 1 : index == 1 ? 5 : 15;
root.save_sync_interval(root.sync_interval_minutes);
}
function select-notification-mode(index: int) {
root.notifications_enabled = index != 0;
root.notification_scope = index == 2 ? "all" : "important";
root.save_notification_settings(
root.notifications_enabled,
root.notification_sound_enabled,
root.notification_scope,
);
}
function select-language(index: int) {
root.language_mode = index == 1 ? "en"
: index == 2 ? "es"
: index == 3 ? "tr"
: index == 4 ? "zh_Hans"
: index == 5 ? "ru" : "system";
root.save_language(root.language_mode);
}
function select-time-format(index: int) {
root.time_format = index == 1 ? "12h" : index == 2 ? "24h" : "automatic";
root.save_time_format(root.time_format);
}
function update-settings-search(value: string) {
root.settings-search-query = value;
if value != "" && !root.settings-section-visible(root.settings_tab) {
root.settings_tab = root.settings-section-visible("General") ? "General"
: root.settings-section-visible("Accounts") ? "Accounts"
: root.settings-section-visible("Contacts") ? "Contacts"
: root.settings-section-visible("Notifications") ? "Notifications"
: root.settings-section-visible("Shortcuts") ? "Shortcuts"
: root.settings-section-visible("Data") ? "Data"
: root.settings-section-visible("Services") ? "Services"
: root.settings-section-visible("About") ? "About" : root.settings_tab;
}
}
pure function mail-history-index(value: string) -> int {
return value == "oneMonth" ? 0
: value == "threeMonths" ? 1
: value == "sixMonths" ? 2
: value == "oneYear" ? 3 : 4;
}
function select-mail-history(account-id: int, index: int) {
root.save_account_mail_history(
account-id,
index == 0 ? "oneMonth"
: index == 1 ? "threeMonths"
: index == 2 ? "sixMonths"
: index == 3 ? "oneYear" : "all",
);
}
pure function keep-on-server-index(value: string) -> int {
return value == "untilDeleted" ? 1
: value == "sevenDays" ? 2
: value == "fourteenDays" ? 3
: value == "thirtyDays" ? 4
: value == "untilDownloaded" ? 5 : 0;
}
function select-keep-on-server(account-id: int, index: int) {
root.save_account_keep_on_server(
account-id,
index == 1 ? "untilDeleted"
: index == 2 ? "sevenDays"
: index == 3 ? "fourteenDays"
: index == 4 ? "thirtyDays"
: index == 5 ? "untilDownloaded" : "always",
);
}
// Whether the server removes downloaded mail sooner than the account's
// choice keeps it there.
pure function server-removes-sooner(account: AccountRow) -> bool {
let chosen = account.keep_on_server == "sevenDays" ? 7
: account.keep_on_server == "fourteenDays" ? 14
: account.keep_on_server == "thirtyDays" ? 30
: account.keep_on_server == "untilDownloaded" ? 0 : -1;
return account.server_keeps_mail_days >= 0
&& (chosen < 0 || account.server_keeps_mail_days < chosen);
}
private property <bool> account_form_valid: root.account_email != ""
&& root.account_password != ""
&& (root.account_protocol == "jmap" || (
root.incoming_host != ""
&& root.incoming_port != ""
&& root.smtp_host != ""
&& root.smtp_port != ""));
function edit-account(account: AccountRow) {
if account.provider == "generic" {
root.account_email = account.email;
root.account_username = account.username;
root.account_protocol = account.mail_protocol;
root.jmap_url = account.jmap_url;
root.incoming_security = account.incoming_security;
root.smtp_security = account.smtp_security;
root.trusted_certificate_pem = account.trusted_certificate_pem;
root.incoming_host = account.incoming_host;
root.incoming_port = account.incoming_port;
root.smtp_host = account.smtp_host;
root.smtp_port = account.smtp_port;
root.account_password = "";
root.account_editing = true;
root.show_account_status = false;
root.account_form_open = true;
} else {
root.reauth_account(account.id);
}
}
function confirm-account-removal(account: AccountRow) {
root.delete_account_id = account.id;
root.delete_account_name = account.email;
root.delete_account_device_mail = account.mail_protocol == "pop3";
root.delete_account_confirmation = true;
}
// Servers are suggested for new accounts only; an edit keeps the
// account's own.
function suggest-servers(email: string) {
if !root.account_editing {
root.suggest_account_settings(email);
}
}
function choose-protocol(protocol: string) {
if MailPorts.is-standard-incoming(root.incoming_port) {
root.incoming_port = MailPorts.incoming(protocol, root.incoming_security);
}
root.account_protocol = protocol;
}
function open_new_mail_account() {
root.account_editing = false;
root.account_email = "";
root.account_username = "";
root.account_password = "";
root.account_protocol = "imap";
root.jmap_url = "";
root.incoming_security = "tls";
root.smtp_security = "tls";
root.trusted_certificate_pem = "";
root.incoming_host = "";
root.incoming_port = "993";
root.smtp_host = "";
root.smtp_port = "465";
root.show_account_status = false;
root.account_form_open = true;
}
// A new profile, starting with `account-id` checked, or none for -1. Each
// new profile starts with the next preset color.
function open-new-profile(account-id: int) {
root.prepare_profile_members("", account-id);
root.profile-editor-id = "";
root.profile-editor-name = "";
root.profile-editor-color = ProfilePalette.colors[mod(root.mail_profiles.length, ProfilePalette.colors.length)];
root.profile-editor-custom-color = false;
root.profile-editor-error = "";
root.profile-editor-open = true;
}
function edit-profile(profile: MailProfileRow) {
root.prepare_profile_members(profile.id, -1);
root.profile-editor-id = profile.id;
root.profile-editor-name = profile.name;
root.profile-editor-color = profile.color;
root.profile-editor-custom-color = !ProfilePalette.is-preset(profile.color);
root.profile-editor-error = "";
root.profile-editor-open = true;
}
function close-profile-editor() {
root.profile-editor-open = false;
root.profile-editor-id = "";
root.profile-editor-name = "";
root.profile-editor-custom-color = false;
root.profile-editor-error = "";
}
function confirm-profile-removal(profile: MailProfileRow) {
root.delete-profile-id = profile.id;
root.delete-profile-name = profile.name;
root.delete-profile-confirmation = true;
}
function open-account-profile-picker(account: AccountRow, index: int) {
root.account-profile-picker-id = account.id;
root.account-profile-picker-index = index;
root.account-profile-picker-open = true;
}
function close-account-profile-picker() {
root.account-profile-picker-open = false;
root.account-profile-picker-id = -1;
root.account-profile-picker-index = -1;
}
function add-caldav(account: AccountRow) {
root.caldav_account_id = account.id;
root.caldav_connection_id = -1;
root.caldav-account-email = account.email;
root.caldav_url = "";
root.caldav_username = account.username != "" ? account.username : account.email;
root.caldav_password = "";
}
function edit-caldav(account: AccountRow, connection: CalendarConnectionRow) {
root.caldav_account_id = account.id;
root.caldav_connection_id = connection.id;
root.caldav-account-email = account.email;
root.caldav_url = connection.base_url;
root.caldav_username = connection.username;
root.caldav_password = "";
}
function close-caldav-settings() {
root.caldav_account_id = -1;
root.caldav_connection_id = -1;
root.caldav_password = "";
}
function confirm-calendar-removal(account: AccountRow, connection: CalendarConnectionRow) {
root.remove-calendar-id = connection.id;
root.remove-calendar-name = connection.kind == "google" ? @tr("Google Calendar")
: connection.kind == "microsoft" ? @tr("Outlook calendar")
: connection.calendar-count == 1 && connection.calendar-name != "" ? connection.calendar-name
: connection.host;
root.remove-calendar-confirmation = true;
}
Rectangle {
visible: root.settings_open;
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
background: root.page-bg;
HorizontalBox {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
padding: 0px;
spacing: 0px;
WorkspaceSidebarSurface {
visible: !root.compact;
width: root.compact ? 0px : LayoutMetrics.sidebar-width;
height: parent.height;
surface-color: root.sidebar-bg;
divider-color: root.border-subtle;
VerticalBox {
padding-top: LayoutMetrics.compact-inset;
padding-right: LayoutMetrics.sidebar-inset;
padding-left: LayoutMetrics.sidebar-inset;
spacing: 5px;
Text { height: max(22px, self.preferred-height); text: @tr("SETTINGS"); color: root.muted-text; font-size: TypeScale.caption; font-weight: 700; vertical-alignment: center; }
SettingsSearch { query <=> root.settings-search-query; muted-color: root.muted-text; hover-background: root.accent-soft; edited(value) => { root.update-settings-search(value); } }
if !root.search-has-results : Text { text: @tr("No settings found"); color: root.muted-text; font-size: TypeScale.secondary; wrap: word-wrap; }
if root.settings-section-visible("General") : SettingsNavItem { text: @tr("General"); icon: @image-url("../icons/lucide/filled/sliders.svg"); icon-color: root.settings-icon-color("General"); selected: root.settings_tab == "General"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "General"; } }
if root.settings-section-visible("Accounts") : SettingsNavItem { text: @tr("Accounts"); icon: @image-url("../icons/lucide/filled/account.svg"); icon-color: root.settings-icon-color("Accounts"); selected: root.settings_tab == "Accounts"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Accounts"; } }
if root.settings-section-visible("Contacts") : SettingsNavItem { text: @tr("Contacts"); icon: @image-url("../icons/lucide/filled/contacts.svg"); icon-color: root.settings-icon-color("Contacts"); selected: root.settings_tab == "Contacts"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Contacts"; } }
if root.settings-section-visible("Notifications") : SettingsNavItem { text: @tr("Notifications"); icon: @image-url("../icons/lucide/filled/bell.svg"); icon-color: root.settings-icon-color("Notifications"); selected: root.settings_tab == "Notifications"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Notifications"; } }
if root.settings-section-visible("Shortcuts") : SettingsNavItem { text: @tr("Shortcuts"); icon: @image-url("../icons/lucide/filled/keyboard.svg"); icon-color: root.settings-icon-color("Shortcuts"); selected: root.settings_tab == "Shortcuts"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Shortcuts"; } }
if root.settings-section-visible("Data") : SettingsNavItem { text: @tr("Storage"); icon: @image-url("../icons/lucide/filled/database.svg"); icon-color: root.settings-icon-color("Data"); selected: root.settings_tab == "Data"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Data"; } }
if root.settings-section-visible("Services") : SettingsNavItem { text: @tr("Services"); icon: @image-url("../icons/lucide/filled/cloud.svg"); icon-color: root.settings-icon-color("Services"); selected: root.settings_tab == "Services"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Services"; } }
if root.settings-section-visible("About") : SettingsNavItem { text: @tr("About"); icon: @image-url("../icons/lucide/filled/info.svg"); icon-color: root.settings-icon-color("About"); selected: root.settings_tab == "About"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "About"; } }
Rectangle { vertical-stretch: 1; }
}
}
Rectangle {
horizontal-stretch: 1;
height: parent.height;
background: root.surface-raised;
general-scroll := ScrollView {
visible: root.settings_tab == "General";
width: parent.width;
height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox {
x: root.compact ? 16px : max(24px, (general-scroll.visible-width - 920px) / 2);
width: root.compact ? general-scroll.visible-width - 32px : min(920px, general-scroll.visible-width - 48px);
padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px;
padding-bottom: 32px;
spacing: 12px;
SettingsPageTitle { label: @tr("General"); title-color: root.title-color; }
Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Manage appearance, privacy, and synchronization."); color: root.secondary-text; font-size: TypeScale.secondary; wrap: word-wrap; }
Rectangle {
height: max(root.compact ? 158px : 96px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding: 16px;
SettingsSelectRow {
compact: root.compact;
title: @tr("Message appearance");
description: @tr("Choose the background and text colors used to read messages.");
choices: [@tr("Follow app theme"), @tr("Light"), @tr("Dark")];
selected-index: MessageColors.preference == "light" ? 1
: MessageColors.preference == "dark" ? 2 : 0;
title-color: root.body-text;
description-color: root.secondary-text;
selected(index, value) => {
MessageColors.choose(index == 1 ? "light" : index == 2 ? "dark" : "system");
}
}
}
}
theme-editor := ThemeEditor {
compact: root.compact;
theme-mode <=> root.theme_mode;
save-theme-mode(mode) => { root.save_theme(mode); }
save-preset(preset) => { root.save_theme_preset(preset); }
save-custom-theme(
light-primary,
light-page,
light-surface,
light-text,
light-border,
dark-primary,
dark-page,
dark-surface,
dark-text,
dark-border,
) => {
return root.save_custom_theme(
light-primary,
light-page,
light-surface,
light-text,
light-border,
dark-primary,
dark-page,
dark-surface,
dark-text,
dark-border,
);
}
}
if AppIcon.available : AppIconPicker { compact: root.compact; }
Rectangle {
height: max((root.compact ? 435px : 323px)
+ (WindowChrome.available ? (root.compact ? 89px : 59px) : 0px), self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
spacing: 5px;
Text { text: @tr("Interface"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; wrap: word-wrap; }
SettingsSwitchRow {
compact: root.compact;
title: @tr("Monochrome sidebar icons");
description: @tr("Use one neutral color for mailbox and folder icons.");
checked: root.monochrome_sidebar_icons;
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => {
root.monochrome_sidebar_icons = checked;
root.save_sidebar_icon_style(checked);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
title: @tr("No avatars");
description: @tr("Hide sender images in the message list and reader.");
checked: !root.show_avatars;
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => {
root.show_avatars = !checked;
root.save_avatar_visibility(root.show_avatars);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
title: @tr("Tabs");
description: @tr("Keep several mailboxes, conversations and other places open in tabs.");
checked: root.show_tabs;
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => {
root.show_tabs = checked;
root.save_show_tabs(checked);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
if WindowChrome.available : SettingsSwitchRow {
compact: root.compact;
title: @tr("Frameless window");
description: @tr("Replace the system title bar with window controls in the toolbar.");
checked: WindowChrome.preference == "integrated";
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => { WindowChrome.choose(checked); }
}
if WindowChrome.available : Rectangle { height: 1px; background: root.border-subtle; }
Text { height: max(20px, self.preferred-height); text: @tr("Workspace layout"); color: root.secondary-text; font-size: TypeScale.secondary; vertical-alignment: center; }
HorizontalBox {
padding: 0px;
spacing: 8px;
WorkspaceLayoutChoice {
horizontal-stretch: 1; min-width: 0px;
name: @tr("Default");
selected: root.workspace_layout == "default";
chosen => { root.workspace_layout = "default"; root.save_workspace_layout(root.workspace_layout); }
}
WorkspaceLayoutChoice {
horizontal-stretch: 1; min-width: 0px;
name: @tr("Minimal"); minimal: true;
selected: root.workspace_layout == "minimal";
chosen => { root.workspace_layout = "minimal"; root.save_workspace_layout(root.workspace_layout); }
}
}
}
}
Rectangle {
height: max(root.compact ? 154px : 116px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
spacing: 5px;
Text { text: @tr("Language and time"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; wrap: word-wrap; }
SettingsSelectRow {
compact: root.compact;
title: @tr("Display language");
description: root.language_mode == "system" ? @tr("Follow your device language when a translation is available.") : @tr("Choose the language used throughout Flectar Mail.");
choices: root.language-options;
selected-index: root.language-index;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-language(index); }
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSelectRow {
compact: root.compact;
title: @tr("Time format");
description: root.time_format == "automatic" ? @tr("Follow your region, for example {}.", root.time_format_sample) : @tr("Times appear as {}.", root.time_format_sample);
choices: root.time-format-options;
selected-index: root.time-format-index;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-time-format(index); }
}
if root.spelling_available : Rectangle { height: 1px; background: root.border-subtle; }
if root.spelling_available : SettingsSwitchRow {
compact: root.compact;
title: @tr("Check spelling");
description: !root.spelling_installed && root.spelling_system == "windows"
? @tr("Add a language in Windows Settings, under Time & language, to check spelling.")
: !root.spelling_installed && root.spelling_system == "android"
? @tr("Turn on the spell checker in Android's settings to check spelling.")
: !root.spelling_installed
? @tr("Install Hunspell dictionaries for your languages from your system's software to check spelling.")
: root.spelling_system == "macos"
? @tr("Underline misspelled words in messages you write, in the languages set in System Settings. Right-click a word for corrections.")
: root.spelling_system == "android"
? @tr("Underline misspelled words in messages you write, in the language set for Android's spell checker. Tap a word for corrections.")
: root.spelling_system == "ios"
? @tr("Underline misspelled words in messages you write, in your preferred languages from Settings. Tap a word for corrections.")
: @tr("Underline misspelled words in messages you write. Right-click a word for corrections and languages.");
checked: root.spelling_enabled;
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => {
root.spelling_enabled = checked;
root.save_spelling_enabled(checked);
}
}
}
}
Rectangle {
height: max(root.compact ? 413px : 321px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 7px;
Text { text: @tr("Inbox behavior"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; wrap: word-wrap; }
HorizontalBox {
spacing: 8px;
Text { horizontal-stretch: 1; text: @tr("Unified Inbox"); color: root.body-text; font-size: TypeScale.body; wrap: word-wrap; }
Text { text: @tr("DEFAULT"); color: root.accent; font-size: TypeScale.caption; font-weight: 700; }
}
Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Combines incoming messages from every connected account."); color: root.faint-text; font-size: TypeScale.secondary; wrap: word-wrap; }
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
title: @tr("Group messages by date");
description: @tr("Show collapsible date sections in the mail list.");
checked: root.group_mail_by_date;
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => {
root.group_mail_by_date = checked;
root.save_group_mail_by_date(checked);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
title: @tr("Conversation view");
description: @tr("Group replies with the messages they answer. Turn this off to list, open and act on each message on its own.");
checked: root.conversation_view;
title-color: root.body-text;
description-color: root.faint-text;
toggled(checked) => {
root.conversation_view = checked;
root.save_conversation_view(checked);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
HorizontalBox {
height: (root.compact ? 16px : 13px) + mark-read-title.height + mark-read-description.height;
spacing: 16px;
VerticalBox {
horizontal-stretch: 1;
min-width: 0px;
spacing: 3px;
mark-read-title := Text { height: max(18px, self.preferred-height); text: @tr("Mark messages as read when opened"); color: root.body-text; font-size: TypeScale.body; font-weight: 600; overflow: elide; }
mark-read-description := Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Turn this off to keep unread status until you change it manually."); color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
}
Switch {
checked: root.mark_read_on_open;
toggled => {
root.mark_read_on_open = self.checked;
root.save_mark_read_on_open(self.checked);
}
}
}
}
}
Rectangle {
height: max(root.compact ? 150px : 112px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 4px;
Text { height: max(22px, self.preferred-height); text: @tr("Sending"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; vertical-alignment: center; wrap: word-wrap; }
SettingsSelectRow {
compact: root.compact;
title: @tr("Undo send");
description: @tr("How long a sent message waits before it leaves, so that you can take it back.");
choices: root.undo-send-options;
selected-index: root.undo-send-index;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-undo-send(index); }
}
}
}
Rectangle {
height: (root.compact ? 54px : 58px) + remote-title.height + remote-description.height + remote-note.height;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
HorizontalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 16px;
VerticalBox {
spacing: 5px;
remote-title := Text { height: max(20px, self.preferred-height); text: @tr("Remote images"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; }
remote-description := Text { height: max(root.compact ? 38px : 17px, self.preferred-height); text: root.remote-images-available ? @tr("Load sender favicons and images embedded in message bodies.") : @tr("Unavailable in this minimal build."); color: root.faint-text; font-size: TypeScale.secondary; wrap: word-wrap; }
remote-note := Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Off by default to block tracking pixels and minimize memory use."); color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
}
Rectangle { horizontal-stretch: 1; }
Switch {
enabled: root.remote-images-available;
checked: root.remote-images-enabled;
toggled => { root.set-remote-images(self.checked); }
}
}
}
Rectangle {
visible: !root.compact;
height: root.compact ? 0px : 51px + tray-title.height + tray-description.height + tray-note.height;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
HorizontalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 16px;
VerticalBox {
spacing: 5px;
tray-title := Text { height: max(20px, self.preferred-height); text: root.tray_supported ? @tr("Keep running in system tray") : @tr("Keep running in the background"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; }
tray-description := Text { height: max(17px, self.preferred-height); text: @tr("Closing the window hides Flectar Mail while mail continues syncing."); color: root.faint-text; font-size: TypeScale.secondary; wrap: word-wrap; }
tray-note := Text { height: max(16px, self.preferred-height); text: root.tray_supported ? @tr("Disabled by default; Quit from the tray menu stops the app.") : @tr("Disabled by default; open Flectar Mail again to bring its window back."); color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
}
Rectangle { horizontal-stretch: 1; }
Switch {
// Two-way, so the app can correct the switch after a toggle.
checked <=> root.close_to_tray;
toggled => { root.set_close_to_tray(self.checked); }
}
}
}
Rectangle {
visible: !root.compact && root.launch_at_login_available;
height: self.visible ? 51px + login-title.height + login-description.height + login-note.height : 0px;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
HorizontalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 16px;
VerticalBox {
spacing: 5px;
login-title := Text { height: max(20px, self.preferred-height); text: @tr("Start automatically"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; }
login-description := Text { height: max(17px, self.preferred-height); text: !root.close_to_tray ? @tr("Opens Flectar Mail when you sign in.") : root.tray_supported ? @tr("Starts Flectar Mail in the system tray when you sign in.") : @tr("Starts Flectar Mail in the background when you sign in."); color: root.faint-text; font-size: TypeScale.secondary; wrap: word-wrap; }
login-note := Text { height: max(16px, self.preferred-height); text: @tr("Disabled by default; also listed in your system's startup apps."); color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
}
Rectangle { horizontal-stretch: 1; }
Switch {
// Two-way, so the app can correct the switch after a toggle.
checked <=> root.launch_at_login;
toggled => { root.set_launch_at_login(self.checked); }
}
}
}
Rectangle {
visible: !root.compact && DefaultEmailApp.status != "";
height: self.visible ? 51px + mailto-title.height + mailto-description.height + mailto-note.height : 0px;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
HorizontalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 16px;
VerticalBox {
spacing: 5px;
mailto-title := Text { height: max(20px, self.preferred-height); text: @tr("Default email app"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; }
mailto-description := Text {
height: max(17px, self.preferred-height);
text: DefaultEmailApp.status == "default" ? @tr("Email links in browsers and other apps open in Flectar Mail.")
: DefaultEmailApp.status == "system-settings" ? @tr("To open email links from browsers and other apps here, choose Flectar Mail for email in your system's default apps.")
: DefaultEmailApp.status == "not-installed" ? @tr("Add Flectar Mail to your applications menu to open email links from browsers and other apps here.")
: @tr("Open email links from browsers and other apps in Flectar Mail.");
color: root.faint-text;
font-size: TypeScale.secondary;
wrap: word-wrap;
}
mailto-note := Text {
height: max(16px, self.preferred-height);
text: DefaultEmailApp.status == "not-default" && AppIcon.platform == "windows" ? @tr("Opens Default apps in Windows Settings, where you choose Flectar Mail.")
: DefaultEmailApp.status == "not-default" && AppIcon.platform == "macos" ? @tr("macOS asks you to confirm the change.")
: DefaultEmailApp.status == "not-installed" ? @tr("Run the AppImage with --integrate, or add it with your AppImage manager.")
: @tr("Links fill in a new message for you to review; files are never attached from them.");
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
}
Rectangle { horizontal-stretch: 1; }
// The buttons share the width of the wider one.
VerticalLayout {
alignment: center;
spacing: make-default.visible && remove-registration.visible ? 8px : 0px;
make-default := Button {
visible: DefaultEmailApp.status == "not-default";
width: self.visible ? max(132px, self.content-width, remove-registration.visible ? remove-registration.content-width : 0px) : 0px;
height: self.visible ? 32px : 0px;
text: @tr("Make default");
enabled: !DefaultEmailApp.busy;
clicked => { DefaultEmailApp.make-default(); }
}
remove-registration := Button {
visible: DefaultEmailApp.removable;
width: self.visible ? max(132px, self.content-width, make-default.visible ? make-default.content-width : 0px) : 0px;
height: self.visible ? 32px : 0px;
text: @tr("Remove from Windows");
enabled: !DefaultEmailApp.busy;
clicked => { DefaultEmailApp.remove(); }
}
}
}
}
Rectangle {
// Without notifications on this platform, their row and its separator go.
height: max(SystemNotifications.availability == ""
? (root.compact ? 231px : 179px)
: (root.compact ? 360px : 246px), self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 18px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 4px;
Text { height: max(22px, self.preferred-height); text: @tr("Sync and notifications"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; vertical-alignment: center; wrap: word-wrap; }
SettingsSelectRow {
compact: root.compact;
title: @tr("Check for new mail");
description: @tr("How often to refresh when an instant provider update is unavailable.");
choices: root.sync-interval-options;
selected-index: root.sync-interval-index;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-sync-interval(index); }
}
Rectangle { height: 1px; background: root.border-subtle; }
if SystemNotifications.availability != "" : SettingsSelectRow {
compact: root.compact;
title: root.notifications-title;
description: @tr("Choose which incoming messages can alert you.");
choices: root.notification-options;
selected-index: root.notification-index;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-notification-mode(index); }
}
if SystemNotifications.availability != "" : Rectangle { height: 1px; background: root.border-subtle; }
HorizontalBox {
height: max(36px, sync-state.preferred-height + 16px);
spacing: 12px;
sync-state := Text {
horizontal-stretch: 1;
text: root.sync_status == "" ? @tr("Provider synchronization is ready.") : root.sync_status;
color: root.faint-text;
font-size: TypeScale.caption;
vertical-alignment: center;
overflow: elide;
}
Button {
width: max(112px, self.content-width);
text: root.sync_in_progress ? @tr("Syncing…") : @tr("Sync now");
enabled: !root.sync_in_progress;
clicked => { root.sync_mail(); }
}
}
}
}
if RendererSettings.available : Rectangle {
height: max(root.compact ? 176px : 114px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding: 16px;
spacing: 6px;
SettingsSelectRow {
compact: root.compact;
title: @tr("Renderer");
description: RendererSettings.gpu-supported ? @tr("Changes take effect after restarting Flectar Mail.") : @tr("GPU mode is unavailable in this build.");
choices: RendererSettings.gpu-supported ? [@tr("CPU — Low Memory"), @tr("GPU — WGPU")] : [@tr("CPU — Low Memory")];
selected-index: RendererSettings.gpu-supported && RendererSettings.preferred == "gpu" ? 1 : 0;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => {
RendererSettings.choose(index == 1 ? "gpu" : "cpu");
self.selected-index = RendererSettings.gpu-supported && RendererSettings.preferred == "gpu" ? 1 : 0;
}
}
HorizontalBox {
height: max(18px, self.preferred-height);
padding: 0px;
padding-left: root.compact ? 8px : 16px;
Text {
text: RendererSettings.error != "" ? RendererSettings.error : RendererSettings.gpu-failed && RendererSettings.preferred == "gpu" ? @tr("GPU — WGPU could not start, so CPU — Low Memory is active.") : RendererSettings.restart-required ? @tr("Restart required. Your current renderer is still active.") : RendererSettings.active == "gpu" ? @tr("Active: GPU — WGPU") : @tr("Active: CPU — Low Memory");
color: root.faint-text;
font-size: TypeScale.caption;
overflow: elide;
}
}
}
}
if root.download_directory_available : Rectangle {
height: self.preferred-height;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding: 16px;
// Laid out like SettingsSelectRow: texts left of the buttons on
// desktop, stacked above them in the compact layout.
if !root.compact : HorizontalBox {
spacing: 20px;
VerticalBox {
horizontal-stretch: 1;
spacing: 2px;
Text {
height: max(18px, self.preferred-height);
text: @tr("Download location");
color: root.body-text;
font-size: TypeScale.body;
font-weight: 600;
overflow: elide;
}
Text {
height: max(17px, self.preferred-height);
text: root.download_directory == ""
? @tr("Saving an attachment asks where to put it, starting in the folder your system suggests.")
: root.download_directory;
color: root.faint-text;
font-size: TypeScale.caption;
overflow: elide;
}
}
Button {
width: max(132px, self.content-width);
text: @tr("Choose folder…");
clicked => { root.choose_download_directory(); }
}
Button {
width: max(96px, self.content-width);
text: @tr("Reset");
enabled: root.download_directory != "";
clicked => { root.clear_download_directory(); }
}
}
if root.compact : VerticalBox {
padding: 8px;
spacing: 7px;
Text {
height: max(20px, self.preferred-height);
text: @tr("Download location");
color: root.body-text;
font-size: TypeScale.body;
font-weight: 600;
wrap: word-wrap;
}
Text {
height: max(17px, self.preferred-height);
text: root.download_directory == ""
? @tr("Saving an attachment asks where to put it, starting in the folder your system suggests.")
: root.download_directory;
color: root.faint-text;
font-size: TypeScale.caption;
// A path has no spaces to break at.
wrap: root.download_directory == "" ? TextWrap.word-wrap : TextWrap.char-wrap;
}
HorizontalBox {
padding: 0px;
spacing: 8px;
Button {
horizontal-stretch: 1;
text: @tr("Choose folder…");
clicked => { root.choose_download_directory(); }
}
Button {
horizontal-stretch: 1;
text: @tr("Reset");
enabled: root.download_directory != "";
clicked => { root.clear_download_directory(); }
}
}
}
}
}
}
}
contacts-settings-scroll := ScrollView {
visible: root.settings_tab == "Contacts";
width: parent.width;
height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox {
x: root.compact ? 16px : max(24px, (contacts-settings-scroll.visible-width - 920px) / 2);
width: root.compact ? contacts-settings-scroll.visible-width - 32px : min(920px, contacts-settings-scroll.visible-width - 48px);
padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px;
padding-bottom: 32px;
spacing: 14px;
SettingsPageTitle { label: @tr("Contacts"); title-color: root.title-color; }
Text {
height: max(root.compact ? 52px : 34px, self.preferred-height);
text: @tr("Keep your address book clean while still making frequent recipients easy to find.");
color: root.secondary-text;
font-size: TypeScale.secondary;
wrap: word-wrap;
}
Rectangle {
height: max(root.compact ? 466px : 274px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 12px;
padding-right: 20px;
padding-bottom: 12px;
padding-left: 20px;
spacing: 0px;
SettingsSwitchRow {
compact: root.compact;
compact-description-height: 51px;
desktop-height: 60px;
title: @tr("Remember people I email");
description: @tr("While enabled, remember To, Cc, and Bcc recipients from new sends and synced Sent mail.");
checked: root.collect_outgoing_contacts;
title-color: root.body-text;
description-color: root.faint-text;
toggled(value) => {
root.collect_outgoing_contacts = value;
root.save_contact_discovery_settings(value, root.collect_incoming_contacts, root.contact_suggest_all_accounts, root.suggest_learned_contacts);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
compact-title-height: 34px;
compact-description-height: 51px;
desktop-height: 60px;
title: @tr("Remember incoming senders");
description: @tr("Remember human senders from new mail. Automated, spam, trash, and existing mail are excluded.");
checked: root.collect_incoming_contacts;
title-color: root.body-text;
description-color: root.faint-text;
toggled(value) => {
root.collect_incoming_contacts = value;
root.save_contact_discovery_settings(root.collect_outgoing_contacts, value, root.contact_suggest_all_accounts, root.suggest_learned_contacts);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
compact-title-height: 34px;
compact-description-height: 51px;
desktop-height: 60px;
title: @tr("Use suggestions from all accounts");
description: @tr("Include people emailed from any connected account. Turn off to keep learned recipients account-specific.");
checked: root.contact_suggest_all_accounts;
title-color: root.body-text;
description-color: root.faint-text;
toggled(value) => {
root.contact_suggest_all_accounts = value;
root.save_contact_discovery_settings(root.collect_outgoing_contacts, root.collect_incoming_contacts, value, root.suggest_learned_contacts);
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
compact-title-height: 34px;
compact-description-height: 51px;
desktop-height: 60px;
title: @tr("Use suggestions when composing");
description: @tr("Include learned people in recipient autocomplete. Saved and CardDAV contacts are always available.");
checked: root.suggest_learned_contacts;
title-color: root.body-text;
description-color: root.faint-text;
toggled(value) => {
root.suggest_learned_contacts = value;
root.save_contact_discovery_settings(root.collect_outgoing_contacts, root.collect_incoming_contacts, root.contact_suggest_all_accounts, value);
}
}
}
}
Rectangle {
height: max(root.compact ? 188px : 154px, self.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding: 18px;
spacing: 8px;
Text { text: @tr("Suggested people"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; wrap: word-wrap; }
Text {
height: max(root.compact ? 52px : 34px, self.preferred-height);
text: @tr("People you email remain available as local suggestions and never sync to CardDAV. Clear them any time to restart learning.");
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
HorizontalBox {
height: 36px;
spacing: 8px;
Rectangle { horizontal-stretch: 1; }
if root.clear-contact-suggestions-confirming : Button {
width: max(100px, self.content-width);
horizontal-stretch: 0;
text: @tr("Cancel");
clicked => { root.clear-contact-suggestions-confirming = false; }
}
Button {
width: max(root.compact ? 180px : 220px, self.content-width);
horizontal-stretch: 0;
text: root.clear-contact-suggestions-confirming ? @tr("Clear now") : @tr("Clear suggested people");
clicked => {
if root.clear-contact-suggestions-confirming {
if root.clear_contact_suggestions() {
root.clear-contact-suggestions-confirming = false;
}
} else {
root.clear-contact-suggestions-confirming = true;
}
}
}
}
}
}
Rectangle {
height: max(112px, blocked-senders.preferred-height);
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
blocked-senders := VerticalBox {
padding: 18px;
spacing: 8px;
Text { text: @tr("Blocked senders"); color: root.heading; font-size: TypeScale.subtitle; font-weight: 700; wrap: word-wrap; }
Text {
height: self.preferred-height;
text: @tr("Their mail goes to Spam when Flectar Mail receives it. Block a sender from the menu of one of their messages.");
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
if BlockSender.addresses.length == 0 : Text {
text: @tr("No one is blocked.");
color: root.secondary-text;
font-size: TypeScale.secondary;
wrap: word-wrap;
}
for address in BlockSender.addresses : HorizontalBox {
height: 36px;
// The buttons line up with the card's other buttons.
padding: 0px;
padding-right: 8px;
spacing: 8px;
Text {
text: address;
color: root.body-text;
font-size: TypeScale.secondary;
vertical-alignment: center;
overflow: elide;
horizontal-stretch: 1;
}
Button {
width: max(96px, self.content-width);
horizontal-stretch: 0;
text: @tr("Unblock");
accessible-label: @tr("Unblock {}", address);
clicked => { BlockSender.unblock(address); }
}
}
}
}
if root.sync_status != "" : CopyableDiagnostic {
text: root.sync_status;
text-color: root.secondary-text;
font-size: TypeScale.secondary;
}
}
}
accounts-scroll := ScrollView {
visible: root.settings_tab == "Accounts";
width: parent.width;
height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox {
x: root.compact ? 16px : max(24px, (accounts-scroll.visible-width - 920px) / 2);
width: root.compact ? accounts-scroll.visible-width - 32px : min(920px, accounts-scroll.visible-width - 48px);
padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px;
padding-bottom: 32px;
spacing: 14px;
SettingsPageTitle { label: @tr("Accounts"); title-color: root.title-color; }
Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Manage connected mailboxes and add providers. Drag an account to change its order everywhere."); color: root.secondary-text; font-size: TypeScale.secondary; wrap: word-wrap; }
if root.sync_status != "" : CopyableDiagnostic {
text: root.sync_status;
text-color: root.secondary-text;
font-size: TypeScale.secondary;
}
Rectangle {
height: profiles-toggle.y + profiles-toggle.height + (root.compact ? 18px : 16px);
background: root.surface;
border-radius: 10px;
border-width: 1px;
border-color: root.border-subtle;
profiles-title := Text {
x: 20px; y: 16px;
width: parent.width - (root.compact ? 40px : 180px);
height: max(24px, self.preferred-height);
text: @tr("Profiles");
color: root.heading;
font-size: TypeScale.subtitle;
font-weight: 600;
vertical-alignment: center;
}
profiles-description := Text {
x: 20px; y: profiles-title.y + profiles-title.height + 3px;
width: parent.width - 40px;
height: max(root.compact ? 38px : 20px, self.preferred-height);
text: @tr("Group accounts as Personal or Work and identify them by color in the mail list.");
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
vertical-alignment: center;
}
if !root.compact : Button {
x: parent.width - self.width - 20px; y: 14px;
width: max(126px, self.content-width); height: 32px;
text: @tr("Add profile");
icon: @image-url("../icons/lucide/outline/plus.svg");
has-icon: true;
primary: true;
clicked => { root.open-new-profile(-1); }
}
profiles-toggle := HorizontalBox {
x: 20px;
y: profiles-description.y + profiles-description.height + (root.compact ? 7px : 13px);
width: parent.width - 40px;
height: max(root.compact ? 70px : 34px, profiles-toggle-label.preferred-height + 16px);
spacing: 10px;
profiles-toggle-label := Text {
horizontal-stretch: 1;
text: @tr("Show account color at the left edge of each message");
color: root.body-text;
font-size: TypeScale.secondary;
vertical-alignment: center;
wrap: word-wrap;
}
Switch {
checked: root.show_account_markers;
control-label: @tr("Show account color at the left edge of each message");
toggled => {
root.show_account_markers = self.checked;
root.save_account_markers_visibility(self.checked);
}
}
if root.compact : Button {
width: max(112px, self.content-width); height: 40px;
text: @tr("Add profile");
primary: true;
clicked => { root.open-new-profile(-1); }
}
}
}
if root.mail_profiles.length == 0 : Rectangle {
height: max(58px, no-profiles-text.preferred-height + 20px);
background: root.surface-raised;
border-radius: 10px;
border-width: 1px;
border-color: root.border-subtle;
no-profiles-text := Text {
x: 18px; y: 0px; width: parent.width - 36px; height: parent.height;
text: @tr("No profiles yet. Accounts keep their individual or generated colors until you create one.");
color: root.secondary-text;
font-size: TypeScale.secondary;
wrap: word-wrap;
vertical-alignment: center;
}
}
for profile in root.mail_profiles : Rectangle {
height: max(66px, profile-count.y + profile-count.height + 14px);
background: root.surface;
border-radius: 10px;
border-width: 1px;
border-color: root.border-subtle;
Rectangle {
x: 16px; y: 13px; width: 8px; height: parent.height - 26px;
border-radius: 4px;
background: profile.color;
}
profile-name := Text {
x: 38px; y: 11px;
width: parent.width - 152px; height: max(23px, self.preferred-height);
text: profile.name;
color: root.heading;
font-size: TypeScale.body-large;
font-weight: 700;
overflow: elide;
vertical-alignment: center;
}
profile-count := Text {
x: 38px; y: profile-name.y + profile-name.height;
width: parent.width - 152px; height: max(18px, self.preferred-height);
text: profile.account_count == 0 ? @tr("No accounts")
: profile.account_count == 1 ? @tr("1 account")
: @tr("{} accounts", profile.account_count);
color: root.faint-text;
font-size: TypeScale.caption;
vertical-alignment: center;
}
IconButton {
x: parent.width - 90px; y: 13px;
width: 38px; height: 38px;
icon: @image-url("../icons/lucide/outline/pencil.svg");
icon-color: root.secondary-text;
ghost: true;
hover-background: root.accent-soft;
tooltip-text: @tr("Edit profile");
clicked => { root.edit-profile(profile); }
}
IconButton {
x: parent.width - 48px; y: 13px;
width: 38px; height: 38px;
icon: @image-url("../icons/lucide/outline/trash.svg");
icon-color: AppTheme.destructive;
ghost: true;
hover-background: AppTheme.destructive.with-alpha(0.12);
tooltip-text: @tr("Delete profile");
clicked => { root.confirm-profile-removal(profile); }
}
}
Rectangle {
height: root.compact ? (root.oauth_in_progress ? 260px : 208px) : (root.oauth_in_progress ? 152px : 92px);
background: root.surface;
border-radius: 10px;
border-width: 1px;
border-color: root.border-subtle;
if !root.compact : HorizontalBox {
padding: 20px;
spacing: 10px;
ProviderButton {
horizontal-stretch: 1;
text: @tr("Add Gmail");
icon: @image-url("../icons/brand/gmail.svg");
enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving && root.google_oauth_available;
tooltip-text: root.google_oauth_available ? "" : @tr("Add app keys in Sign-in settings to enable this provider.");
foreground: root.body-text;
muted: root.muted-text;
surface: root.surface-raised;
hover-surface: root.accent-soft;
button-border: root.border-subtle;
clicked => { root.start_oauth("gmail"); }
}
ProviderButton {
horizontal-stretch: 1;
text: @tr("Add Outlook");
icon: @image-url("../icons/brand/outlook.svg");
enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving && root.microsoft_oauth_available;
tooltip-text: root.microsoft_oauth_available ? "" : @tr("Add app keys in Sign-in settings to enable this provider.");
foreground: root.body-text;
muted: root.muted-text;
surface: root.surface-raised;
hover-surface: root.accent-soft;
button-border: root.border-subtle;
clicked => { root.start_oauth("microsoft"); }
}
ProviderButton {
horizontal-stretch: 1;
text: @tr("Add mail account");
icon: @image-url("../icons/lucide/outline/mail.svg");
colorize-icon: true;
enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving;
foreground: root.body-text;
muted: root.muted-text;
surface: root.surface-raised;
hover-surface: root.accent-soft;
button-border: root.border-subtle;
clicked => { root.open_new_mail_account(); }
}
if root.oauth_in_progress : Button {
height: 52px;
text: @tr("Cancel sign-in");
clicked => { root.cancel_oauth(); }
}
}
if root.compact : VerticalBox {
padding: 14px;
spacing: 8px;
ProviderButton { text: @tr("Add Gmail"); icon: @image-url("../icons/brand/gmail.svg"); enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving && root.google_oauth_available; tooltip-text: root.google_oauth_available ? "" : @tr("Add app keys in Sign-in settings to enable this provider."); foreground: root.body-text; muted: root.muted-text; surface: root.surface-raised; hover-surface: root.accent-soft; button-border: root.border-subtle; clicked => { root.start_oauth("gmail"); } }
ProviderButton { text: @tr("Add Outlook"); icon: @image-url("../icons/brand/outlook.svg"); enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving && root.microsoft_oauth_available; tooltip-text: root.microsoft_oauth_available ? "" : @tr("Add app keys in Sign-in settings to enable this provider."); foreground: root.body-text; muted: root.muted-text; surface: root.surface-raised; hover-surface: root.accent-soft; button-border: root.border-subtle; clicked => { root.start_oauth("microsoft"); } }
ProviderButton { text: @tr("Add mail account"); icon: @image-url("../icons/lucide/outline/mail.svg"); colorize-icon: true; enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; foreground: root.body-text; muted: root.muted-text; surface: root.surface-raised; hover-surface: root.accent-soft; button-border: root.border-subtle; clicked => { root.open_new_mail_account(); } }
if root.oauth_in_progress : Button { height: 44px; text: @tr("Cancel sign-in"); clicked => { root.cancel_oauth(); } }
}
}
if root.oauth_in_progress && root.oauth_authorization_url != "" : OAuthBrowserHelp {
launch-failed: root.oauth_browser_error;
retry => { root.retry_oauth_browser(); }
copy-link => { root.copy_oauth_link(); }
submit-redirect(address) => { root.submit_oauth_redirect(address); }
}
for account[account-index] in root.connected_accounts : drop-target := DropArea {
private property <length> divider-y: account.sync_error != "" ? sync-error.y + sync-error.height + 4px : account-header.height;
private property <bool> has-sent-copy: account.provider != "gmail" && account.mail_protocol == "imap";
private property <bool> has-server-copies: account.mail_protocol == "pop3";
private property <bool> unencrypted: account.incoming_security == "none" || account.smtp_security == "none";
// Preference rows keep their designed gaps below the last select row.
private property <length> profile-y: self.has-sent-copy
? sent-copy-row.y + sent-copy-row.height + (root.compact ? 10px : 8px)
: self.has-server-copies
? server-copies-note.y + server-copies-note.height + (root.compact ? 10px : 8px)
: history-row.y + history-row.height + (root.compact ? 9px : 1px);
height: calendars.y + calendars.height + (root.compact ? 18px : 16px);
can-drop(event) => root.read_account_transfer(event.data) != "" ? DragAction.move : DragAction.none;
dropped(event) => {
let source = root.read_account_transfer(event.data);
if source == "" {
return DragAction.none;
}
root.reorder_account(source, account.id, event.position.y > self.height / 2);
return DragAction.move;
}
OnScreenSurface {
width: parent.width;
height: parent.height;
background: drop-target.has-drag ? root.accent-soft : root.surface;
border-radius: 10px;
border-width: drop-target.has-drag ? 2px : 1px;
border-color: drop-target.has-drag ? root.accent : root.border-subtle;
account-header := HorizontalBox {
x: 0px;
y: 0px;
width: parent.width;
height: (account.sync_error != "" ? 27px : root.compact ? 41px : 37px) + account-name.height + account-email.height + account-state.height;
padding-left: 14px;
padding-right: 16px;
spacing: 12px;
if !root.compact : DragArea {
width: 20px;
data: root.make_account_transfer(account.drag_key);
allow-move: true;
Image { width: LayoutMetrics.icon-size; height: LayoutMetrics.icon-size; source: @image-url("../icons/lucide/outline/list-unordered.svg"); colorize: root.muted-text; image-fit: contain; accessible-role: none; }
}
Rectangle {
width: 44px;
height: 44px;
border-radius: 12px;
background: account.provider == "gmail" ? #fce8e6 : account.provider == "microsoft" ? #e6f2ff : root.accent-soft;
if account.has_avatar : Avatar {
x: 2px; y: 2px;
size: 40px;
source: account.avatar;
has-source: true;
fallback: account.initials;
background-color: #ffffff;
foreground-color: root.accent;
avatar-border-width: 1px;
avatar-border-color: root.border-subtle;
}
if !account.has_avatar : Image {
x: 10px; y: 10px; width: 24px; height: 24px;
source: account.provider == "gmail" ? @image-url("../icons/brand/gmail.svg") : account.provider == "microsoft" ? @image-url("../icons/brand/outlook.svg") : @image-url("../icons/lucide/outline/mail.svg");
colorize: account.provider == "generic" ? root.accent : transparent;
image-fit: contain;
accessible-role: none;
}
}
// Names and addresses elide before the account's controls
// can push past the card.
VerticalBox {
horizontal-stretch: 1;
min-width: root.compact ? 0px : 120px;
spacing: 2px;
account-name := Text { height: max(19px, self.preferred-height); text: account.name; color: root.heading; font-size: TypeScale.body-large; font-weight: 700; overflow: elide; }
account-email := Text { height: max(16px, self.preferred-height); text: account.email; color: root.secondary-text; font-size: TypeScale.caption; overflow: elide; }
account-state := Text { height: max(16px, self.preferred-height); text: account.status == "offline" ? @tr("Offline · retrying automatically") : account.status == "needs_reauth" ? @tr("Sign-in required") : account.status == "syncing" ? @tr("Syncing…") : account.status == "idle" ? (account.mail_protocol == "gmail" ? @tr("Connected · Gmail") : account.mail_protocol == "jmap" ? @tr("Connected · JMAP") : account.mail_protocol == "pop3" ? (drop-target.unencrypted ? @tr("Connected without encryption · POP3 + SMTP") : @tr("Connected · POP3 + SMTP")) : drop-target.unencrypted ? @tr("Connected without encryption · IMAP + SMTP") : @tr("Connected · IMAP + SMTP")) : @tr("Account status: {}", account.status); color: account.status == "needs_reauth" ? #c0392b : account.status == "idle" && drop-target.unencrypted ? AppTheme.destructive : root.faint-text; font-size: TypeScale.caption; overflow: elide; }
}
AccountActions {
edit-label: account.provider == "generic" ? @tr("Edit") : @tr("Reconnect");
reconnect: account.provider != "generic";
edit-enabled: !root.account_setup_in_progress && (account.provider == "generic" || !root.oauth_in_progress) && !root.oauth_settings_saving;
remove-enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving;
sync-enabled: !root.account_setup_in_progress && !root.sync_in_progress && !root.oauth_settings_saving;
edit => { root.edit-account(account); }
remove => { root.confirm-account-removal(account); }
sync => { root.sync_account(account.id); }
width: 108px;
}
// The switch row shares the buttons' 30px band and grows
// to fit its label.
if !root.compact && account.carddav_connected : HorizontalBox {
width: max(122px, self.preferred-width);
height: 30px;
padding-top: 0px;
padding-bottom: 0px;
spacing: 8px;
cross-axis-alignment: center;
Text { text: @tr("Contacts"); color: root.body-text; font-size: TypeScale.caption; vertical-alignment: center; }
Rectangle { horizontal-stretch: 1; }
Switch { checked: account.carddav_enabled; toggled => { root.set_account_carddav_enabled(account.id, self.checked); } }
}
if !root.compact : Button {
width: max(98px, self.content-width);
height: 30px;
text: account.carddav_connected ? (account.carddav_status == "" ? @tr("Manage") : @tr("Fix CardDAV")) : @tr("CardDAV");
clicked => {
root.carddav_account_id = account.id;
root.carddav_url = account.carddav_url;
root.carddav_username = account.carddav_username;
root.carddav_password = "";
root.carddav_manage_existing = account.carddav_connected;
}
}
}
sync-error := CopyableDiagnostic {
visible: account.sync_error != "";
x: root.compact ? 24px : 32px;
y: account-header.height + 2px;
width: parent.width - (root.compact ? 48px : 64px);
text: account.sync_error == "" ? "" : @tr("Mail synchronization error: {}", account.sync_error);
text-color: AppTheme.destructive;
font-size: TypeScale.caption;
}
Rectangle {
x: 8px;
y: drop-target.divider-y;
width: parent.width - 16px;
height: 1px;
background: root.border-subtle;
}
history-row := SettingsSelectRow {
x: root.compact ? 24px : 32px;
y: drop-target.divider-y + (root.compact ? 9px : 7px);
width: parent.width - (root.compact ? 48px : 64px);
compact: root.compact;
title: @tr("Mail history");
compact-description-height: 51px;
description: account.mail_history == "all"
? @tr("Download the full mailbox. Large accounts can take considerable time and storage.")
: @tr("How far back to download. Mail already stored on this device is kept.");
choices: root.mail-history-options;
selected-index: root.mail-history-index(account.mail_history);
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-mail-history(account.id, index); }
}
sent-copy-row := SettingsSelectRow {
visible: drop-target.has-sent-copy;
x: root.compact ? 24px : 32px;
y: history-row.y + history-row.height + (root.compact ? 7px : 5px);
width: parent.width - (root.compact ? 48px : 64px);
compact: root.compact;
title: @tr("Sent copies");
compact-description-height: 51px;
description: @tr("Choose who saves sent mail. Use Flectar if your server does not save a copy.");
choices: [@tr("Provider default"), @tr("Server saves a copy"), @tr("Flectar saves a copy")];
selected-index: account.sent_copy_policy == "server" ? 1 : account.sent_copy_policy == "client" ? 2 : 0;
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => {
root.save_account_sent_copy_policy(account.id, index == 1 ? "server" : index == 2 ? "client" : "auto");
}
}
server-copies-row := SettingsSelectRow {
visible: drop-target.has-server-copies;
x: root.compact ? 24px : 32px;
y: history-row.y + history-row.height + (root.compact ? 7px : 5px);
width: parent.width - (root.compact ? 48px : 64px);
compact: root.compact;
title: @tr("Keep mail on the server");
compact-description-height: 51px;
description: account.keep_on_server == "untilDeleted" ? @tr("Mail you delete here is removed from the server too.")
: account.keep_on_server == "untilDownloaded" ? @tr("Mail is removed from the server once it's on this device.")
: account.keep_on_server == "always" ? @tr("Flectar Mail never removes mail from the server.")
: @tr("Mail is removed after this time, or once deleted here.");
choices: root.keep-on-server-options;
selected-index: root.keep-on-server-index(account.keep_on_server);
title-color: root.body-text;
description-color: root.faint-text;
selected(index, value) => { root.select-keep-on-server(account.id, index); }
}
// The server's own rule wins over the account's choice.
server-rule := Text {
visible: drop-target.has-server-copies && root.server-removes-sooner(account);
// Aligned with the row's own texts.
x: server-copies-row.x + (root.compact ? 8px : 16px);
y: server-copies-row.y + server-copies-row.height + (self.visible ? 4px : 0px);
width: server-copies-row.width - (root.compact ? 16px : 32px);
height: self.visible ? self.preferred-height : 0px;
text: account.server_keeps_mail_days == 0
? @tr("This server removes mail as soon as it's downloaded.")
: @tr("This server removes downloaded mail after one day." | "This server removes downloaded mail after {n} days." % account.server_keeps_mail_days);
color: AppTheme.destructive;
font-size: TypeScale.caption;
wrap: word-wrap;
}
server-copies-note := Text {
visible: drop-target.has-server-copies && (account.keep_on_server != "always" || root.server-removes-sooner(account));
x: server-rule.x;
y: server-rule.y + server-rule.height + (self.visible ? 4px : 0px);
width: server-rule.width;
height: self.visible ? self.preferred-height : 0px;
text: @tr("Mail removed from the server is kept only on this device. Database snapshots in Storage include it.");
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
profile-row := Rectangle {
x: root.compact ? 24px : 32px;
y: drop-target.profile-y;
width: parent.width - (root.compact ? 48px : 64px);
height: 12px + profile-label.height + profile-description.height;
background: transparent;
profile-label := Text {
x: 0px; y: 4px;
width: parent.width - 146px; height: max(20px, self.preferred-height);
text: @tr("Profile");
color: root.body-text;
font-size: TypeScale.body;
font-weight: 600;
vertical-alignment: center;
}
profile-description := Text {
x: 0px; y: profile-label.y + profile-label.height + 1px;
width: parent.width - 146px; height: max(18px, self.preferred-height);
text: account.has_profile
? @tr("Uses the {} color in the mail list", account.profile_name)
: account.has_account_color
? @tr("Uses a custom account color")
: @tr("Uses a generated account color");
color: root.faint-text;
font-size: TypeScale.caption;
overflow: elide;
vertical-alignment: center;
}
Button {
x: parent.width - self.width;
y: 8px;
width: 136px;
height: 34px;
text: account.has_profile ? account.profile_name : @tr("No profile");
clicked => { root.open-account-profile-picker(account, account-index); }
}
Rectangle {
x: parent.width - 126px;
y: 20px;
width: 10px; height: 10px;
border-radius: 5px;
background: account.profile_color;
}
}
calendars := AccountCalendars {
x: root.compact ? 24px : 32px;
y: profile-row.y + profile-row.height + (root.compact ? 14px : 12px);
width: parent.width - (root.compact ? 48px : 64px);
height: self.preferred-height;
account: account;
compact: root.compact;
provider-actions-enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving;
title-color: root.body-text;
description-color: root.faint-text;
line-color: root.border-subtle;
connect-provider => { root.connect_account_calendar(account.id); }
add-caldav => { root.add-caldav(account); }
edit(connection) => { root.edit-caldav(account, connection); }
remove(connection) => { root.confirm-calendar-removal(account, connection); }
set-enabled(connection-id, enabled) => { root.set_calendar_connection_enabled(connection-id, enabled); }
}
}
}
AccountMailPreferencesPanel { accounts: root.connected_accounts; compact: root.compact; }
Rectangle {
height: root.custom_oauth_open ? oauth-fields.y + oauth-fields.height + 8px : oauth-card.content-y + 60px;
oauth-card := SettingsCard { title: @tr("OAuth app credentials"); description: root.custom_oauth_configured ? @tr("Custom desktop app credentials are configured.") : @tr("Advanced: use your own Gmail or Microsoft desktop app registration."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
Button { x: parent.width - self.width - 20px; y: oauth-card.content-y + 12px; width: 110px; height: 32px; enabled: !root.oauth_settings_saving; text: root.custom_oauth_open ? @tr("Hide") : @tr("Configure"); clicked => { root.custom_oauth_open = !root.custom_oauth_open; } }
oauth-fields := VerticalBox {
x: 20px; y: oauth-card.content-y + 58px; width: parent.width - 40px; height: self.preferred-height; spacing: 8px;
if root.custom_oauth_open && !root.compact : HorizontalBox { spacing: 8px; LineEdit { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text <=> root.google_client_id; placeholder-text: @tr("Gmail desktop client ID"); } LineEdit { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text <=> root.google_client_secret; input-type: password; placeholder-text: @tr("Gmail client secret (optional)"); } Button { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text: @tr("Save Gmail"); clicked => { root.save_oauth_app("gmail", root.google_client_id, root.google_client_secret); } } }
if root.custom_oauth_open && root.compact : VerticalBox { spacing: 8px; LineEdit { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text <=> root.google_client_id; placeholder-text: @tr("Gmail desktop client ID"); } LineEdit { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text <=> root.google_client_secret; input-type: password; placeholder-text: @tr("Gmail client secret (optional)"); } Button { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text: @tr("Save Gmail settings"); clicked => { root.save_oauth_app("gmail", root.google_client_id, root.google_client_secret); } } }
if root.custom_oauth_open : HorizontalBox { spacing: 8px; LineEdit { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text <=> root.ms_client_id; placeholder-text: @tr("Microsoft Entra desktop client ID"); } Button { enabled: !root.account_setup_in_progress && !root.oauth_in_progress && !root.oauth_settings_saving; text: @tr("Save Outlook"); clicked => { root.save_oauth_app("microsoft", root.ms_client_id, ""); } } }
if root.custom_oauth_open : Text { text: @tr("Outlook public desktop clients use PKCE and do not need a client secret."); color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
}
}
Rectangle {
visible: root.carddav_account_id > 0;
height: max(206px, carddav-fields.preferred-height);
carddav-card := SettingsCard { title: @tr("Connect CardDAV"); description: @tr("Sync contacts with a standards-compliant CardDAV server."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
carddav-fields := VerticalBox {
padding-top: carddav-card.content-y + 12px; padding-right: 20px; padding-bottom: 16px; padding-left: 20px; spacing: 8px;
LineEdit { text <=> root.carddav_url; placeholder-text: @tr("https://contacts.example.com/"); }
HorizontalBox { spacing: 8px; LineEdit { text <=> root.carddav_username; placeholder-text: @tr("Username"); } LineEdit { text <=> root.carddav_password; input-type: password; placeholder-text: @tr("App password"); } }
HorizontalBox { spacing: 8px; if root.carddav_manage_existing : Button { text: @tr("Disconnect"); clicked => { root.disconnect_carddav(root.carddav_account_id); root.carddav_account_id = -1; root.carddav_password = ""; root.carddav_manage_existing = false; } } Rectangle { horizontal-stretch: 1; } Button { text: @tr("Cancel"); clicked => { root.carddav_account_id = -1; root.carddav_password = ""; root.carddav_manage_existing = false; } } Button { text: root.carddav_manage_existing ? @tr("Reconnect") : @tr("Connect"); primary: true; clicked => { root.connect_carddav(root.carddav_account_id, root.carddav_url, root.carddav_username, root.carddav_password); } } }
}
}
}
}
data-scroll := ScrollView {
visible: root.settings_tab == "Data";
width: parent.width;
height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox {
x: root.compact ? 16px : max(24px, (data-scroll.visible-width - 920px) / 2);
width: root.compact ? data-scroll.visible-width - 32px : min(920px, data-scroll.visible-width - 48px);
padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px;
padding-bottom: 32px;
spacing: 10px;
SettingsPageTitle { label: @tr("Storage"); title-color: root.title-color; }
Text {
height: max(root.compact ? 34px : 20px, self.preferred-height);
text: @tr("See what Flectar Mail stores locally and manage the data on this device.");
color: root.secondary-text;
font-size: TypeScale.secondary;
vertical-alignment: center;
wrap: word-wrap;
}
StorageUsageCard {
compact: root.compact;
loading: root.storage_loading;
loaded: root.storage_loaded;
total: root.storage_total;
error: root.storage_error;
rows: root.storage_usage;
surface: root.surface;
card-border-color: root.border-subtle;
heading: root.heading;
body-text: root.body-text;
secondary-text: root.secondary-text;
refresh => { root.load_storage_usage(); }
}
// Mail imported to this device alone.
if LocalMailUi.account-id >= 0 : Rectangle {
height: local-status.y + local-status.height + (root.compact ? 74px : 22px);
vertical-stretch: 0;
local-card := SettingsCard { title: @tr("Mail on this device"); description: @tr("Mail you imported. It is kept only here, so export it to keep a copy elsewhere."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
Image { x: 20px; y: local-note.y + (local-note.height - self.height) / 2; width: LayoutMetrics.icon-size; height: LayoutMetrics.icon-size; source: @image-url("../icons/lucide/outline/file-directory.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
local-note := Text { x: 48px; y: local-card.content-y + 14px; width: root.compact ? parent.width - 68px : parent.width - 352px; height: max(42px, self.preferred-height); text: @tr("One message" | "{} messages" % LocalMailUi.messages, Numbers.count(LocalMailUi.messages)) + " · " + LocalMailUi.size; color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; vertical-alignment: center; }
local-status := Text { x: 48px; y: local-note.y + local-note.height; width: local-note.width; height: root.mail_export_status == "" ? 0px : max(18px, self.preferred-height); text: root.mail_export_status; color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; }
HorizontalLayout {
x: root.compact ? 20px : parent.width - self.width - 20px;
y: root.compact ? local-status.y + local-status.height + 25px : local-note.y + (local-note.height - self.height) / 2;
width: root.compact ? parent.width - 40px : 264px;
height: 32px;
spacing: 8px;
Button { text: @tr("Export mail…"); icon: @image-url("../icons/lucide/outline/download.svg"); has-icon: true; enabled: !root.mail_export_running; clicked => { root.export_device_mail(LocalMailUi.account-id); } }
Button { text: @tr("Delete…"); icon: @image-url("../icons/lucide/outline/trash.svg"); has-icon: true; enabled: !root.account_setup_in_progress; clicked => { root.delete_local_mail_confirmation = true; } }
}
}
Rectangle {
visible: root.account-transfer-supported;
height: root.account-transfer-supported ? snapshot-note.y + snapshot-note.height + (root.compact ? 74px : 22px) : 0px;
vertical-stretch: 0;
snapshot-card := SettingsCard { title: @tr("Database snapshot"); description: @tr("Export verified copies of the mail and calendar databases with their schema versions."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
Image { x: 20px; y: snapshot-note.y + (snapshot-note.height - self.height) / 2; width: LayoutMetrics.icon-size; height: LayoutMetrics.icon-size; source: @image-url("../icons/lucide/outline/database.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
snapshot-note := Text { x: 48px; y: snapshot-card.content-y + 14px; width: root.compact ? parent.width - 68px : snapshot-button.x - 70px; height: max(42px, self.preferred-height); text: @tr("The snapshot contains private local mail and settings, but not passwords or OAuth tokens. Store it securely."); color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; vertical-alignment: center; }
snapshot-button := Button { x: root.compact ? 20px : parent.width - self.width - 20px; y: root.compact ? snapshot-note.y + snapshot-note.height + 25px : snapshot-note.y + (snapshot-note.height - self.height) / 2; width: root.compact ? parent.width - 40px : max(170px, self.content-width); text: @tr("Export snapshot"); icon: @image-url("../icons/lucide/outline/download.svg"); has-icon: true; clicked => { root.export_database_snapshot(); } }
}
Rectangle {
visible: root.account-transfer-supported;
height: root.account-transfer-supported ? transfer-note.y + transfer-note.height + (root.compact ? 74px : 22px) : 0px;
vertical-stretch: 0;
transfer-card := SettingsCard { title: @tr("Transfer account setup"); description: @tr("Import accounts and mail from Thunderbird, K-9 Mail, mail files, or a backup, or export one connected account and your preferences." | "Import accounts and mail from Thunderbird, K-9 Mail, mail files, or a backup, or export {n} connected accounts and your preferences." % root.connected_accounts.length); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
Image { x: 20px; y: transfer-note.y + (transfer-note.height - self.height) / 2; width: LayoutMetrics.icon-size; height: LayoutMetrics.icon-size; source: @image-url("../icons/lucide/outline/check-circle.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
transfer-note := Text { x: 48px; y: transfer-card.content-y + 14px; width: root.compact ? parent.width - 68px : parent.width - 352px; height: max(42px, self.preferred-height); text: @tr("Passwords and OAuth tokens stay in the system keyring. A restored device asks you to sign in securely."); color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; vertical-alignment: center; }
// Unpadded, so the buttons line up with the other cards'.
HorizontalLayout {
x: root.compact ? 20px : parent.width - self.width - 20px;
y: root.compact ? transfer-note.y + transfer-note.height + 25px : transfer-note.y + (transfer-note.height - self.height) / 2;
width: root.compact ? parent.width - 40px : 264px;
height: 32px;
spacing: 8px;
Button { text: @tr("Import"); icon: @image-url("../icons/lucide/outline/download.svg"); has-icon: true; clicked => { root.import_accounts(); } }
Button { text: @tr("Export"); icon: @image-url("../icons/lucide/outline/download.svg"); has-icon: true; primary: true; enabled: root.connected_accounts.length > 0; clicked => { root.export_account_backup(); } }
}
}
Rectangle {
height: delete-note.y + delete-note.height + (root.compact ? 74px : 22px);
vertical-stretch: 0;
delete-card := SettingsCard { title: @tr("Delete all local data"); description: @tr("Remove accounts, downloaded mail, calendars, contacts, preferences, and credentials from this device."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
Image { x: 20px; y: delete-note.y + (delete-note.height - self.height) / 2; width: LayoutMetrics.icon-size; height: LayoutMetrics.icon-size; source: @image-url("../icons/lucide/outline/trash.svg"); colorize: #d04a43; image-fit: contain; accessible-role: none; }
delete-note := Text { x: 48px; y: delete-card.content-y + 14px; width: root.compact ? parent.width - 68px : delete-button.x - 70px; height: max(42px, self.preferred-height); text: LocalMailUi.account-id >= 0 ? @tr("Imported mail and the mail of any POP3 account are stored only on this device. Nothing is deleted from Gmail, Outlook, or IMAP servers.") : root.has_device_mail ? @tr("POP3 mail is stored only on this device. Nothing is deleted from Gmail, Outlook, or IMAP servers.") : @tr("This does not delete anything from Gmail, Outlook, or your IMAP server."); color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; vertical-alignment: center; }
delete-button := Button { x: root.compact ? 20px : parent.width - self.width - 20px; y: root.compact ? delete-note.y + delete-note.height + 25px : delete-note.y + (delete-note.height - self.height) / 2; width: root.compact ? parent.width - 40px : max(170px, self.content-width); text: @tr("Delete everything"); icon: @image-url("../icons/lucide/outline/trash.svg"); has-icon: true; clicked => { root.delete_data_confirmation = true; } }
}
if root.sync_status != "" : Text { height: max(22px, self.preferred-height); text: root.sync_status; color: root.secondary-text; font-size: TypeScale.caption; vertical-alignment: center; overflow: elide; }
}
}
notifications-scroll := ScrollView {
visible: root.settings_tab == "Notifications";
width: parent.width; height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox { x: root.compact ? 16px : max(24px, (notifications-scroll.visible-width - 920px) / 2); width: root.compact ? notifications-scroll.visible-width - 32px : min(920px, notifications-scroll.visible-width - 48px); padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px; padding-bottom: 32px; spacing: 10px;
SettingsPageTitle { label: @tr("Notifications"); title-color: root.title-color; }
Text { height: max(20px, self.preferred-height); text: @tr("Choose how Flectar Mail gets your attention."); color: root.secondary-text; font-size: TypeScale.secondary; vertical-alignment: center; wrap: word-wrap; }
Rectangle {
height: max(root.compact ? 228px : 152px, self.preferred-height);
vertical-stretch: 0;
background: root.surface;
border-radius: 8px;
border-width: 1px;
border-color: root.border-subtle;
VerticalBox {
padding-top: 14px;
padding-right: 18px;
padding-bottom: 14px;
padding-left: 18px;
spacing: 4px;
SettingsSelectRow {
compact: root.compact;
title: root.notifications-title;
description: @tr("Choose which incoming messages can alert you.");
choices: root.notification-options;
selected-index: root.notification-index;
title-color: root.heading;
description-color: root.faint-text;
selected(index, value) => { root.select-notification-mode(index); }
}
// The system has the last word: say so while it shows nothing.
// Laid out like the row above, so the texts and the right edge line up.
if root.notifications-blocked && !root.compact : HorizontalBox {
padding-top: 0px;
spacing: 20px;
VerticalBox {
padding-top: 0px;
padding-bottom: 0px;
horizontal-stretch: 1;
blocked-text := Text {
text: root.notifications-blocked-text;
color: root.faint-text;
font-size: TypeScale.caption;
vertical-alignment: center;
wrap: word-wrap;
}
}
if root.notifications-settings-offered : Button {
width: max(132px, self.content-width);
height: 32px;
text: @tr("Open system settings");
clicked => { SystemNotifications.open-settings(); }
}
}
if root.notifications-blocked && root.compact : VerticalBox {
padding: 8px;
spacing: 7px;
Text {
text: root.notifications-blocked-text;
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
if root.notifications-settings-offered : Button {
height: 36px;
text: @tr("Open system settings");
clicked => { SystemNotifications.open-settings(); }
}
}
// When new mail can show, where the system decides.
if root.background-checks-noted && !root.compact : HorizontalBox {
padding-top: 0px;
spacing: 20px;
VerticalBox {
padding-top: 0px;
padding-bottom: 0px;
horizontal-stretch: 1;
Text {
text: root.background-checks-text;
color: root.faint-text;
font-size: TypeScale.caption;
vertical-alignment: center;
wrap: word-wrap;
}
}
if SystemNotifications.background-checks == "off" : Button {
width: max(132px, self.content-width);
height: 32px;
text: @tr("Open system settings");
clicked => { SystemNotifications.open-app-settings(); }
}
}
if root.background-checks-noted && root.compact : VerticalBox {
padding: 8px;
spacing: 7px;
Text {
text: root.background-checks-text;
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
if SystemNotifications.background-checks == "off" : Button {
height: 36px;
text: @tr("Open system settings");
clicked => { SystemNotifications.open-app-settings(); }
}
}
Rectangle { height: 1px; background: root.border-subtle; }
SettingsSwitchRow {
compact: root.compact;
title: @tr("Notification sounds");
description: SystemNotifications.sound-fixed
? @tr("Your desktop decides whether notifications play a sound.")
: @tr("Play a sound with new mail notifications.");
checked: root.notification_sound_enabled;
enabled: !SystemNotifications.sound-fixed;
title-color: root.heading;
description-color: root.faint-text;
toggled(checked) => {
root.notification_sound_enabled = checked;
root.save_notification_settings(root.notifications_enabled, checked, root.notification_scope);
}
}
if SystemNotifications.badge-supported : Rectangle { height: 1px; background: root.border-subtle; }
if SystemNotifications.badge-supported : SettingsSwitchRow {
compact: root.compact;
title: @tr("Unread count on the app icon");
// It counts what notifications cover; docks on Linux
// show it only where they support counts.
description: WindowChrome.platform == "linux"
? (root.notification_scope == "all"
? @tr("Show how many unread messages are in your Inbox, on docks that show counts.")
: @tr("Show how many unread Important messages are in your Inbox, on docks that show counts."))
: (root.notification_scope == "all"
? @tr("Show how many unread messages are in your Inbox.")
: @tr("Show how many unread Important messages are in your Inbox."));
checked: root.unread_badge_enabled;
title-color: root.heading;
description-color: root.faint-text;
toggled(checked) => {
root.unread_badge_enabled = checked;
root.save_unread_badge(checked);
}
}
}
}
}
}
shortcuts-scroll := ScrollView {
visible: root.settings_tab == "Shortcuts";
width: parent.width; height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox { x: root.compact ? 16px : max(24px, (shortcuts-scroll.visible-width - 920px) / 2); width: root.compact ? shortcuts-scroll.visible-width - 32px : min(920px, shortcuts-scroll.visible-width - 48px); padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px; padding-bottom: 32px; spacing: 10px;
SettingsPageTitle { label: @tr("Keyboard shortcuts"); title-color: root.title-color; }
Text { height: max(20px, self.preferred-height); text: @tr("Move through mail without leaving the keyboard."); color: root.secondary-text; font-size: TypeScale.secondary; vertical-alignment: center; wrap: word-wrap; }
ShortcutSection { vertical-stretch: 0; title: @tr("Mail"); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading;
ShortcutRow { title: @tr("Compose a new message"); keys: [Shortcuts.compose]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Edit a message as new"); keys: [Shortcuts.edit-as-new]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Take back a message just sent"); keys: [Shortcuts.undo]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Check for new mail"); keys: [Shortcuts.check-mail]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Check every account for new mail"); keys: KeyboardKeys.apple ? [Shortcuts.check-all-mail, Shortcuts.check-all-mail-alternate] : [Shortcuts.check-all-mail]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Search mail"); keys: [Shortcuts.search, Shortcuts.search-alternate]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Archive"); keys: [Shortcuts.archive]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Move to a folder"); keys: [Shortcuts.move-to]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Copy to a folder"); keys: [Shortcuts.copy-to]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Move to spam"); keys: [Shortcuts.spam]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Move to trash"); keys: [Shortcuts.trash]; title-color: root.body-text; separator-color: root.faint-text; }
}
ShortcutSection { vertical-stretch: 0; title: @tr("Navigation"); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading;
ShortcutRow { title: @tr("Move between messages"); keys: [Shortcuts.previous-message, Shortcuts.next-message]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Open message"); keys: [Shortcuts.open-message]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Select message"); keys: [Shortcuts.select-message]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Switch between list and message"); keys: [Shortcuts.switch-pane]; title-color: root.body-text; separator-color: root.faint-text; }
}
ShortcutSection { vertical-stretch: 0; title: @tr("Reading"); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading;
ShortcutRow { title: @tr("Find in message"); keys: [Shortcuts.find]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Zoom in"); keys: [Shortcuts.zoom-in]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Zoom out"); keys: [Shortcuts.zoom-out]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Reset zoom"); keys: [Shortcuts.zoom-reset]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Caret browsing"); keys: [Shortcuts.caret-browsing]; title-color: root.body-text; separator-color: root.faint-text; }
}
if TabsUi.enabled : ShortcutSection { vertical-stretch: 0; title: @tr("Tabs"); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading;
ShortcutRow { title: @tr("New tab"); keys: [Shortcuts.new-tab]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Close tab"); keys: [Shortcuts.close-tab]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Reopen closed tab"); keys: [Shortcuts.reopen-tab]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Next tab"); keys: [Shortcuts.next-tab]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Previous tab"); keys: [Shortcuts.previous-tab]; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Go to tab 1–8"); keys: [Shortcuts.first-tab, Shortcuts.eighth-tab]; separator: "–"; title-color: root.body-text; separator-color: root.faint-text; }
ShortcutRow { title: @tr("Go to last tab"); keys: [Shortcuts.last-tab]; title-color: root.body-text; separator-color: root.faint-text; }
}
if WindowChrome.platform == "linux" : ShortcutSection { vertical-stretch: 0; title: @tr("General"); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading;
ShortcutRow { title: @tr("Quit Flectar Mail"); keys: [Shortcuts.quit]; title-color: root.body-text; separator-color: root.faint-text; }
}
}
}
services-scroll := ScrollView {
visible: root.settings_tab == "Services";
width: parent.width;
height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox {
x: root.compact ? 16px : max(24px, (services-scroll.visible-width - 920px) / 2);
width: root.compact ? services-scroll.visible-width - 32px : min(920px, services-scroll.visible-width - 48px);
padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px;
padding-bottom: 32px;
spacing: 12px;
SettingsPageTitle { label: @tr("Services"); title-color: root.title-color; }
Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Optional paid services from Flectar. Flectar Mail stays free and works without them."); color: root.secondary-text; font-size: TypeScale.secondary; vertical-alignment: center; wrap: word-wrap; }
Rectangle {
height: account-note.y + account-note.height + (root.compact ? 70px : 22px);
vertical-stretch: 0;
account-card := SettingsCard { title: @tr("Flectar account"); description: @tr("Not signed in"); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
Image { x: 20px; y: account-note.y + (account-note.height - self.height) / 2; width: LayoutMetrics.icon-size; height: LayoutMetrics.icon-size; source: @image-url("../icons/lucide/outline/lock.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
account-note := Text { x: 48px; y: account-card.content-y + 14px; width: root.compact ? parent.width - 68px : sign-in-button.x - 70px; height: max(root.compact ? 68px : 42px, self.preferred-height); text: @tr("A Flectar account is only needed for these optional services. Learn more at flectar.com/mail."); color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; vertical-alignment: center; }
sign-in-button := Button {
x: root.compact ? 20px : parent.width - self.width - 20px;
y: root.compact ? account-note.y + account-note.height + 18px : account-note.y + (account-note.height - self.height) / 2;
width: root.compact ? parent.width - 40px : max(210px, self.content-width);
text: root.flectar-services-available ? @tr("Sign in with Flectar") : @tr("Sign-in coming soon");
icon: @image-url("../icons/lucide/outline/browser.svg");
has-icon: true;
primary: root.flectar-services-available;
enabled: root.flectar-services-available;
clicked => { root.start_flectar_sign_in(); }
}
}
ServicePreviewCard {
compact: root.compact;
icon: @image-url("../icons/lucide/filled/cloud-sync.svg");
title: @tr("Sync");
tagline: @tr("Your setup on every device");
description: @tr("Sign in on a new phone or computer and your accounts, calendars, contacts, and preferences are ready without setting them up again.");
features: [
@tr("Mail accounts, profiles, and signatures"),
@tr("Calendar and contact connections"),
@tr("Themes, layout, and notification preferences"),
@tr("Account credentials, encrypted on this device before upload"),
];
note: @tr("Your messages stay with your mail provider. Sync only carries your Flectar Mail setup.");
surface: root.surface;
card-border-color: root.border-subtle;
heading: root.heading;
body-text: root.body-text;
secondary-text: root.secondary-text;
faint-text: root.faint-text;
accent: ServiceIconPalette.sync;
accent-soft: ServiceIconPalette.sync.with-alpha(AppTheme.dark ? 0.16 : 0.10);
learn-more => { root.open-link("https://flectar.com/mail"); }
}
ServicePreviewCard {
compact: root.compact;
icon: @image-url("../icons/lucide/filled/building.svg");
title: @tr("Business email");
tagline: @tr("Mailboxes on your own domain");
description: @tr("Professional addresses such as you@yourcompany.com, hosted by Flectar.");
features: [
@tr("Connect a domain you already own"),
@tr("Create and manage mailboxes for your team"),
@tr("New mailboxes appear in Flectar Mail automatically"),
];
note: @tr("Billed per mailbox.");
surface: root.surface;
card-border-color: root.border-subtle;
heading: root.heading;
body-text: root.body-text;
secondary-text: root.secondary-text;
faint-text: root.faint-text;
accent: ServiceIconPalette.business;
accent-soft: ServiceIconPalette.business.with-alpha(AppTheme.dark ? 0.16 : 0.10);
learn-more => { root.open-link("https://flectar.com/mail"); }
}
Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("These services are in development. Plans and availability will be announced on flectar.com/mail."); color: root.faint-text; font-size: TypeScale.caption; vertical-alignment: center; wrap: word-wrap; }
}
}
about-scroll := ScrollView {
visible: root.settings_tab == "About";
width: parent.width;
height: parent.height;
mouse-drag-pan-enabled: root.compact;
content-width: self.visible-width;
VerticalBox {
x: root.compact ? 16px : max(24px, (about-scroll.visible-width - 920px) / 2);
width: root.compact ? about-scroll.visible-width - 32px : min(920px, about-scroll.visible-width - 48px);
padding-top: root.compact ? LayoutMetrics.compact-header-height + 16px : 28px;
padding-bottom: 32px;
spacing: 14px;
SettingsPageTitle { label: @tr("About"); title-color: root.title-color; }
Text { height: max(root.compact ? 34px : 17px, self.preferred-height); text: @tr("Flectar Mail, its makers, and its community."); color: root.secondary-text; font-size: TypeScale.secondary; vertical-alignment: center; wrap: word-wrap; }
Rectangle {
height: about-version.y + about-version.height + (root.compact ? 31px : 18px);
background: root.surface;
border-radius: 10px;
border-width: 1px;
border-color: root.border-subtle;
Image {
x: (parent.width - self.width) / 2;
y: 24px;
width: 62px;
height: 62px;
source: AppIcon.logo(AppIcon.variant);
image-fit: contain;
accessible-label: @tr("Flectar Mail logo");
}
about-name := Text {
x: 20px;
y: 104px;
width: parent.width - 40px;
height: max(28px, self.preferred-height);
text: @tr("Flectar Mail");
color: root.title-color;
font-size: TypeScale.display-large;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
letter-spacing: -0.6px;
}
about-tagline := Text {
x: 20px;
y: about-name.y + about-name.height + 5px;
width: parent.width - 40px;
height: max(root.compact ? 34px : 17px, self.preferred-height);
text: @tr("A native mail client for a calm, private inbox.");
color: root.secondary-text;
font-size: TypeScale.secondary;
horizontal-alignment: center;
vertical-alignment: center;
wrap: word-wrap;
}
about-version := Text {
x: 20px;
y: about-tagline.y + about-tagline.height + (root.compact ? 10px : 16px);
width: parent.width - 40px;
height: max(16px, self.preferred-height);
text: @tr("Version {}", root.app-version);
color: root.faint-text;
font-size: TypeScale.caption;
horizontal-alignment: center;
vertical-alignment: center;
}
}
if Updates.offered : Rectangle {
height: updates-rows.y + updates-rows.height + 10px;
vertical-stretch: 0;
updates-card := SettingsCard { title: @tr("Updates"); description: @tr("Flectar Mail looks for new versions once a day."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
updates-rows := VerticalBox {
x: 20px;
y: updates-card.content-y + 8px;
width: parent.width - 40px;
height: self.preferred-height;
padding-bottom: 8px;
spacing: 4px;
SettingsSwitchRow {
compact: root.compact;
title: @tr("Check for new versions");
description: @tr("It asks GitHub, without any account or mail details.");
checked: Updates.enabled;
title-color: root.heading;
description-color: root.faint-text;
toggled(checked) => {
Updates.enabled = checked;
Updates.set-enabled(checked);
}
}
if Updates.enabled : Rectangle { height: 1px; background: root.border-subtle; }
// Laid out like the row above, so the texts and the right
// edge line up.
if Updates.enabled && !root.compact : HorizontalBox {
padding-top: 6px;
spacing: 20px;
VerticalBox {
padding-top: 0px;
padding-bottom: 0px;
spacing: 2px;
horizontal-stretch: 1;
alignment: center;
Text {
height: max(17px, self.preferred-height);
text: root.update-status;
color: root.heading;
font-size: TypeScale.secondary;
font-weight: Updates.version != "" ? 600 : 400;
wrap: word-wrap;
}
if Updates.version != "" && Updates.summary != "" : Text {
height: max(16px, self.preferred-height);
text: Updates.summary;
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
}
if Updates.version != "" : Button {
width: max(112px, self.content-width);
height: 32px;
text: @tr("Download");
primary: true;
clicked => { Updates.open-release(); }
}
Button {
width: max(112px, self.content-width);
height: 32px;
text: @tr("Check now");
enabled: !Updates.checking;
clicked => { Updates.check-now(); }
}
}
if Updates.enabled && root.compact : VerticalBox {
padding: 8px;
spacing: 7px;
Text {
height: max(17px, self.preferred-height);
text: root.update-status;
color: root.heading;
font-size: TypeScale.secondary;
font-weight: Updates.version != "" ? 600 : 400;
wrap: word-wrap;
}
if Updates.version != "" && Updates.summary != "" : Text {
height: max(16px, self.preferred-height);
text: Updates.summary;
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
if Updates.version != "" : Button {
height: 36px;
text: @tr("Download");
primary: true;
clicked => { Updates.open-release(); }
}
Button {
height: 36px;
text: @tr("Check now");
enabled: !Updates.checking;
clicked => { Updates.check-now(); }
}
}
}
}
Rectangle {
height: credits-links.y + credits-links.height + 10px;
vertical-stretch: 0;
credits-card := SettingsCard { title: @tr("Credits"); description: @tr("Built by Flectar with help from the open-source community."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
credits-links := VerticalBox {
x: 20px;
y: credits-card.content-y + 8px;
width: parent.width - 40px;
height: self.preferred-height;
padding-bottom: 0px;
spacing: 0px;
SettingsLinkRow {
text: @tr("Flectar");
description: @tr("Company website");
icon: @image-url("../../resources/app-icon/flectar-mail-masked.svg");
colorize-icon: false;
accent: root.accent;
body-text: root.body-text;
muted-text: root.faint-text;
hover-background: root.accent-soft;
clicked => { root.open-link("https://flectar.com"); }
}
SettingsLinkRow {
text: @tr("Open-source contributors");
description: @tr("Meet the people who have contributed on GitHub");
icon: @image-url("../icons/lucide/outline/people.svg");
accent: root.accent;
body-text: root.body-text;
muted-text: root.faint-text;
hover-background: root.accent-soft;
clicked => { root.open-link("https://github.com/flectar/mail/graphs/contributors"); }
}
}
}
Rectangle {
height: privacy-links.y + privacy-links.height + 10px;
vertical-stretch: 0;
privacy-card := SettingsCard { title: @tr("Privacy"); description: @tr("How Flectar Mail handles your data."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
privacy-links := VerticalBox {
x: 20px;
y: privacy-card.content-y + 8px;
width: parent.width - 40px;
height: self.preferred-height;
padding-bottom: 0px;
spacing: 0px;
SettingsLinkRow {
text: @tr("Privacy policy");
description: @tr("Read it on flectar.com");
icon: @image-url("../icons/lucide/outline/shield.svg");
accent: root.accent;
body-text: root.body-text;
muted-text: root.faint-text;
hover-background: root.accent-soft;
clicked => { root.open-link(root.privacy-policy-url); }
}
}
}
Rectangle {
height: license-notice.y + license-notice.height + (root.compact ? 50px : 38px);
license-card := SettingsCard { title: @tr("License"); description: root.compact ? @tr("AGPL-3.0-only.") : @tr("Flectar Mail is released under the GNU Affero General Public License v3.0 only."); surface: root.surface; card-border-color: root.border-subtle; heading: root.heading; secondary-text: root.faint-text; }
license-notice := Text {
x: 20px;
y: license-card.content-y + 18px;
width: parent.width - 40px;
height: max(root.compact ? 38px : 18px, self.preferred-height);
text: @tr("Third-party components retain their own licenses and notices.");
color: root.secondary-text;
font-size: TypeScale.caption;
vertical-alignment: center;
wrap: word-wrap;
}
}
}
}
}
}
CompactPageHeader {
visible: root.compact;
x: 0px;
y: 0px;
width: parent.width;
z: 200;
title: root.settings_tab == "General" ? @tr("General")
: root.settings_tab == "Accounts" ? @tr("Accounts")
: root.settings_tab == "Contacts" ? @tr("Contacts")
: root.settings_tab == "Notifications" ? @tr("Notifications")
: root.settings_tab == "Shortcuts" ? @tr("Shortcuts")
: root.settings_tab == "Data" ? @tr("Storage")
: root.settings_tab == "Services" ? @tr("Services") : @tr("About");
show-leading: true;
leading-icon: @image-url("../icons/lucide/filled/sliders.svg");
leading-tooltip: @tr("Settings sections");
leading-accessible-id: "settings-menu-button";
background-color: root.sidebar-bg;
leading-clicked => { root.compact-nav-open = true; }
if TabsUi.enabled && TabsUi.compact : TabSwitcherButton { compact: true; }
IconButton {
width: LayoutMetrics.touch-target;
height: LayoutMetrics.touch-target;
icon: @image-url("../icons/lucide/outline/x.svg");
icon-color: root.secondary-text;
idle-background: transparent;
hover-background: root.accent-soft;
tooltip-text: @tr("Close settings");
accessible-id: "settings-close-button";
clicked => {
root.compact-nav-open = false;
root.settings_open = false;
}
}
}
Rectangle {
visible: root.compact && (root.compact-nav-open || self.opacity > 0);
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
z: 209;
background: #00000055;
opacity: root.compact-nav-open ? 1 : 0;
animate opacity { duration: 180ms; easing: ease-out; }
TouchArea {
enabled: root.compact-nav-open;
accessible-role: button;
accessible-label: @tr("Close settings sections");
clicked => { root.compact-nav-open = false; }
}
}
Rectangle {
visible: root.compact && (root.compact-nav-open || self.x > -self.width - 11px);
x: root.compact-nav-open ? 0px : -self.width - 12px;
y: 0px;
width: min(310px, parent.width - 48px);
height: parent.height;
z: 210;
background: root.sidebar-bg;
opacity: root.compact-nav-open ? 1 : 0.92;
drop-shadow-blur: root.compact-nav-open ? 24px : 0px;
drop-shadow-offset-x: 6px;
drop-shadow-color: #00000035;
animate x { duration: 260ms; easing: spring(0.08); }
animate opacity { duration: 180ms; easing: ease-out; }
animate drop-shadow-blur { duration: 180ms; easing: ease-out; }
VerticalBox {
padding-top: 18px;
padding-right: 12px;
padding-bottom: 18px;
padding-left: 12px;
spacing: 7px;
Text { height: 34px; text: @tr("SETTINGS"); color: root.muted-text; font-size: TypeScale.caption; font-weight: 700; vertical-alignment: center; }
SettingsSearch { query <=> root.settings-search-query; muted-color: root.muted-text; hover-background: root.accent-soft; edited(value) => { root.update-settings-search(value); } }
if !root.search-has-results : Text { text: @tr("No settings found"); color: root.muted-text; font-size: TypeScale.secondary; wrap: word-wrap; }
if root.settings-section-visible("General") : SettingsNavItem { text: @tr("General"); icon: @image-url("../icons/lucide/filled/sliders.svg"); icon-color: root.settings-icon-color("General"); selected: root.settings_tab == "General"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "General"; root.compact-nav-open = false; } }
if root.settings-section-visible("Accounts") : SettingsNavItem { text: @tr("Accounts"); icon: @image-url("../icons/lucide/filled/account.svg"); icon-color: root.settings-icon-color("Accounts"); selected: root.settings_tab == "Accounts"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Accounts"; root.compact-nav-open = false; } }
if root.settings-section-visible("Contacts") : SettingsNavItem { text: @tr("Contacts"); icon: @image-url("../icons/lucide/filled/contacts.svg"); icon-color: root.settings-icon-color("Contacts"); selected: root.settings_tab == "Contacts"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Contacts"; root.compact-nav-open = false; } }
if root.settings-section-visible("Notifications") : SettingsNavItem { text: @tr("Notifications"); icon: @image-url("../icons/lucide/filled/bell.svg"); icon-color: root.settings-icon-color("Notifications"); selected: root.settings_tab == "Notifications"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Notifications"; root.compact-nav-open = false; } }
if root.settings-section-visible("Shortcuts") : SettingsNavItem { text: @tr("Shortcuts"); icon: @image-url("../icons/lucide/filled/keyboard.svg"); icon-color: root.settings-icon-color("Shortcuts"); selected: root.settings_tab == "Shortcuts"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Shortcuts"; root.compact-nav-open = false; } }
if root.settings-section-visible("Data") : SettingsNavItem { text: @tr("Storage"); icon: @image-url("../icons/lucide/filled/database.svg"); icon-color: root.settings-icon-color("Data"); selected: root.settings_tab == "Data"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Data"; root.compact-nav-open = false; } }
if root.settings-section-visible("Services") : SettingsNavItem { text: @tr("Services"); icon: @image-url("../icons/lucide/filled/cloud.svg"); icon-color: root.settings-icon-color("Services"); selected: root.settings_tab == "Services"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "Services"; root.compact-nav-open = false; } }
if root.settings-section-visible("About") : SettingsNavItem { text: @tr("About"); icon: @image-url("../icons/lucide/filled/info.svg"); icon-color: root.settings-icon-color("About"); selected: root.settings_tab == "About"; accent: root.accent; accent-soft: root.accent-soft; body-text: root.body-text; muted-text: root.muted-text; clicked => { root.settings_tab = "About"; root.compact-nav-open = false; } }
Rectangle { vertical-stretch: 1; }
}
}
}
Rectangle {
visible: root.settings_open && root.caldav_account_id > 0;
width: parent.width;
height: parent.height;
background: #00000088;
z: 900;
accessible-role: groupbox;
accessible-label: root.caldav_connection_id > 0 ? @tr("CalDAV settings") : @tr("Add CalDAV calendar");
TouchArea { clicked => { root.close-caldav-settings(); } }
caldav-panel := Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(520px, parent.width - 24px);
height: min(root.compact ? 500px : 430px, parent.height - 24px);
background: root.surface;
border-radius: 12px;
border-width: 1px;
border-color: root.border-subtle;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 8px;
drop-shadow-color: #00000055;
caldav-focus := FocusScope {
forward-focus: caldav-url-input;
key-pressed(event) => {
if event.text == Key.Escape {
root.close-caldav-settings();
return accept;
}
reject
}
VerticalBox {
padding: 0px;
padding-top: 22px;
padding-right: 22px;
padding-bottom: 20px;
padding-left: 22px;
spacing: 12px;
HorizontalBox {
padding: 0px;
height: 46px;
spacing: 12px;
Rectangle {
width: 44px;
height: 44px;
border-radius: 12px;
background: root.accent-soft;
Image { x: 11px; y: 11px; width: 22px; height: 22px; source: @image-url("../icons/lucide/outline/calendar.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
}
VerticalBox {
padding: 0px;
spacing: 2px;
Text { text: root.caldav_connection_id > 0 ? @tr("CalDAV settings") : @tr("Add CalDAV calendar"); color: root.title-color; font-size: TypeScale.heading; font-weight: 700; overflow: elide; }
Text { text: root.caldav_connection_id > 0 ? @tr("Discover new calendars.") : @tr("Works with iCloud, Fastmail, Nextcloud, and standards-compliant CalDAV servers."); color: root.faint-text; font-size: TypeScale.caption; overflow: elide; }
}
Rectangle { horizontal-stretch: 1; }
IconButton { width: 40px; height: 40px; icon: @image-url("../icons/lucide/outline/x.svg"); icon-color: root.secondary-text; idle-background: transparent; hover-background: root.accent-soft; tooltip-text: @tr("Close"); clicked => { root.close-caldav-settings(); } }
}
// Keep the actions reachable when the Android keyboard
// shrinks the window or a connection error wraps.
caldav-scroll := ScrollView {
vertical-stretch: 1;
min-height: 0px;
content-width: self.visible-width;
horizontal-scrollbar-policy: always-off;
mouse-drag-pan-enabled: root.compact;
VerticalBox {
x: 0px;
y: 0px;
padding: 0px;
spacing: 12px;
width: max(0px, caldav-scroll.visible-width - 12px);
height: self.preferred-height;
caldav-url-input := FormField {
enabled: !root.caldav_connecting;
height: self.preferred-height;
label: @tr("Server address");
value <=> root.caldav_url;
placeholder: @tr("https://calendar.example.com/");
label-color: root.secondary-text;
}
Text {
height: self.preferred-height;
text: @tr("To sync one calendar, such as a shared mailbox's, enter that calendar's own address.");
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
caldav-username-input := FormField {
enabled: !root.caldav_connecting;
height: self.preferred-height;
label: @tr("Username");
value <=> root.caldav_username;
placeholder: @tr("Usually your email address");
label-color: root.secondary-text;
}
caldav-password-input := FormField {
enabled: !root.caldav_connecting;
height: self.preferred-height;
label: root.caldav_connection_id > 0 ? @tr("New app password · Optional") : @tr("App password");
value <=> root.caldav_password;
placeholder: root.caldav_connection_id > 0 ? @tr("Leave blank to keep the current password") : @tr("Stored securely in your system keyring");
input-type: password;
label-color: root.secondary-text;
}
Text {
height: self.preferred-height;
text: @tr("Invitations for events you create here are sent from {}.", root.caldav-account-email);
color: root.faint-text;
font-size: TypeScale.caption;
wrap: word-wrap;
}
if root.sync_status != "" : Text { text: root.sync_status; color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; height: self.preferred-height; }
}
}
HorizontalBox {
padding: 0px;
height: root.compact ? 44px : 34px;
spacing: 8px;
Rectangle { horizontal-stretch: 1; }
Button { min-width: self.content-width; height: parent.height; text: @tr("Cancel"); clicked => { root.close-caldav-settings(); } }
Button {
min-width: self.content-width;
height: parent.height;
text: root.caldav_connecting ? @tr("Connecting…") : root.caldav_connection_id > 0 ? @tr("Reconnect") : @tr("Connect");
primary: true;
enabled: !root.caldav_connecting && root.caldav_url != "" && (root.caldav_connection_id > 0 || root.caldav_password != "");
clicked => { root.connect_caldav(root.caldav_account_id, root.caldav_connection_id, root.caldav_url, root.caldav_username, root.caldav_password); }
}
}
}
}
}
}
Rectangle {
visible: root.settings_open && root.account_form_open;
width: parent.width;
height: parent.height;
background: #00000088;
z: 900;
accessible-role: groupbox;
accessible-label: root.account_editing ? @tr("Edit mail account") : @tr("Connect a mail account");
TouchArea { clicked => { root.account_form_open = false; } }
account-panel := Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(640px, parent.width - 24px);
height: min(root.compact ? 520px : root.account_protocol == "jmap" ? 480px : 520px, parent.height - 24px);
background: root.surface;
border-radius: 12px;
border-width: 1px;
border-color: root.border-subtle;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 8px;
drop-shadow-color: #00000055;
account-focus := FocusScope {
key-pressed(event) => {
if event.text == Key.Escape {
root.account_form_open = false;
return accept;
}
reject
}
account-content := VerticalBox {
padding: 0px;
padding-top: 22px;
padding-right: 22px;
padding-bottom: 20px;
padding-left: 22px;
spacing: 12px;
HorizontalBox {
padding: 0px;
height: 44px;
spacing: 12px;
Rectangle {
width: 44px;
height: 44px;
border-radius: 12px;
background: root.accent-soft;
Image { x: 11px; y: 11px; width: 22px; height: 22px; source: @image-url("../icons/lucide/outline/mail.svg"); colorize: root.accent; image-fit: contain; accessible-role: none; }
}
VerticalBox {
padding: 0px;
spacing: 2px;
Text { text: root.account_editing ? @tr("Edit {}", root.account_email) : @tr("Connect a mail account"); color: root.title-color; font-size: TypeScale.heading; font-weight: 700; overflow: elide; }
Text { text: root.account_protocol == "jmap" ? @tr("Uses JMAP for mail sync and sending, with automatic discovery.") : root.account_protocol == "pop3" ? @tr("Downloads mail from a POP3 server to this device and sends through SMTP.") : @tr("Uses separate IMAP and SMTP servers for mail sync and sending."); color: root.faint-text; font-size: TypeScale.caption; overflow: elide; }
}
Rectangle { horizontal-stretch: 1; }
IconButton { width: 40px; height: 40px; icon: @image-url("../icons/lucide/outline/x.svg"); icon-color: root.secondary-text; idle-background: transparent; hover-background: root.accent-soft; tooltip-text: @tr("Close"); clicked => { root.account_form_open = false; } }
}
account-scroll := ScrollView {
vertical-stretch: 1;
content-width: self.visible-width;
mouse-drag-pan-enabled: root.compact;
VerticalBox {
padding: 0px;
width: max(0px, account-scroll.visible-width - 16px);
height: self.preferred-height;
padding-right: 4px;
spacing: 10px;
if !root.compact : HorizontalBox {
padding: 0px;
height: 57px;
spacing: 10px;
FormField { enabled: !root.account_setup_in_progress; horizontal-stretch: 1; label: @tr("Email address"); value <=> root.account_email; placeholder: @tr("name@example.com"); label-color: root.secondary-text; edited(value) => { root.suggest-servers(value); } }
FormField { enabled: !root.account_setup_in_progress; horizontal-stretch: 1; label: @tr("Username"); value <=> root.account_username; placeholder: @tr("Usually your email address"); label-color: root.secondary-text; }
}
if root.compact : VerticalBox {
padding: 0px;
height: 124px;
spacing: 10px;
FormField { enabled: !root.account_setup_in_progress; height: 57px; label: @tr("Email address"); value <=> root.account_email; placeholder: @tr("name@example.com"); label-color: root.secondary-text; edited(value) => { root.suggest-servers(value); } }
FormField { enabled: !root.account_setup_in_progress; height: 57px; label: @tr("Username"); value <=> root.account_username; placeholder: @tr("Usually your email address"); label-color: root.secondary-text; }
}
FormField { enabled: !root.account_setup_in_progress; height: 57px; label: root.account_protocol == "jmap" ? @tr("Password, app password, or API token") : @tr("Password or app password"); value <=> root.account_password; placeholder: @tr("Stored securely in your system keyring"); input-type: password; label-color: root.secondary-text; }
// POP3 mail exists only on this device, so an account
// never switches to or from POP3.
if !(root.account_editing && root.account_protocol == "pop3") : HorizontalBox {
padding: 0px;
height: 40px;
spacing: 8px;
Button { horizontal-stretch: 1; height: 40px; text: @tr("IMAP"); enabled: !root.account_setup_in_progress; primary: root.account_protocol == "imap"; clicked => { root.choose-protocol("imap"); } }
if !root.account_editing : Button { horizontal-stretch: 1; height: 40px; text: @tr("POP3"); enabled: !root.account_setup_in_progress; primary: root.account_protocol == "pop3"; clicked => { root.choose-protocol("pop3"); } }
Button { horizontal-stretch: 1; height: 40px; text: @tr("JMAP"); enabled: !root.account_setup_in_progress; primary: root.account_protocol == "jmap"; clicked => { root.choose-protocol("jmap"); } }
}
if root.account_protocol == "jmap" : FormField { enabled: !root.account_setup_in_progress;
height: 57px;
label: @tr("Server address · Optional");
value <=> root.jmap_url;
placeholder: @tr("Discovered from your email domain");
label-color: root.secondary-text;
}
if root.account_protocol != "jmap" : HorizontalBox {
padding: 0px;
height: 57px;
spacing: 10px;
FormField { enabled: !root.account_setup_in_progress; horizontal-stretch: 1; label: root.account_protocol == "pop3" ? @tr("POP3 server") : @tr("IMAP server"); value <=> root.incoming_host; placeholder: root.account_protocol == "pop3" ? @tr("pop.example.com") : @tr("imap.example.com"); label-color: root.secondary-text; }
FormField { enabled: !root.account_setup_in_progress; width: 88px; label: @tr("Port"); value <=> root.incoming_port; placeholder: MailPorts.incoming(root.account_protocol, root.incoming_security); label-color: root.secondary-text; }
}
if root.account_protocol != "jmap" : HorizontalBox {
padding: 0px;
height: 57px;
spacing: 10px;
FormField { enabled: !root.account_setup_in_progress; horizontal-stretch: 1; label: @tr("SMTP server"); value <=> root.smtp_host; placeholder: @tr("smtp.example.com"); label-color: root.secondary-text; }
FormField { enabled: !root.account_setup_in_progress; width: 88px; label: @tr("Port"); value <=> root.smtp_port; placeholder: MailPorts.smtp(root.smtp_security); label-color: root.secondary-text; }
}
if root.account_protocol == "imap" : Text {
text: @tr("Proton Bridge: use the local servers, ports and password shown in Bridge, and match its encryption settings.");
color: root.secondary-text; font-size: TypeScale.caption; wrap: word-wrap; height: self.preferred-height;
}
if root.account_protocol != "jmap" : MailSecurityFields {
incoming-protocol: root.account_protocol;
incoming-security <=> root.incoming_security;
smtp-security <=> root.smtp_security;
incoming-port <=> root.incoming_port;
smtp-port <=> root.smtp_port;
enabled: !root.account_setup_in_progress;
certificate-pem <=> root.trusted_certificate_pem;
import-certificate => { root.import_mail_certificate(); }
}
}
}
if root.show_account_status && root.sync_status != "" : OnScreenSurface {
height: max(36px, account-status.preferred-height + 16px);
background: root.account_setup_in_progress ? root.accent-soft : AppTheme.destructive.transparentize(0.9);
border-radius: 8px;
border-width: 1px;
border-color: root.account_setup_in_progress ? root.accent.transparentize(0.55) : AppTheme.destructive.transparentize(0.45);
account-status := CopyableDiagnostic {
x: 10px;
y: 8px;
width: parent.width - 20px;
text: root.sync_status;
text-color: root.account_setup_in_progress ? root.body-text : AppTheme.destructive;
font-size: TypeScale.caption;
}
}
// Phone-width dialogs give the whole row to the buttons
// and stack them when a label does not fit in half of it.
GridLayout {
property <bool> stacked: root.compact && max(cancel-account.content-width, save-account.content-width) * 2 + self.spacing
> account-panel.width - account-content.padding-left - account-content.padding-right;
spacing: 8px;
if !root.compact : Rectangle { row: 0; col: 0; horizontal-stretch: 1; }
cancel-account := Button { row: 0; col: root.compact ? 0 : 1; height: root.compact ? 44px : 34px; label-overflow: elide; text: @tr("Cancel"); clicked => { root.account_form_open = false; } }
save-account := Button {
row: parent.stacked ? 1 : 0;
col: parent.stacked ? 0 : root.compact ? 1 : 2;
height: root.compact ? 44px : 34px;
label-overflow: elide;
text: root.account_setup_in_progress ? @tr("Connecting…") : root.account_editing ? @tr("Save account") : @tr("Connect account");
primary: true;
enabled: root.account_form_valid && !root.account_setup_in_progress;
clicked => {
root.show_account_status = true;
root.add_password_account(root.account_protocol, root.account_email, root.account_username, root.account_password, root.jmap_url, root.incoming_host, root.incoming_port, root.smtp_host, root.smtp_port);
}
}
}
}
}
}
}
Rectangle {
visible: root.settings_open && root.profile-editor-open;
width: parent.width;
height: parent.height;
background: #00000088;
z: 950;
accessible-role: groupbox;
accessible-label: root.profile-editor-id == "" ? @tr("Create profile") : @tr("Edit profile");
TouchArea { clicked => { root.close-profile-editor(); } }
profile-panel := Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(480px, parent.width - 24px);
// Up to the content's height; taller content scrolls.
height: min(profile-content.preferred-height, parent.height - 24px);
background: root.surface;
border-radius: 12px;
border-width: 1px;
border-color: root.border-subtle;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 8px;
drop-shadow-color: #00000055;
clip: true;
profile-focus := FocusScope {
forward-focus: profile-name-field;
key-pressed(event) => {
if event.text == Key.Escape {
root.close-profile-editor();
return accept;
}
reject
}
ScrollView {
width: parent.width;
height: parent.height;
content-width: self.visible-width;
mouse-drag-pan-enabled: root.compact;
profile-content := VerticalLayout {
width: profile-panel.width;
padding-top: 20px;
padding-right: 20px;
padding-bottom: 18px;
padding-left: 20px;
spacing: 14px;
HorizontalLayout {
spacing: 12px;
Rectangle {
width: 40px;
height: 40px;
border-radius: 11px;
background: root.profile-editor-color.transparentize(0.82);
Rectangle {
x: 15px; y: 7px; width: 10px; height: 26px;
border-radius: 5px;
background: root.profile-editor-color;
}
}
VerticalLayout {
alignment: center;
spacing: 1px;
Text { text: root.profile-editor-id == "" ? @tr("Create profile") : @tr("Edit profile"); color: root.title-color; font-size: TypeScale.heading; font-weight: 700; overflow: elide; }
Text { text: @tr("Its color marks its accounts' mail, and choosing it shows only their mail."); color: root.faint-text; font-size: TypeScale.caption; wrap: word-wrap; }
}
IconButton { width: 36px; height: 36px; icon: @image-url("../icons/lucide/outline/x.svg"); icon-color: root.secondary-text; ghost: true; hover-background: root.accent-soft; tooltip-text: @tr("Close"); clicked => { root.close-profile-editor(); } }
}
profile-name-field := FormField {
label: @tr("Profile name");
value <=> root.profile-editor-name;
placeholder: @tr("Personal or Work");
label-color: root.secondary-text;
edited(value) => { root.profile-editor-error = ""; }
}
VerticalLayout {
spacing: 8px;
Text { text: @tr("Color"); color: root.secondary-text; font-size: TypeScale.caption; font-weight: 600; }
HorizontalLayout {
spacing: profile-panel.width < 400px ? 5px : 8px;
alignment: start;
for swatch[index] in ProfilePalette.colors : ProfileColorChoice {
width: profile-panel.width < 400px ? 28px : 32px;
height: self.width;
value: swatch;
selected: !root.profile-editor-custom-color && root.profile-editor-color == swatch;
label: @tr("Profile color {}", index + 1);
chosen => {
root.profile-editor-color = swatch;
root.profile-editor-custom-color = false;
}
}
}
Button {
height: 34px;
text: @tr("Custom color…");
selected: root.profile-editor-custom-color;
clicked => { root.profile-editor-custom-color = true; }
}
if root.profile-editor-custom-color : RuntimeColorPicker {
height: 222px;
value: root.profile-editor-color;
edited(value) => { root.profile-editor-color = value; }
}
}
VerticalLayout {
spacing: 6px;
Text { text: @tr("Accounts"); color: root.secondary-text; font-size: TypeScale.caption; font-weight: 600; }
VerticalLayout {
spacing: 2px;
accessible-role: list;
accessible-label: @tr("Accounts");
for member in root.profile_members : ProfileMemberChoice {
member: member;
toggled => { member.checked = !member.checked; }
}
}
}
if root.profile-editor-error != "" : Text { text: root.profile-editor-error; color: AppTheme.destructive; font-size: TypeScale.caption; wrap: word-wrap; }
HorizontalLayout {
spacing: 8px;
padding-top: 2px;
// A phone gives both actions the whole row, which
// their labels need at larger platform fonts.
if !root.compact : Rectangle { horizontal-stretch: 1; }
Button { height: root.compact ? 44px : 34px; text: @tr("Cancel"); clicked => { root.close-profile-editor(); } }
Button {
height: root.compact ? 44px : 34px;
text: root.profile-editor-id == "" ? @tr("Create profile") : @tr("Save profile");
primary: true;
enabled: root.profile-editor-name != "";
clicked => {
let saved-id = root.save_mail_profile(root.profile-editor-id, root.profile-editor-name, root.profile-editor-color);
if saved-id != "" {
root.close-profile-editor();
} else {
root.profile-editor-error = @tr("Could not save this profile. Check the name and try again.");
}
}
}
}
}
}
}
}
}
if root.settings_open && root.account-profile-picker-open
&& root.account-profile-picker-account.id == root.account-profile-picker-id : Rectangle {
width: parent.width;
height: parent.height;
background: #00000088;
z: 950;
accessible-role: groupbox;
accessible-label: @tr("Choose a profile for {}", root.account-profile-picker-account.name);
TouchArea { clicked => { root.close-account-profile-picker(); } }
AccountProfilePicker {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(460px, parent.width - 24px);
height: min(self.content-height, parent.height - 24px);
compact: root.compact;
profiles: root.mail_profiles;
account: root.account-profile-picker-account;
choose-profile(profile-id) => {
root.assign_account_profile(root.account-profile-picker-id, profile-id);
}
choose-color(color, generated) => {
root.save_account_color(root.account-profile-picker-id, color, generated);
}
create-profile => {
let account-id = root.account-profile-picker-id;
root.close-account-profile-picker();
root.open-new-profile(account-id);
}
close => { root.close-account-profile-picker(); }
}
}
ConfirmationDialog {
width: parent.width;
height: parent.height;
open <=> root.delete_data_confirmation;
title: @tr("Delete all local data?");
description: @tr("This removes every account, downloaded message, calendar, contact, preference, and credential stored by Flectar Mail on this device.");
detail: LocalMailUi.account-id >= 0
? @tr("Imported mail and the mail of any POP3 account are stored only on this device and will be deleted. Mail and events on Gmail, Outlook, and IMAP servers will not be deleted.")
: root.has_device_mail
? @tr("POP3 mail is stored only on this device and will be deleted. Mail and events on Gmail, Outlook, and IMAP servers will not be deleted.")
: @tr("Mail and events on Gmail, Outlook, and IMAP servers will not be deleted.");
action-label: LocalMailUi.account-id >= 0 ? @tr("Export mail…") : root.has_device_mail ? @tr("Export POP3 mail…") : "";
action-status: root.mail_export_status;
action-enabled: !root.mail_export_running;
action => { root.export_device_mail(-1); }
confirm-label: @tr("Delete everything");
cancel-label: @tr("Keep my data");
icon: @image-url("../icons/lucide/outline/trash.svg");
surface: root.surface;
dialog-border-color: root.border-subtle;
title-color: root.title-color;
body-color: root.body-text;
muted-color: root.secondary-text;
confirmed => { root.delete_all_data(); }
}
ConfirmationDialog {
width: parent.width;
height: parent.height;
open <=> root.delete_local_mail_confirmation;
title: @tr("Delete mail on this device?");
subject: @tr("One message" | "{} messages" % LocalMailUi.messages, Numbers.count(LocalMailUi.messages));
description: @tr("Delete the mail you imported and its folders from this device.");
detail: @tr("It exists only here. Export it first to keep a copy.");
action-label: @tr("Export mail…");
action-status: root.mail_export_status;
action-enabled: !root.mail_export_running;
action => { root.export_device_mail(LocalMailUi.account-id); }
confirm-label: @tr("Delete mail");
cancel-label: @tr("Keep mail");
icon: @image-url("../icons/lucide/outline/trash.svg");
surface: root.surface;
dialog-border-color: root.border-subtle;
title-color: root.title-color;
body-color: root.body-text;
muted-color: root.secondary-text;
confirmed => { root.delete_account(LocalMailUi.account-id); }
}
ConfirmationDialog {
width: parent.width;
height: parent.height;
open <=> root.delete_account_confirmation;
title: @tr("Remove account?");
subject: root.delete_account_name;
description: @tr("Remove this account and its downloaded mail from this device.");
detail: root.delete_account_device_mail
? @tr("This account's mail is stored only on this device. Export it first to keep a copy.")
: @tr("Messages on the mail server will not be deleted.");
action-label: root.delete_account_device_mail ? @tr("Export mail…") : "";
action-status: root.mail_export_status;
action-enabled: !root.mail_export_running;
action => { root.export_device_mail(root.delete_account_id); }
confirm-label: @tr("Remove account");
cancel-label: @tr("Keep account");
icon: @image-url("../icons/lucide/outline/trash.svg");
surface: root.surface;
dialog-border-color: root.border-subtle;
title-color: root.title-color;
body-color: root.body-text;
muted-color: root.secondary-text;
confirmed => { root.delete_account(root.delete_account_id); }
}
ConfirmationDialog {
width: parent.width;
height: parent.height;
open <=> root.delete-profile-confirmation;
title: @tr("Delete profile?");
subject: root.delete-profile-name;
description: @tr("Accounts assigned to this profile will return to their individual or generated account colors.");
detail: @tr("Accounts and provider data are not changed.");
confirm-label: @tr("Delete profile");
cancel-label: @tr("Keep profile");
icon: @image-url("../icons/lucide/outline/trash.svg");
surface: root.surface;
dialog-border-color: root.border-subtle;
title-color: root.title-color;
body-color: root.body-text;
muted-color: root.secondary-text;
confirmed => { root.delete_mail_profile(root.delete-profile-id); }
}
ConfirmationDialog {
width: parent.width;
height: parent.height;
open <=> root.remove-calendar-confirmation;
title: @tr("Remove calendar?");
subject: root.remove-calendar-name;
description: @tr("Stop syncing it and remove its events from this device.");
detail: @tr("Events on the server will not be deleted.");
confirm-label: @tr("Remove calendar");
cancel-label: @tr("Keep calendar");
icon: @image-url("../icons/lucide/outline/trash.svg");
surface: root.surface;
dialog-border-color: root.border-subtle;
title-color: root.title-color;
body-color: root.body-text;
muted-color: root.secondary-text;
confirmed => { root.remove_calendar_connection(root.remove-calendar-id); }
}
}