mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[ui] unify settings dialogs and improve announcement controls
This commit is contained in:
@@ -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()
|
||||
Binary file not shown.
@@ -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());
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
|
||||
Reference in New Issue
Block a user