mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
437 lines
16 KiB
Plaintext
437 lines
16 KiB
Plaintext
import { SidebarRowSurface, SidebarStyle } from "sidebar-row.slint";
|
|
import { MailDragApi } from "../models.slint";
|
|
import { Avatar } from "avatar.slint";
|
|
import { TypeScale } from "../typography.slint";
|
|
|
|
component SidebarIcon {
|
|
in property <image> icon;
|
|
in property <color> color;
|
|
in property <length> icon-size: SidebarStyle.icon-size;
|
|
|
|
width: SidebarStyle.icon-size;
|
|
height: 28px;
|
|
|
|
Image {
|
|
x: (parent.width - root.icon-size) / 2;
|
|
y: (parent.height - root.icon-size) / 2;
|
|
width: root.icon-size;
|
|
height: root.icon-size;
|
|
source: root.icon;
|
|
colorize: root.color;
|
|
image-fit: contain;
|
|
accessible-role: none;
|
|
}
|
|
}
|
|
|
|
// Counts are compact status badges rather than loose right-aligned text. This
|
|
// keeps large unread values visually grouped and gives selected rows a clear
|
|
// accent treatment without changing the row's geometry.
|
|
component SidebarCountBadge inherits Rectangle {
|
|
in property <string> count;
|
|
in property <bool> selected: false;
|
|
in property <color> accent-color;
|
|
in property <color> muted-color;
|
|
|
|
visible: root.count != "";
|
|
width: min(52px, max(24px, count-label.preferred-width + 12px));
|
|
height: 20px;
|
|
border-radius: 7px;
|
|
background: root.selected
|
|
? root.accent-color.transparentize(0.78)
|
|
: root.muted-color.transparentize(0.86);
|
|
|
|
count-label := Text {
|
|
text: root.count;
|
|
color: root.selected ? root.accent-color : root.muted-color;
|
|
font-size: TypeScale.caption;
|
|
font-weight: 600;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
// A row's pointer area, which sits inside the row's ContextMenuArea so taps
|
|
// reach it first: on Android, Slint holds every press on a menu's own area to
|
|
// wait for a long press, which swallowed taps on the rows above it. A
|
|
// right-click opens the menu, and so does holding a finger still for half a
|
|
// second, as on Android and iOS.
|
|
component SidebarRowHit inherits TouchArea {
|
|
in property <bool> menu-enabled;
|
|
callback activated();
|
|
callback open-menu(Point);
|
|
private property <bool> holding: false;
|
|
private property <bool> held: false;
|
|
private property <length> hold-x;
|
|
private property <length> hold-y;
|
|
|
|
pointer-event(event) => {
|
|
if event.kind == PointerEventKind.down {
|
|
self.held = false;
|
|
if event.button == PointerEventButton.right && root.menu-enabled {
|
|
root.open-menu({ x: self.mouse-x, y: self.mouse-y });
|
|
} else if event.button == PointerEventButton.left && event.touch-finger-id != 0 && root.menu-enabled {
|
|
self.hold-x = self.mouse-x;
|
|
self.hold-y = self.mouse-y;
|
|
self.holding = true;
|
|
}
|
|
} else if event.kind == PointerEventKind.move {
|
|
if self.holding && (abs(self.mouse-x - self.hold-x) > 8px || abs(self.mouse-y - self.hold-y) > 8px) {
|
|
self.holding = false;
|
|
}
|
|
} else {
|
|
self.holding = false;
|
|
}
|
|
}
|
|
clicked => {
|
|
// The finger that opened the menu lifts here; it was not a tap.
|
|
if !self.held {
|
|
root.activated();
|
|
}
|
|
self.held = false;
|
|
}
|
|
|
|
Timer {
|
|
interval: 500ms;
|
|
running: root.holding;
|
|
triggered => {
|
|
root.holding = false;
|
|
root.held = true;
|
|
root.open-menu({ x: root.hold-x, y: root.hold-y });
|
|
}
|
|
}
|
|
}
|
|
|
|
// A folder row is shared by unified and account sections so indentation,
|
|
// selection, hover, and counts remain consistent throughout the tree.
|
|
export component SidebarItem inherits SidebarRowSurface {
|
|
in property <bool> interactive: true;
|
|
in property <string> label;
|
|
in property <string> accessible-name: root.label;
|
|
in property <string> count;
|
|
in property <string> scope;
|
|
in property <image> icon;
|
|
in property <bool> selected: false;
|
|
in property <bool> label-has-emoji: false;
|
|
in property <string> label-path: root.label;
|
|
in property <length> row-height: 28px;
|
|
in property <length> left-padding: SidebarStyle.icon-left;
|
|
in property <int> account-id: -1;
|
|
in property <int> folder-id: -1;
|
|
in property <int> label-id: -1;
|
|
in property <bool> has-children: false;
|
|
in property <bool> expanded: true;
|
|
in property <bool> label-tree-node: false;
|
|
in property <bool> folder-menu-enabled: false;
|
|
in property <bool> can-create-child: false;
|
|
in property <bool> can-rename: false;
|
|
in property <bool> can-delete: false;
|
|
in property <bool> can-empty-trash: false;
|
|
in property <bool> label-manageable: false;
|
|
in property <bool> label-can-create-child: false;
|
|
in property <bool> label-is-global: false;
|
|
in property <string> label-account-name: "";
|
|
in property <bool> mail-drop-enabled: false;
|
|
in property <bool> mail-drop-copies: false;
|
|
in property <color> sidebar-background;
|
|
in property <color> selection-background;
|
|
in property <color> accent-color;
|
|
in property <color> body-color;
|
|
in property <color> muted-color;
|
|
in property <color> icon-color: muted-color;
|
|
in property <color> editable-color: icon-color;
|
|
callback clicked(string);
|
|
callback toggled(int, bool);
|
|
callback new-subfolder(int, int, string);
|
|
callback rename-folder(int, string);
|
|
callback delete-folder(int, string);
|
|
callback empty-trash(int);
|
|
callback new-child-label(int, int, string, string);
|
|
callback rename-label(int, string, color, string, bool);
|
|
callback delete-label(int, string, string, bool, bool);
|
|
|
|
height: root.visible ? root.row-height : 0px;
|
|
idle-background: root.sidebar-background;
|
|
hover-background: root.selection-background;
|
|
hovered: row-hit.has-hover;
|
|
active: root.selected || mail-drop-target.has-drag;
|
|
border-width: mail-drop-target.has-drag ? 2px : 0px;
|
|
border-color: root.accent-color;
|
|
|
|
SidebarIcon {
|
|
x: root.left-padding;
|
|
y: 0px;
|
|
height: root.row-height;
|
|
icon: root.icon;
|
|
color: root.icon-color;
|
|
}
|
|
|
|
if root.has-children: Image {
|
|
x: root.left-padding - 18px;
|
|
y: (root.row-height - self.height) / 2;
|
|
width: 12px;
|
|
height: 12px;
|
|
source: root.expanded
|
|
? @image-url("../icons/lucide/outline/chevron-down.svg")
|
|
: @image-url("../icons/lucide/outline/chevron-right.svg");
|
|
colorize: root.muted-color;
|
|
image-fit: contain;
|
|
accessible-role: none;
|
|
}
|
|
|
|
Text {
|
|
x: root.left-padding + SidebarStyle.label-offset;
|
|
y: 0px;
|
|
width: max(0px, parent.width - self.x - (root.count == "" ? 6px : count-badge.width + 12px));
|
|
height: root.row-height;
|
|
text: root.label;
|
|
color: root.body-color;
|
|
font-size: SidebarStyle.label-size;
|
|
font-weight: root.selected ? SidebarStyle.selected-weight : SidebarStyle.normal-weight;
|
|
font-family: root.label-has-emoji ? "Noto Emoji" : "";
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
count-badge := SidebarCountBadge {
|
|
x: parent.width - self.width - 6px;
|
|
y: (root.row-height - self.height) / 2;
|
|
count: root.count;
|
|
selected: root.selected;
|
|
accent-color: root.accent-color;
|
|
muted-color: root.muted-color;
|
|
}
|
|
|
|
// Folder and label actions. Rows without any keep the area disabled.
|
|
row-menu := ContextMenuArea {
|
|
enabled: root.interactive && (root.folder-menu-enabled || root.can-empty-trash || root.label-manageable);
|
|
width: parent.width;
|
|
height: parent.height;
|
|
Menu {
|
|
if root.folder-menu-enabled: MenuItem {
|
|
title: @tr("New subfolder");
|
|
enabled: root.can-create-child;
|
|
activated => { root.new-subfolder(root.account-id, root.folder-id, root.label); }
|
|
}
|
|
if root.folder-menu-enabled: MenuItem {
|
|
title: @tr("Rename folder");
|
|
enabled: root.can-rename;
|
|
activated => { root.rename-folder(root.folder-id, root.label); }
|
|
}
|
|
if root.folder-menu-enabled: MenuItem {
|
|
title: @tr("Delete folder");
|
|
enabled: root.can-delete;
|
|
activated => { root.delete-folder(root.folder-id, root.label); }
|
|
}
|
|
if root.can-empty-trash: MenuItem {
|
|
title: @tr("Empty Trash…");
|
|
activated => { root.empty-trash(root.account-id); }
|
|
}
|
|
if root.label-manageable && root.label-can-create-child: MenuItem {
|
|
title: @tr("New nested label");
|
|
activated => { root.new-child-label(root.account-id, root.label-id, root.label-path, root.label-account-name); }
|
|
}
|
|
if root.label-manageable: MenuItem {
|
|
title: root.label-is-global ? @tr("Rename global label") : @tr("Rename label in {}", root.label-account-name);
|
|
activated => { root.rename-label(root.label-id, root.label-path, root.editable-color, root.label-account-name, root.label-is-global); }
|
|
}
|
|
if root.label-manageable: MenuItem {
|
|
title: root.label-is-global ? @tr("Delete global label") : @tr("Delete label from {}", root.label-account-name);
|
|
activated => { root.delete-label(root.label-id, root.label-path, root.label-account-name, root.label-is-global, root.has-children); }
|
|
}
|
|
}
|
|
|
|
row-hit := SidebarRowHit {
|
|
enabled: root.interactive;
|
|
menu-enabled: row-menu.enabled;
|
|
accessible-role: button;
|
|
accessible-label: root.accessible-name;
|
|
accessible-checked: root.selected;
|
|
accessible-action-default => { root.clicked(root.scope); }
|
|
activated => { root.clicked(root.scope); }
|
|
open-menu(position) => { row-menu.show(position); }
|
|
mouse-cursor: pointer;
|
|
}
|
|
}
|
|
|
|
if root.has-children: TouchArea {
|
|
enabled: root.interactive;
|
|
x: max(0px, root.left-padding - 22px);
|
|
y: 0px;
|
|
width: 22px;
|
|
height: root.row-height;
|
|
accessible-role: button;
|
|
accessible-label: root.label-tree-node
|
|
? (root.expanded ? @tr("Collapse label") : @tr("Expand label"))
|
|
: (root.expanded ? @tr("Collapse folder") : @tr("Expand folder"));
|
|
accessible-expanded: root.expanded;
|
|
accessible-action-default => {
|
|
root.toggled(root.folder-id, !root.expanded);
|
|
}
|
|
clicked => { root.toggled(root.folder-id, !root.expanded); }
|
|
mouse-cursor: pointer;
|
|
}
|
|
|
|
mail-drop-target := DropArea {
|
|
width: parent.width;
|
|
height: parent.height;
|
|
enabled: root.interactive && root.mail-drop-enabled;
|
|
can-drop(event) => {
|
|
MailDragApi.can-drop-on-mailbox(
|
|
event.data,
|
|
root.scope,
|
|
root.account-id,
|
|
root.folder-id,
|
|
) ? (root.mail-drop-copies ? DragAction.copy : DragAction.move) : DragAction.none
|
|
}
|
|
dropped(event) => {
|
|
if MailDragApi.drop-on-mailbox(
|
|
event.data,
|
|
root.scope,
|
|
root.account-id,
|
|
root.folder-id,
|
|
) {
|
|
return root.mail-drop-copies ? DragAction.copy : DragAction.move;
|
|
}
|
|
return DragAction.none;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expansion belongs to the host model so recycled headers retain their state.
|
|
export component SidebarSectionHeader inherits SidebarRowSurface {
|
|
in property <bool> interactive: true;
|
|
in property <string> label;
|
|
in property <string> detail;
|
|
// A collapsed account shows the unread mail of the folders it hides.
|
|
in property <string> count;
|
|
in property <string> avatar;
|
|
in property <image> avatar-image;
|
|
in property <bool> has-avatar-image: false;
|
|
in property <image> icon;
|
|
in property <bool> has-avatar: false;
|
|
in property <bool> open: true;
|
|
in property <color> sidebar-background;
|
|
in property <color> body-color;
|
|
in property <color> muted-color;
|
|
in property <color> icon-color;
|
|
in property <int> label-font-weight: SidebarStyle.selected-weight;
|
|
in property <length> left-padding: 4px;
|
|
in property <bool> folder-row-style: false;
|
|
// An account's heading offers that account's actions on right-click.
|
|
in property <bool> account-menu: false;
|
|
in property <bool> can-check-mail: true;
|
|
// Headers and children share the same icon and text columns. The leading
|
|
// gutter belongs to disclosure controls, not to an extra icon indent.
|
|
private property <length> icon-left: root.left-padding + SidebarStyle.icon-left - 4px;
|
|
private property <length> label-left: root.icon-left + SidebarStyle.label-offset;
|
|
private property <length> trailing: root.count == "" ? 8px : count-badge.width + 12px;
|
|
callback toggled(bool);
|
|
callback check-mail();
|
|
callback account-settings();
|
|
|
|
height: root.folder-row-style ? 28px : root.detail == "" ? 32px : 7px + max(20px, header-label.preferred-height) + header-detail.height;
|
|
idle-background: root.sidebar-background;
|
|
hovered: header-hit.has-hover;
|
|
|
|
Image {
|
|
x: root.left-padding;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 12px;
|
|
height: 12px;
|
|
source: root.open
|
|
? @image-url("../icons/lucide/outline/chevron-down.svg")
|
|
: @image-url("../icons/lucide/outline/chevron-right.svg");
|
|
colorize: root.muted-color;
|
|
image-fit: contain;
|
|
accessible-role: none;
|
|
}
|
|
|
|
if root.has-avatar: Avatar {
|
|
x: root.icon-left + (SidebarStyle.icon-size - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
size: 22px;
|
|
source: root.avatar-image;
|
|
has-source: root.has-avatar-image;
|
|
fallback: root.avatar;
|
|
background-color: root.has-avatar-image ? #ffffff : root.hover-background;
|
|
foreground-color: root.body-color;
|
|
}
|
|
|
|
if !root.has-avatar: SidebarIcon {
|
|
x: root.icon-left;
|
|
y: (parent.height - self.height) / 2;
|
|
height: 22px;
|
|
icon: root.icon;
|
|
color: root.icon-color;
|
|
}
|
|
|
|
header-label := Text {
|
|
x: root.label-left;
|
|
y: root.detail == "" ? 0px : 4px;
|
|
width: parent.width - self.x - root.trailing;
|
|
height: root.detail == "" ? parent.height : max(20px, self.preferred-height);
|
|
text: root.label;
|
|
color: root.body-color;
|
|
font-size: SidebarStyle.label-size;
|
|
font-weight: root.label-font-weight;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
header-detail := Text {
|
|
visible: root.detail != "";
|
|
x: root.label-left;
|
|
y: header-label.y + header-label.height - 2px;
|
|
width: parent.width - self.x - root.trailing;
|
|
height: max(15px, self.preferred-height);
|
|
text: root.detail;
|
|
color: root.muted-color;
|
|
font-size: TypeScale.caption;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
|
|
count-badge := SidebarCountBadge {
|
|
x: parent.width - self.width - 6px;
|
|
y: (parent.height - self.height) / 2;
|
|
count: root.count;
|
|
accent-color: root.muted-color;
|
|
muted-color: root.muted-color;
|
|
}
|
|
|
|
heading-menu := ContextMenuArea {
|
|
enabled: root.interactive && root.account-menu;
|
|
width: parent.width;
|
|
height: parent.height;
|
|
Menu {
|
|
MenuItem {
|
|
title: @tr("Check for new mail");
|
|
enabled: root.can-check-mail;
|
|
activated => { root.check-mail(); }
|
|
}
|
|
|
|
MenuItem {
|
|
title: @tr("Account settings");
|
|
activated => { root.account-settings(); }
|
|
}
|
|
}
|
|
|
|
header-hit := SidebarRowHit {
|
|
enabled: root.interactive;
|
|
menu-enabled: heading-menu.enabled;
|
|
accessible-role: button;
|
|
accessible-label: root.label;
|
|
accessible-expanded: root.open;
|
|
accessible-action-default => {
|
|
root.toggled(!root.open);
|
|
}
|
|
activated => {
|
|
root.toggled(!root.open);
|
|
}
|
|
open-menu(position) => { heading-menu.show(position); }
|
|
mouse-cursor: pointer;
|
|
}
|
|
}
|
|
}
|