[ui] unify settings dialogs and improve announcement controls

This commit is contained in:
dijunkun
2026-10-14 20:18:51 +08:00
parent 2cf0d79543
commit e76d2bf903
10 changed files with 411 additions and 240 deletions
@@ -1493,6 +1493,10 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
SyncAnnouncements();
});
main->on_refresh_announcements([this] { announcements_.Refresh(); });
main->on_read_all_announcements([this] {
announcements_.ReadAll();
SyncAnnouncements();
});
main->on_open_announcement_link([this](slint::SharedString link) {
const std::string url(link);
if (announcement_text::IsWebUrl(url)) OpenUrl(url);
@@ -2637,6 +2641,7 @@ void GuiApplication::SyncAnnouncements() {
revision == ui_->announcement_selected_revision) {
selected_found = true;
main->set_announcement_selected_id(row.id);
main->set_announcement_selected_revision(row.revision);
main->set_announcement_selected_title(row.title);
main->set_announcement_selected_body(
announcement_text::Format(item["body"].get<std::string>()));
@@ -2648,6 +2653,7 @@ void GuiApplication::SyncAnnouncements() {
if (!selected_found) {
ui_->announcement_selected_id.clear();
main->set_announcement_selected_id("");
main->set_announcement_selected_revision("");
main->set_announcement_selected_body(slint::StyledText{});
}
main->set_announcements(
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Regenerate message_spacing.ttf with fontTools (only needed for regeneration).
Slint 1.17 pins line height to the requested font's ascent, descent and leading,
but StyledText does not expose line-height. This metrics-only font supplies 1.5em
line boxes without replacing any visible glyphs: Slint's normal system fallback
fonts still render the text and links. Use this family for message and release-note
bodies without changing the typography of other controls.
"""
from pathlib import Path
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
def main():
font = FontBuilder(1000, isTTF=True)
glyphs = [".notdef", "zeroWidthSpace"]
font.setupGlyphOrder(glyphs)
font.setupCharacterMap({0x200B: "zeroWidthSpace"})
font.setupGlyf({name: TTGlyphPen(None).glyph() for name in glyphs})
font.setupHorizontalMetrics({name: (0, 0) for name in glyphs})
font.setupHorizontalHeader(ascent=1000, descent=-250, lineGap=250)
font.setupOS2(
version=4,
sTypoAscender=1000,
sTypoDescender=-250,
sTypoLineGap=250,
usWinAscent=1000,
usWinDescent=250,
fsSelection=0xC0, # Regular, use typographic metrics.
)
font.setupNameTable({
"familyName": "CrossDesk Message Spacing",
"styleName": "Regular",
"uniqueFontIdentifier": "CrossDesk-Message-Spacing-1.0",
"fullName": "CrossDesk Message Spacing",
"psName": "CrossDeskMessageSpacing-Regular",
"version": "Version 1.0",
"copyright": "Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.",
})
font.setupPost()
# Fixed timestamps keep the generated asset reproducible.
font.font["head"].created = font.font["head"].modified = 3874867200
font.font.recalcTimestamp = False
font.save(Path(__file__).with_name("message_spacing.ttf"))
if __name__ == "__main__":
main()
@@ -23,10 +23,11 @@ struct TranslationRow {
X(messages, u8"消息", "Messages", u8"Сообщения") \
X(message_unread, u8"未读", "Unread", u8"Не прочитано") \
X(message_read, u8"已读", "Read", u8"Прочитано") \
X(message_read_all, u8"全部已读", "Mark all as read", u8"Отметить всё прочитанным") \
X(message_empty, u8"暂无公告", "No announcements", u8"Нет объявлений") \
X(message_select, u8"选择公告查看详情", "Select an announcement to read", u8"Выберите объявление") \
X(message_loading, u8"正在加载…", "Loading…", u8"Загрузка…") \
X(message_read_save_error, u8"无法保存本地阅读状态,请重新打开公告重试。", "Unable to save reading status locally. Open the announcement again to retry.", u8"Не удалось сохранить статус чтения. Откройте объявление снова.") \
X(message_read_save_error, u8"无法保存本地阅读状态,请重试。", "Unable to save reading status locally. Please try again.", u8"Не удалось сохранить статус чтения. Повторите попытку.") \
X(message_close, u8"关闭", "Close", u8"Закрыть") \
X(message_delete, u8"删除", "Delete", u8"Удалить") \
X(message_delete_title, u8"删除公告", "Delete announcement", u8"Удалить объявление") \
@@ -154,6 +154,29 @@ bool AnnouncementInbox::Read(int64_t id, int64_t revision) {
}
return false;
}
bool AnnouncementInbox::ReadAll() {
std::lock_guard<std::mutex> lock(mutex_);
if (catalog_revision_ < 0) return false;
auto updated = reads_;
// Use the complete catalog, including announcements whose bodies have not
// been paged in. Persist once before changing any visible reading state.
for (const auto& [id, revision] : catalog_) {
if (!IsDismissed(id, revision)) updated[id] = revision;
}
if (updated == reads_) return true;
if (!SaveLocalState(updated, dismissed_)) {
state_.read_save_failed = true;
++state_.version;
return false;
}
reads_ = std::move(updated);
state_.read_save_failed = false;
state_.delete_failed = false;
UpdateReadingState();
++state_.version;
return true;
}
bool AnnouncementInbox::IsDismissed(int64_t id, int64_t revision) const {
const auto found = dismissed_.find(id);
return found != dismissed_.end() && found->second == revision;
@@ -38,6 +38,7 @@ class AnnouncementInbox {
void Refresh();
void LoadMore();
bool Read(int64_t id, int64_t revision);
bool ReadAll();
bool Dismiss(int64_t id, int64_t revision);
nlohmann::json TakePendingRequest(std::chrono::steady_clock::time_point now =
std::chrono::steady_clock::now());
+104 -95
View File
@@ -5,13 +5,14 @@ import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
import { DeviceGroupDialog, DeviceGroupOption } from "device_group_dialog.slint";
import { GalleryView, GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
import { GalleryNetworkDialog, GalleryNetworkDevice } from "gallery_network_dialog.slint";
import { SettingsButton, SettingsSelect, SettingsGroup, SettingsRow, SettingsToggleRow } from "settings_controls.slint";
import { SettingsDialog, SettingsButton, SettingsSelect, SettingsGroup, SettingsRow, SettingsToggleRow } from "settings_controls.slint";
import { GallerySettings } from "gallery_settings.slint";
import { GroupFilter } from "group_filter.slint";
import { ScrollView } from "std-widgets.slint";
import { RoundedLineEdit } from "rounded_line_edit.slint";
import { AnnouncementItem, MessagePanel, MessageStrings } from "message_panel.slint";
import { FontAwesomeIcons, IconButton, CompactComboBox, ImGuiCheckBox, MiniToggleSwitch, CompactButton, PrimaryButton, SecondaryButton, ToggleSwitch, SectionTitle, DialogSurface, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
import "../assets/fonts/message_spacing.ttf";
export struct RecentConnection {
remote-id: string,
@@ -344,6 +345,7 @@ export component MainWindow inherits Window {
in property <bool> announcement-loaded;
in-out property <bool> messages-open: false;
in property <string> announcement-selected-id;
in property <string> announcement-selected-revision;
in property <string> announcement-selected-title;
in property <styled-text> announcement-selected-body;
in property <string> announcement-selected-date;
@@ -352,6 +354,7 @@ export component MainWindow inherits Window {
callback open-announcement-link(string);
callback delete-announcement(string, string);
callback refresh-announcements;
callback read-all-announcements;
callback load-more-announcements;
in property <bool> update-available: false;
in property <UpdateCheckStatus> update-check-status: idle;
@@ -500,6 +503,10 @@ export component MainWindow inherits Window {
if root.messages-open { focus-sink.focus(); }
}
changed about-open => {
if root.about-open { focus-sink.focus(); }
}
reset-remote-id => {
root.remote-id-input = "";
remote-id-editor.focus();
@@ -713,6 +720,10 @@ export component MainWindow inherits Window {
}
return accept;
}
if root.about-open && !root.update-open {
if event.text == Key.Escape { root.about-open = false; }
return accept;
}
if root.messages-open {
if event.text == Key.Escape { root.messages-open = false; }
return accept;
@@ -1477,14 +1488,14 @@ export component MainWindow inherits Window {
}
if root.messages-open: Rectangle {
background: #00000042;
background: #15223850;
z: 40;
TouchArea { clicked => { focus-sink.focus(); } }
MessagePanel {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 520px;
height: 330px;
width: min(592px, parent.width - 32px);
height: min(400px, parent.height - 24px);
items: root.announcements;
unread-count: root.announcement-unread; total: root.announcement-total;
loading: root.announcement-loading; failed: root.announcement-failed; loaded: root.announcement-loaded;
@@ -1492,12 +1503,14 @@ export component MainWindow inherits Window {
delete-failed: root.announcement-delete-failed;
connected: root.signal-connected;
selected-id: root.announcement-selected-id; selected-title: root.announcement-selected-title;
selected-revision: root.announcement-selected-revision;
selected-body: root.announcement-selected-body; selected-date: root.announcement-selected-date;
selected-read: root.announcement-selected-read;
open-message(id, revision) => { root.open-announcement(id, revision); }
open-link(url) => { root.open-announcement-link(url); }
delete-message(id, revision) => { root.delete-announcement(id, revision); }
load-more => { root.load-more-announcements(); }
read-all => { root.read-all-announcements(); }
close => { root.messages-open = false; }
}
}
@@ -1941,113 +1954,107 @@ export component MainWindow inherits Window {
}
if root.about-open: Rectangle {
background: transparent;
background: #15223850;
z: 46;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
SettingsDialog {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 320px;
height: max(216px, about-content.preferred-height);
border-radius: UiRadius.surface;
width: min(400px, parent.width - 40px);
height: self.content-top + about-content.preferred-height + 24px;
title: UiStrings.about;
close-label: MessageStrings.close;
close => { root.about-open = false; }
Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: 21px;
background: #d3d3d3;
border-top-left-radius: UiRadius.surface;
border-top-right-radius: UiRadius.surface;
z: 5;
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
// Grow for update actions and wrapped translations while keeping
// the footer below the version and check result.
// Content-driven height keeps translated update states and actions
// above the footer without reserving an empty update section.
about-content := VerticalLayout {
padding: 16px;
padding-top: 35px;
spacing: 12px;
alignment: space-between;
VerticalLayout {
vertical-stretch: 0;
spacing: 4px;
Text {
text: "CrossDesk " + root.current-version;
color: #202124;
font-size: ImGuiFontStyle.base;
font-weight: 600;
min-height: 18px;
overflow: elide;
x: 24px; y: parent.content-top; width: parent.width - 48px;
height: self.preferred-height;
padding: 0px; spacing: 20px;
Rectangle {
height: max(96px, product-info.preferred-height + 32px);
background: WorkbenchStyle.surface;
border-radius: UiRadius.surface;
border-width: 1px; border-color: WorkbenchStyle.border;
Image {
x: 16px; y: (parent.height - self.height) / 2;
width: 56px; height: 56px;
source: @image-url("../../../resources/linux/crossdesk_128x128.png");
image-fit: contain;
}
if root.update-available: Rectangle {
height: max(18px, version-link.preferred-height);
accessible-role: button;
accessible-label: UiStrings.release-notes + " " + root.latest-version;
accessible-action-default => { version-touch.clicked(); }
version-link := Text {
x: 0px;
y: 0px;
width: parent.width;
height: parent.height;
text: UiStrings.update-available + ": " + root.latest-version;
color: version-touch.has-hover ? #174ea6 : #334fd1;
font-size: ImGuiFontStyle.body;
vertical-alignment: center;
horizontal-alignment: left;
wrap: word-wrap;
}
version-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.about-open = false; root.update-open = true; }
product-info := VerticalLayout {
x: 88px; y: 16px; width: parent.width - 104px; height: parent.height - 32px;
alignment: center; spacing: 5px;
Text { text: "CrossDesk"; font-size: 22px; font-weight: 600; color: WorkbenchStyle.text; }
Text {
text: UiStrings.version + " " + root.current-version;
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
}
}
if root.update-available: HorizontalLayout {
alignment: start;
padding-top: 4px;
CompactButton {
width: max(76px, release-notes-button-metrics.preferred-width + 20px);
text: UiStrings.view-release-notes;
}
if root.update-available || root.update-status-text != "": Rectangle {
height: update-summary.preferred-height + 24px;
background: root.update-check-status == UpdateCheckStatus.failed ? #fef2f2 : WorkbenchStyle.field-surface;
border-radius: UiRadius.control;
border-width: 1px;
border-color: root.update-check-status == UpdateCheckStatus.failed ? #f3d4d1 : WorkbenchStyle.separator;
update-summary := VerticalLayout {
x: 12px; y: 12px; width: parent.width - 24px;
height: self.preferred-height;
padding: 0px; spacing: 10px;
if root.update-available: Rectangle {
height: version-link.preferred-height;
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { self.clicked(); }
clicked => { root.about-open = false; root.update-open = true; }
release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: ImGuiFontStyle.body; }
accessible-label: UiStrings.release-notes + " " + root.latest-version;
accessible-action-default => { version-touch.clicked(); }
version-link := Text {
width: parent.width;
text: UiStrings.update-available + ": " + root.latest-version;
color: version-touch.has-hover ? #254cc0 : WorkbenchStyle.accent;
font-size: 13px; font-weight: 500; wrap: word-wrap;
}
version-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.about-open = false; root.update-open = true; }
}
}
if root.update-status-text != "": Text {
text: root.update-status-text;
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e
: root.update-check-status == UpdateCheckStatus.up-to-date ? WorkbenchStyle.success : WorkbenchStyle.muted;
font-size: 12px; wrap: word-wrap;
}
if root.update-available: HorizontalLayout {
alignment: start;
SettingsButton {
width: max(116px, release-notes-button-metrics.preferred-width + 24px);
text: UiStrings.view-release-notes;
clicked => { root.about-open = false; root.update-open = true; }
release-notes-button-metrics := Text { visible: false; text: UiStrings.view-release-notes; font-size: 12px; }
}
}
}
if root.update-status-text != "": Text {
text: root.update-status-text;
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e : #61656c;
font-size: ImGuiFontStyle.body;
min-height: 18px;
wrap: word-wrap;
}
}
VerticalLayout {
vertical-stretch: 0;
spacing: 3px;
Rectangle { height: 1px; background: #e6e8eb; }
Text { height: 16px; text: UiStrings.copyright; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
Text { height: 16px; text: UiStrings.license; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
spacing: 5px;
Text { text: UiStrings.copyright; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
Text { text: UiStrings.license; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap; }
}
HorizontalLayout {
vertical-stretch: 0;
alignment: end;
spacing: 8px;
CompactButton {
width: max(76px, check-button-metrics.preferred-width + 20px);
text: UiStrings.check-for-updates;
enabled: root.update-check-status != UpdateCheckStatus.checking;
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { if self.enabled { root.check-for-updates(); } }
clicked => { root.check-for-updates(); }
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: ImGuiFontStyle.body; }
VerticalLayout {
spacing: 16px;
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
HorizontalLayout {
alignment: end; spacing: 8px;
SettingsButton {
width: max(116px, check-button-metrics.preferred-width + 24px);
text: UiStrings.check-for-updates;
enabled: root.update-check-status != UpdateCheckStatus.checking;
clicked => { root.check-for-updates(); }
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: 12px; }
}
SettingsButton { width: 80px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
CompactButton { width: 48px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
}
}
}
@@ -2151,6 +2158,8 @@ export component MainWindow inherits Window {
text: block.content;
default-color: WorkbenchStyle.text;
default-font-size: 13px;
// Match the 1.5em line height of message bodies.
default-font-family: "CrossDesk Message Spacing";
}
}
}
+162 -143
View File
@@ -1,6 +1,9 @@
import { UiRadius } from "ui_radius.slint";
import { ScrollView } from "std-widgets.slint";
import { CompactButton, DialogSurface, IconButton, FontAwesomeIcons, ImGuiFontStyle } from "common.slint";
import { IconButton, FontAwesomeIcons } from "common.slint";
import { SettingsDialog, SettingsButton } from "settings_controls.slint";
import { WorkbenchStyle } from "workbench_controls.slint";
import "../assets/fonts/message_spacing.ttf";
export struct AnnouncementItem {
id: string,
@@ -14,10 +17,11 @@ export global MessageStrings {
in-out property <string> messages: "Messages";
in-out property <string> unread: "Unread";
in-out property <string> read: "Read";
in-out property <string> read-all: "Mark all as read";
in-out property <string> empty: "No announcements";
in-out property <string> select-message: "Select an announcement to read";
in-out property <string> loading: "Loading…";
in-out property <string> read-save-error: "Unable to save reading status locally. Open the announcement again to retry.";
in-out property <string> read-save-error: "Unable to save reading status locally. Please try again.";
in-out property <string> close: "Close";
in-out property <string> delete-message: "Delete";
in-out property <string> delete-title: "Delete announcement";
@@ -26,7 +30,7 @@ export global MessageStrings {
in-out property <string> cancel: "Cancel";
}
export component MessagePanel inherits DialogSurface {
export component MessagePanel inherits SettingsDialog {
in property <[AnnouncementItem]> items;
in property <int> unread-count;
in property <int> total;
@@ -37,6 +41,7 @@ export component MessagePanel inherits DialogSurface {
in property <bool> loaded;
in property <bool> connected;
in property <string> selected-id;
in property <string> selected-revision;
in property <string> selected-title;
in property <styled-text> selected-body;
in property <string> selected-date;
@@ -45,11 +50,9 @@ export component MessagePanel inherits DialogSurface {
callback open-link(string);
callback delete-message(string, string);
callback load-more;
callback close;
callback read-all;
private property <string> notice-text: root.delete-failed ? MessageStrings.delete-error
: root.read-save-failed ? MessageStrings.read-save-error : "";
private property <length> content-top: title-bar.height + (root.notice-text == "" ? 14px : 50px);
private property <color> separator-color: #e6e8eb;
private property <string> pending-delete-id;
private property <string> pending-delete-revision;
function request-delete(id: string, revision: string) {
@@ -68,136 +71,151 @@ export component MessagePanel inherits DialogSurface {
return reject;
}
}
background: white;
border-radius: UiRadius.surface;
TouchArea { }
title-bar := Rectangle {
x: 0px; y: 0px; width: parent.width; height: 21px;
background: #d3d3d3; border-top-left-radius: UiRadius.surface; border-top-right-radius: UiRadius.surface;
message-heading := Text {
x: 6px; width: parent.width - 100px; height: parent.height;
text: MessageStrings.messages;
font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124;
vertical-alignment: center; overflow: elide;
}
if root.unread-count > 0: Rectangle {
x: message-heading.x + message-heading.preferred-width + 8px;
y: (parent.height - self.height) / 2; height: 16px;
width: max(16px, unread-count-label.preferred-width + 8px);
border-radius: 8px; background: #eaf2ff;
accessible-role: text;
accessible-label: MessageStrings.unread + " " + root.unread-count;
unread-count-label := Text {
text: "" + root.unread-count; color: #2563eb;
font-size: ImGuiFontStyle.small; font-weight: 600;
horizontal-alignment: center; vertical-alignment: center;
accessible-role: none;
}
}
if root.loading: Text {
x: parent.width - 150px; width: 110px; height: parent.height;
text: MessageStrings.loading; color: #61656c; font-size: ImGuiFontStyle.small;
horizontal-alignment: right; vertical-alignment: center;
}
IconButton {
x: parent.width - 25px; y: 2px; width: 19px; height: parent.height - 4px;
border-radius: UiRadius.control;
icon: FontAwesomeIcons.xmark; icon-size: ImGuiFontStyle.base;
icon-color: #202124;
hover-background: #bfc2c7; pressed-background: #b2b2b2;
accessible-role: button;
accessible-label: MessageStrings.close;
accessible-action-default => { root.close(); }
clicked => { root.close(); }
title: MessageStrings.messages;
close-label: MessageStrings.close;
badge-count: root.unread-count;
badge-label: MessageStrings.unread + " " + root.unread-count;
header-status: root.loading ? MessageStrings.loading : "";
action-text: MessageStrings.read-all;
action-enabled: root.loaded && !root.loading && root.unread-count > 0;
action => { root.read-all(); }
notice := Rectangle {
x: 24px; y: root.content-top; width: parent.width - 48px;
height: root.notice-text == "" ? 0px : notice-label.preferred-height + 20px;
visible: root.notice-text != "";
border-radius: UiRadius.control; background: #fef2f2;
notice-label := Text {
x: 10px; y: 10px; width: parent.width - 20px;
height: self.preferred-height;
text: root.notice-text; font-size: 12px; wrap: word-wrap; color: #b3261e;
}
}
if root.notice-text != "": Rectangle {
x: 16px; y: title-bar.height + 10px; width: parent.width - 32px; height: 28px;
border-radius: UiRadius.surface;
background: #fef2f2;
Text {
x: 8px; width: parent.width - 16px; height: parent.height;
text: root.notice-text; font-size: ImGuiFontStyle.body; overflow: elide;
color: #b3261e;
vertical-alignment: center;
message-content := Rectangle {
x: 24px; y: root.content-top + (root.notice-text == "" ? 0px : notice.height + 12px);
width: parent.width - 48px; height: parent.height - self.y - 24px;
background: WorkbenchStyle.surface;
border-radius: UiRadius.surface; border-width: 1px; border-color: WorkbenchStyle.border;
if root.items.length > 0 || root.selected-id != "": Rectangle {
x: 1px; y: 1px; width: content-divider.x - 1px; height: parent.height - 2px;
background: WorkbenchStyle.field-surface;
border-top-left-radius: UiRadius.surface - 1px;
border-bottom-left-radius: UiRadius.surface - 1px;
}
}
announcement-list := Rectangle {
x: 16px; y: root.content-top; width: 168px; height: parent.height - self.y - 16px;
announcements-scroll := ScrollView {
width: parent.width; height: parent.height;
VerticalLayout {
width: announcements-scroll.width; spacing: 0px; padding: 0px;
padding-right: 8px;
alignment: start;
for item[index] in root.items: Rectangle {
height: 54px;
background: root.selected-id == item.id ? #eaf2ff : entry-touch.has-hover ? #f0f1f2 : transparent;
border-radius: UiRadius.control;
Text { x: 8px; y: 6px; width: parent.width - 32px; height: 18px; text: item.title; overflow: elide; font-size: ImGuiFontStyle.base; font-weight: item.read ? 400 : 600; color: #202124; vertical-alignment: center; }
if !item.read: Rectangle {
x: parent.width - 17px; y: 13px; width: 5px; height: 5px;
border-radius: 2.5px; background: #2563eb;
accessible-role: none;
}
Text { x: 8px; y: 28px; width: parent.width - 36px; height: 16px; text: item.date; color: #61656c; font-size: ImGuiFontStyle.small; overflow: elide; vertical-alignment: center; }
accessible-role: button;
accessible-label: item.title + " " + (item.read ? MessageStrings.read : MessageStrings.unread);
accessible-action-default => { root.open-message(item.id, item.revision); }
entry-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-message(item.id, item.revision); } }
IconButton {
x: parent.width - 24px; y: 26px; width: 20px; height: 20px;
icon: FontAwesomeIcons.xmark; icon-size: ImGuiFontStyle.small;
icon-color: #61656c; danger: true;
announcement-list := Rectangle {
x: 8px; y: 8px; width: 176px; height: parent.height - 16px;
announcements-scroll := ScrollView {
width: parent.width; height: parent.height;
viewport-width: self.width - 10px;
viewport-height: announcement-entries.preferred-height;
horizontal-scrollbar-policy: always-off;
vertical-scrollbar-policy: as-needed;
announcement-entries := VerticalLayout {
width: announcements-scroll.viewport-width; spacing: 10px; padding: 0px;
alignment: start;
for item in root.items: Rectangle {
height: 64px;
background: root.selected-id == item.id ? #e8efff
: entry-touch.pressed ? WorkbenchStyle.pressed-surface
: entry-touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface;
border-radius: UiRadius.control;
border-width: 1px;
border-color: root.selected-id == item.id ? #b5c9f5 : WorkbenchStyle.border;
Text {
x: 10px; y: 9px; width: parent.width - 30px; height: 20px;
text: item.title; overflow: elide; font-size: 13px;
font-weight: item.read ? 400 : 600;
color: root.selected-id == item.id ? WorkbenchStyle.accent : WorkbenchStyle.text;
vertical-alignment: center;
}
if !item.read: Rectangle {
x: parent.width - 15px; y: 16px; width: 6px; height: 6px;
border-radius: 3px; background: WorkbenchStyle.accent;
accessible-role: none;
}
Text {
x: 10px; y: 35px; width: parent.width - 20px; height: 18px;
text: item.date; color: WorkbenchStyle.muted; font-size: 11px;
overflow: elide; vertical-alignment: center;
}
accessible-role: button;
accessible-label: MessageStrings.delete-message + " " + item.title;
accessible-action-default => { root.request-delete(item.id, item.revision); }
clicked => { root.request-delete(item.id, item.revision); }
}
if index < root.items.length - 1: Rectangle {
x: 8px; y: parent.height - 1px;
width: parent.width - 16px; height: 1px;
background: root.separator-color;
accessible-role: none;
accessible-label: item.title + " " + (item.read ? MessageStrings.read : MessageStrings.unread);
accessible-action-default => { root.open-message(item.id, item.revision); }
entry-touch := TouchArea {
mouse-cursor: pointer;
clicked => { root.open-message(item.id, item.revision); }
}
}
}
}
}
}
if root.items.length == 0 && root.total == 0 && root.loaded && root.connected && !root.loading && !root.failed: Text {
x: 16px; y: root.content-top; width: parent.width - 32px; height: announcement-list.height;
text: MessageStrings.empty; wrap: word-wrap;
color: #61656c; font-size: ImGuiFontStyle.body;
horizontal-alignment: center; vertical-alignment: center;
}
content-divider := Rectangle {
x: announcement-list.x + announcement-list.width + 12px;
y: root.content-top; width: 1px; height: announcement-list.height;
background: root.separator-color;
visible: root.items.length > 0 || root.selected-id != "";
accessible-role: none;
}
announcement-details := Rectangle {
x: content-divider.x + content-divider.width + 12px;
y: root.content-top; width: parent.width - self.x - 16px; height: announcement-list.height;
if root.selected-id == "" && root.items.length > 0: Text { x: 8px; width: parent.width - 16px; text: MessageStrings.select-message; color: #61656c; font-size: ImGuiFontStyle.body; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; }
if root.selected-id != "": details-scroll := ScrollView {
width: parent.width; height: parent.height;
VerticalLayout {
width: details-scroll.width; padding: 8px; padding-top: 6px; padding-right: 16px;
spacing: 12px; alignment: start;
VerticalLayout {
vertical-stretch: 0; spacing: 4px;
Text { vertical-stretch: 0; min-height: 18px; text: root.selected-title; wrap: word-wrap; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
Text { vertical-stretch: 0; min-height: 16px; text: root.selected-date; accessible-label: root.selected-date + " · " + (root.selected-read ? MessageStrings.read : MessageStrings.unread); color: #61656c; font-size: ImGuiFontStyle.small; wrap: word-wrap; vertical-alignment: center; }
}
Rectangle { height: 1px; background: root.separator-color; }
StyledText {
vertical-stretch: 0;
text: root.selected-body;
default-color: #202124; default-font-size: ImGuiFontStyle.body;
link-color: #2563eb;
link-clicked(url) => { root.open-link(url); }
if root.items.length == 0 && root.total == 0 && root.loaded && root.connected && !root.loading && !root.failed: Text {
x: 24px; width: parent.width - 48px; height: parent.height;
text: MessageStrings.empty; wrap: word-wrap;
color: WorkbenchStyle.muted; font-size: 13px;
horizontal-alignment: center; vertical-alignment: center;
}
content-divider := Rectangle {
x: announcement-list.x + announcement-list.width + 8px;
y: 1px; width: 1px; height: parent.height - 2px;
background: WorkbenchStyle.separator;
visible: root.items.length > 0 || root.selected-id != "";
accessible-role: none;
}
announcement-details := Rectangle {
x: content-divider.x + 17px; y: 16px;
width: parent.width - self.x - 16px; height: parent.height - 32px;
clip: true;
if root.selected-id == "" && root.items.length > 0: Text {
width: parent.width; height: parent.height;
text: MessageStrings.select-message; color: WorkbenchStyle.muted; font-size: 13px;
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
}
if root.selected-id != "": details-scroll := ScrollView {
width: parent.width; height: parent.height;
viewport-width: self.width - 12px;
viewport-height: details-content.preferred-height;
horizontal-scrollbar-policy: always-off;
vertical-scrollbar-policy: as-needed;
details-content := VerticalLayout {
width: details-scroll.viewport-width; padding: 0px; padding-bottom: 4px;
spacing: 14px; alignment: start;
Rectangle {
height: max(28px, selected-heading.preferred-height);
selected-heading := VerticalLayout {
x: 0px; y: 0px;
width: parent.width - 40px; height: self.preferred-height;
padding: 0px; spacing: 6px;
Text {
vertical-stretch: 0; text: root.selected-title; wrap: word-wrap;
font-size: 16px; font-weight: 600; color: WorkbenchStyle.text;
}
Text {
vertical-stretch: 0; text: root.selected-date;
accessible-label: root.selected-date + " · " + (root.selected-read ? MessageStrings.read : MessageStrings.unread);
color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap;
}
}
IconButton {
x: parent.width - self.width; y: 0px; width: 28px; height: 28px;
icon: FontAwesomeIcons.trash-can; icon-size: 12px;
icon-color: WorkbenchStyle.muted; danger: true;
accessible-role: button;
accessible-label: MessageStrings.delete-message + " " + root.selected-title;
accessible-action-default => { root.request-delete(root.selected-id, root.selected-revision); }
clicked => { root.request-delete(root.selected-id, root.selected-revision); }
}
}
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
StyledText {
vertical-stretch: 0;
text: root.selected-body;
default-color: WorkbenchStyle.text; default-font-size: 13px;
// Metrics-only family provides 1.5em line height; visible
// glyphs continue to use Slint's normal font fallback.
default-font-family: "CrossDesk Message Spacing";
link-color: WorkbenchStyle.accent;
link-clicked(url) => { root.open-link(url); }
}
}
}
}
@@ -211,30 +229,31 @@ export component MessagePanel inherits DialogSurface {
triggered => { root.load-more(); }
}
if root.pending-delete-id != "": Rectangle {
background: #00000042; border-radius: root.border-radius; z: 30;
background: #15223850; border-radius: root.border-radius; z: 30;
TouchArea { clicked => { confirmation-keys.focus(); } }
DialogSurface {
SettingsDialog {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: 230px; height: confirmation-content.preferred-height;
border-radius: root.border-radius;
TouchArea { clicked => { confirmation-keys.focus(); } }
width: min(360px, root.width - 48px);
height: self.content-top + confirmation-content.preferred-height + 24px;
title: MessageStrings.delete-title;
close-label: MessageStrings.cancel;
close => { root.pending-delete-id = ""; }
confirmation-content := VerticalLayout {
padding: 16px;
padding-top: 26px;
padding-bottom: 26px;
spacing: 20px;
Text { text: MessageStrings.delete-confirmation; font-size: ImGuiFontStyle.base; color: #202124; wrap: word-wrap; horizontal-alignment: center; }
x: 24px; y: parent.content-top; width: parent.width - 48px;
height: self.preferred-height;
padding: 0px; spacing: 24px;
Text { text: MessageStrings.delete-confirmation; font-size: 13px; color: WorkbenchStyle.text; wrap: word-wrap; }
HorizontalLayout {
alignment: center; spacing: 8px;
cancel-delete-button := CompactButton {
width: 72px; text: MessageStrings.cancel;
alignment: end; spacing: 8px;
cancel-delete-button := SettingsButton {
width: 88px; text: MessageStrings.cancel;
accessible-role: button; accessible-label: MessageStrings.cancel;
accessible-action-default => { cancel-delete-button.clicked(); }
clicked => { root.pending-delete-id = ""; }
}
confirm-delete-button := CompactButton {
width: 72px; text: MessageStrings.delete-message; primary: true;
confirm-delete-button := SettingsButton {
width: 88px; text: MessageStrings.delete-message; primary: true;
accessible-role: button; accessible-label: MessageStrings.delete-title;
accessible-action-default => { confirm-delete-button.clicked(); }
clicked => {
@@ -1,5 +1,5 @@
import { UiRadius } from "ui_radius.slint";
import { CompactComboBox, ToggleSwitch } from "common.slint";
import { CompactComboBox, ToggleSwitch, DialogSurface, IconButton, FontAwesomeIcons } from "common.slint";
import { WorkbenchStyle } from "workbench_controls.slint";
// Shared visual treatment for the global settings pages.
@@ -28,6 +28,66 @@ export component SettingsButton inherits Rectangle {
touch := TouchArea { enabled: root.enabled; mouse-cursor: pointer; clicked => { root.clicked(); } }
}
export component SettingsDialog inherits DialogSurface {
in property <string> title;
in property <string> close-label;
in property <int> badge-count: 0;
in property <string> badge-label;
in property <string> header-status;
in property <string> action-text;
in property <bool> action-enabled: true;
out property <length> content-top: 64px;
callback action();
callback close();
background: #f8fafc;
border-color: WorkbenchStyle.separator;
drop-shadow-color: #14294424;
drop-shadow-blur: 24px;
drop-shadow-offset-y: 8px;
TouchArea { }
HorizontalLayout {
x: 24px; y: 18px; width: parent.width - 48px; height: 28px;
spacing: 10px;
Text {
text: root.title; font-size: 20px; font-weight: 600;
color: WorkbenchStyle.text; vertical-alignment: center;
}
if root.badge-count > 0: Rectangle {
width: max(24px, count-label.preferred-width + 14px); height: 22px;
border-radius: 11px; background: #e8efff;
accessible-role: text; accessible-label: root.badge-label;
count-label := Text {
text: "" + root.badge-count; color: WorkbenchStyle.accent;
font-size: 11px; font-weight: 600;
horizontal-alignment: center; vertical-alignment: center;
accessible-role: none;
}
}
Rectangle { horizontal-stretch: 1; }
if root.header-status != "": Text {
text: root.header-status; color: WorkbenchStyle.muted;
font-size: 11px; vertical-alignment: center;
}
if root.action-text != "": SettingsButton {
width: action-label-metrics.preferred-width + 24px; height: 28px;
text: root.action-text; enabled: root.action-enabled;
clicked => { root.action(); }
action-label-metrics := Text { visible: false; text: root.action-text; font-size: 12px; }
}
IconButton {
width: 28px; height: 28px;
icon: FontAwesomeIcons.xmark; icon-size: 14px;
icon-color: WorkbenchStyle.muted;
hover-background: WorkbenchStyle.hover-surface;
pressed-background: WorkbenchStyle.pressed-surface;
accessible-role: button; accessible-label: root.close-label;
accessible-action-default => { root.close(); }
clicked => { root.close(); }
}
}
@children
}
export component SettingsSelect inherits CompactComboBox {
subtle: true;
show-arrow: false;
@@ -46,6 +46,7 @@ inline int ApplyMainWindowStrings(
messages.set_messages(Text(localization::messages[language]));
messages.set_unread(Text(localization::message_unread[language]));
messages.set_read(Text(localization::message_read[language]));
messages.set_read_all(Text(localization::message_read_all[language]));
messages.set_empty(Text(localization::message_empty[language]));
messages.set_select_message(Text(localization::message_select[language]));
messages.set_loading(Text(localization::message_loading[language]));