mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-19 10:23:10 +08:00
[feat] add announcement dismissal and links
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-29
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _ANNOUNCEMENT_TEXT_H_
|
||||
#define _ANNOUNCEMENT_TEXT_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk::announcement_text {
|
||||
|
||||
inline size_t WebSchemeLength(std::string_view url) {
|
||||
for (const std::string_view prefix : {"https://", "http://"}) {
|
||||
if (url.size() >= prefix.size() &&
|
||||
std::equal(prefix.begin(), prefix.end(), url.begin(),
|
||||
[](char expected, unsigned char actual) {
|
||||
return expected == std::tolower(actual);
|
||||
})) {
|
||||
return prefix.size();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline bool IsWebUrl(std::string_view url) {
|
||||
const size_t scheme = WebSchemeLength(url);
|
||||
if (!scheme || url.size() == scheme ||
|
||||
std::any_of(url.begin(), url.end(), [](unsigned char ch) {
|
||||
return ch <= 0x20 || ch == 0x7f || ch == '\\' || ch == '<' ||
|
||||
ch == '>' || ch == '"';
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
const auto authority =
|
||||
url.substr(scheme, url.find_first_of("/?#", scheme) - scheme);
|
||||
return !authority.empty() && authority.back() != '@';
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline size_t UrlEnd(std::string_view text, size_t start) {
|
||||
// Do not include surrounding prose punctuation in a bare URL. Balanced
|
||||
// parentheses and IPv6 brackets are part of the address when present.
|
||||
const std::string_view punctuation =
|
||||
reinterpret_cast<const char*>(u8",。;:!?、()【】《》「」『』“”‘’");
|
||||
int parentheses = 0, brackets = 0;
|
||||
size_t end = start;
|
||||
for (; end < text.size(); ++end) {
|
||||
const unsigned char ch = text[end];
|
||||
if (ch <= 0x20 || ch == 0x7f ||
|
||||
std::string_view("<>\"'`\\{}").find(ch) != std::string_view::npos ||
|
||||
(ch >= 0xe0 && end + 3 <= text.size() &&
|
||||
punctuation.find(text.substr(end, 3)) != std::string_view::npos)) {
|
||||
break;
|
||||
}
|
||||
if (ch == '(') ++parentheses;
|
||||
if (ch == ')' && parentheses-- == 0) break;
|
||||
if (ch == '[') ++brackets;
|
||||
if (ch == ']' && brackets-- == 0) break;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
inline void AppendLiteral(std::string& output, std::string_view text) {
|
||||
for (const unsigned char ch : text) {
|
||||
// Entity-encode spaces so leading indentation is not parsed as code.
|
||||
if (ch == ' ') {
|
||||
output += " ";
|
||||
} else if (ch == '\t') {
|
||||
output += "	";
|
||||
} else {
|
||||
if (ch < 0x80 && std::ispunct(ch)) output += '\\';
|
||||
output += static_cast<char>(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void AppendLink(std::string& output, std::string_view label,
|
||||
std::string_view url) {
|
||||
output += '[';
|
||||
AppendLiteral(output, label);
|
||||
output += "](<";
|
||||
for (const char ch : url) {
|
||||
// Markdown decodes entities in link destinations; keep query strings exact.
|
||||
if (ch == '&')
|
||||
output += "&";
|
||||
else
|
||||
output += ch;
|
||||
}
|
||||
output += ">)";
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline slint::StyledText Format(std::string_view body) {
|
||||
std::string markdown;
|
||||
markdown.reserve(body.size());
|
||||
bool has_link = false;
|
||||
for (size_t pos = 0; pos < body.size();) {
|
||||
if (body[pos] == '[') {
|
||||
const size_t label_end = body.find("](", pos + 1);
|
||||
if (label_end != std::string_view::npos && label_end > pos + 1 &&
|
||||
body.substr(pos + 1, label_end - pos - 1).find_first_of("[]\r\n") ==
|
||||
std::string_view::npos &&
|
||||
WebSchemeLength(body.substr(label_end + 2))) {
|
||||
const size_t end = detail::UrlEnd(body, label_end + 2);
|
||||
const auto url = body.substr(label_end + 2, end - label_end - 2);
|
||||
if (end < body.size() && body[end] == ')' && IsWebUrl(url)) {
|
||||
detail::AppendLink(markdown,
|
||||
body.substr(pos + 1, label_end - pos - 1), url);
|
||||
pos = end + 1;
|
||||
has_link = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (WebSchemeLength(body.substr(pos))) {
|
||||
size_t end = detail::UrlEnd(body, pos);
|
||||
while (end > pos && std::string_view(".,;:!?").find(body[end - 1]) !=
|
||||
std::string_view::npos) {
|
||||
--end;
|
||||
}
|
||||
const auto url = body.substr(pos, end - pos);
|
||||
if (IsWebUrl(url)) {
|
||||
detail::AppendLink(markdown, url, url);
|
||||
pos = end;
|
||||
has_link = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (body[pos] == '\r' && pos + 1 < body.size() && body[pos + 1] == '\n') {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
if (body[pos] == '\n') {
|
||||
// Preserve empty lines instead of letting Markdown collapse them.
|
||||
if (markdown.empty() || markdown.back() == '\n') markdown += " ";
|
||||
markdown += '\n';
|
||||
} else {
|
||||
detail::AppendLiteral(markdown, body.substr(pos, 1));
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
if (has_link) {
|
||||
if (markdown.back() == '\n') markdown += " ";
|
||||
if (const auto styled = slint::StyledText::from_markdown(markdown))
|
||||
return *styled;
|
||||
}
|
||||
return slint::StyledText::from_plain_text(body);
|
||||
}
|
||||
|
||||
} // namespace crossdesk::announcement_text
|
||||
|
||||
#endif
|
||||
@@ -339,6 +339,7 @@ export component MiniToggleSwitch inherits Rectangle {
|
||||
|
||||
export component CompactButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <length> text-size: ImGuiFontStyle.body;
|
||||
in property <bool> primary: false;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
@@ -355,7 +356,7 @@ export component CompactButton inherits Rectangle {
|
||||
Text {
|
||||
text: root.text;
|
||||
color: root.enabled ? #24272d : #8e9298;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
font-size: root.text-size;
|
||||
overflow: elide;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
|
||||
@@ -66,6 +66,7 @@ export global UiStrings {
|
||||
in-out property <string> signal-disconnected: "Signal server disconnected";
|
||||
in-out property <string> tls-error: "Signal server TLS certificate error";
|
||||
in-out property <string> update-available: "A new version is available";
|
||||
in-out property <string> new-version: "New version";
|
||||
in-out property <string> check-for-updates: "Check for updates";
|
||||
in-out property <string> checking-for-updates: "Checking for updates…";
|
||||
in-out property <string> up-to-date: "You’re up to date";
|
||||
@@ -276,21 +277,25 @@ export component MainWindow inherits Window {
|
||||
in property <[RecentConnection]> recent-connections;
|
||||
in property <[AnnouncementItem]> announcements;
|
||||
in property <int> announcement-unread;
|
||||
private property <string> announcement-unread-label:
|
||||
root.announcement-unread > 99 ? "99+" : "" + root.announcement-unread;
|
||||
in property <int> announcement-total;
|
||||
in property <int> announcement-offset;
|
||||
in property <bool> announcement-loading;
|
||||
in property <bool> announcement-failed;
|
||||
in property <bool> announcement-read-save-failed;
|
||||
in property <bool> announcement-delete-failed;
|
||||
in property <bool> announcement-loaded;
|
||||
in-out property <bool> messages-open: false;
|
||||
in property <string> announcement-selected-id;
|
||||
in property <string> announcement-selected-title;
|
||||
in property <string> announcement-selected-body;
|
||||
in property <styled-text> announcement-selected-body;
|
||||
in property <string> announcement-selected-date;
|
||||
in property <bool> announcement-selected-read;
|
||||
callback open-announcement(string, string);
|
||||
callback open-announcement-link(string);
|
||||
callback delete-announcement(string, string);
|
||||
callback refresh-announcements;
|
||||
callback announcement-page(int);
|
||||
callback load-more-announcements;
|
||||
in property <bool> update-available: false;
|
||||
in property <UpdateCheckStatus> update-check-status: idle;
|
||||
private property <string> update-status-text:
|
||||
@@ -394,8 +399,6 @@ export component MainWindow inherits Window {
|
||||
private property <string> delete-remote-id: "";
|
||||
private property <bool> copy-toast: false;
|
||||
private property <bool> compact-language: root.language-index == 0;
|
||||
private property <bool> update-icon-blink-on:
|
||||
root.update-available && mod(animation-tick(), 2s) < 1s;
|
||||
private property <bool> recent-tooltip-visible: false;
|
||||
private property <string> recent-tooltip-name: "";
|
||||
private property <string> recent-tooltip-host: "";
|
||||
@@ -677,23 +680,37 @@ export component MainWindow inherits Window {
|
||||
icon-size: 14px;
|
||||
hover-background: #e1e5eb;
|
||||
pressed-background: #cbd1d9;
|
||||
tooltip: root.announcement-unread > 0 ? MessageStrings.unread + " " + root.announcement-unread : "";
|
||||
tooltip: root.announcement-unread > 0
|
||||
? MessageStrings.unread + " " + root.announcement-unread + (root.update-available ? " · " + UiStrings.update-available : "")
|
||||
: root.update-available ? UiStrings.update-available : "";
|
||||
z: 8;
|
||||
clicked => { root.menu-open = !root.menu-open; }
|
||||
}
|
||||
if root.update-available && !root.menu-open && root.update-icon-blink-on: Text {
|
||||
x: menu-button.x + (root.announcement-unread > 0 ? -18px : 16px);
|
||||
y: menu-button.y + 1px;
|
||||
// Unread messages take precedence so the menu has only one indicator.
|
||||
if root.update-available && root.announcement-unread <= 0: Rectangle {
|
||||
x: menu-button.x + menu-button.width - self.width + 4px;
|
||||
y: menu-button.y - 1px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text: FontAwesomeIcons.circle-arrow-up;
|
||||
color: #24272d;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 13px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
border-radius: 8px;
|
||||
border-width: 1px;
|
||||
border-color: white;
|
||||
background: #2563eb;
|
||||
opacity: root.menu-open || mod(animation-tick(), 2s) < 1s ? 1 : 0;
|
||||
z: 9;
|
||||
accessible-role: button;
|
||||
accessible-label: UiStrings.update-available;
|
||||
accessible-action-default => { root.menu-open = !root.menu-open; }
|
||||
Text {
|
||||
text: "!"; color: white;
|
||||
font-size: 12px; font-weight: 700;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.menu-open = !root.menu-open; }
|
||||
}
|
||||
}
|
||||
if root.announcement-unread > 0: Rectangle {
|
||||
x: menu-button.x + menu-button.width - self.width + 4px;
|
||||
@@ -709,7 +726,7 @@ export component MainWindow inherits Window {
|
||||
accessible-label: MessageStrings.messages + " · " + MessageStrings.unread + " " + root.announcement-unread;
|
||||
accessible-action-default => { root.menu-open = !root.menu-open; }
|
||||
unread-badge-label := Text {
|
||||
text: root.announcement-unread > 99 ? "99+" : "" + root.announcement-unread;
|
||||
text: root.announcement-unread-label;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
@@ -1200,9 +1217,9 @@ export component MainWindow inherits Window {
|
||||
if root.menu-open: Rectangle {
|
||||
x: menu-button.x + menu-button.width - self.width;
|
||||
y: menu-button.y + menu-button.height + 2px;
|
||||
width: max(menu-settings-label.preferred-width,
|
||||
max(menu-messages-label.preferred-width,
|
||||
menu-about-label.preferred-width + (root.update-available ? 25px : 0px))) + 28px;
|
||||
width: max(80px, max(menu-settings-label.preferred-width,
|
||||
max(menu-messages-label.preferred-width + (root.announcement-unread > 0 ? menu-message-badge.width + 8px : 0px),
|
||||
menu-about-label.preferred-width + (root.update-available ? menu-update-badge.width + 8px : 0px))) + 24px);
|
||||
height: 93px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
@@ -1224,7 +1241,7 @@ export component MainWindow inherits Window {
|
||||
accessible-label: UiStrings.settings;
|
||||
accessible-action-default => { settings-touch.clicked(); }
|
||||
menu-settings-label := Text {
|
||||
x: 9px;
|
||||
x: 7px;
|
||||
height: parent.height;
|
||||
text: UiStrings.settings;
|
||||
color: #2d3138;
|
||||
@@ -1242,16 +1259,32 @@ export component MainWindow inherits Window {
|
||||
: messages-touch.has-hover ? #f0f1f2 : white;
|
||||
border-radius: 3px;
|
||||
accessible-role: button;
|
||||
accessible-label: menu-messages-label.text;
|
||||
accessible-label: MessageStrings.messages + (root.announcement-unread > 0 ? " · " + MessageStrings.unread + " " + root.announcement-unread : "");
|
||||
accessible-action-default => { messages-touch.clicked(); }
|
||||
menu-messages-label := Text {
|
||||
x: 9px;
|
||||
x: 7px;
|
||||
height: parent.height;
|
||||
text: MessageStrings.messages + (root.announcement-unread > 0 ? " (" + root.announcement-unread + ")" : "");
|
||||
text: MessageStrings.messages;
|
||||
color: #2d3138;
|
||||
vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
menu-message-badge := Rectangle {
|
||||
visible: root.announcement-unread > 0;
|
||||
x: parent.width - self.width - 7px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: max(18px, menu-message-count.preferred-width + 10px);
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background: #fee4e2;
|
||||
accessible-role: none;
|
||||
menu-message-count := Text {
|
||||
text: root.announcement-unread-label;
|
||||
font-size: 10px; font-weight: 600; color: #b42318;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
}
|
||||
messages-touch := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
@@ -1269,28 +1302,31 @@ export component MainWindow inherits Window {
|
||||
border-width: 0px;
|
||||
border-radius: 3px;
|
||||
accessible-role: button;
|
||||
accessible-label: UiStrings.about;
|
||||
accessible-label: UiStrings.about + (root.update-available ? " · " + UiStrings.update-available : "");
|
||||
accessible-action-default => { about-touch.clicked(); }
|
||||
menu-about-label := Text {
|
||||
x: 9px;
|
||||
x: 7px;
|
||||
height: parent.height;
|
||||
text: UiStrings.about;
|
||||
color: #2d3138;
|
||||
vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
if root.update-available && root.update-icon-blink-on: Text {
|
||||
x: menu-about-label.x + menu-about-label.preferred-width + 7px;
|
||||
width: 18px;
|
||||
height: parent.height;
|
||||
text: FontAwesomeIcons.circle-arrow-up;
|
||||
color: #2d3138;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
menu-update-badge := Rectangle {
|
||||
visible: root.update-available;
|
||||
x: parent.width - self.width - 7px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: menu-update-label.preferred-width + 12px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background: #dbeafe;
|
||||
accessible-role: none;
|
||||
menu-update-label := Text {
|
||||
text: UiStrings.new-version;
|
||||
font-size: 11px; font-weight: 400; color: #1d4ed8;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
}
|
||||
about-touch := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
@@ -1326,16 +1362,18 @@ export component MainWindow inherits Window {
|
||||
width: 520px;
|
||||
height: 330px;
|
||||
items: root.announcements;
|
||||
unread-count: root.announcement-unread; total: root.announcement-total; offset: root.announcement-offset;
|
||||
unread-count: root.announcement-unread; total: root.announcement-total;
|
||||
loading: root.announcement-loading; failed: root.announcement-failed; loaded: root.announcement-loaded;
|
||||
read-save-failed: root.announcement-read-save-failed;
|
||||
delete-failed: root.announcement-delete-failed;
|
||||
connected: root.signal-connected;
|
||||
selected-id: root.announcement-selected-id; selected-title: root.announcement-selected-title;
|
||||
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); }
|
||||
refresh => { root.refresh-announcements(); }
|
||||
change-page(offset) => { root.announcement-page(offset); }
|
||||
open-link(url) => { root.open-announcement-link(url); }
|
||||
delete-message(id, revision) => { root.delete-announcement(id, revision); }
|
||||
load-more => { root.load-more-announcements(); }
|
||||
close => { root.messages-open = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
import { CompactButton, DialogSurface, ImGuiFontStyle } from "common.slint";
|
||||
import { CompactButton, DialogSurface, IconButton, FontAwesomeIcons } from "common.slint";
|
||||
|
||||
export struct AnnouncementItem {
|
||||
id: string,
|
||||
@@ -15,40 +15,88 @@ export global MessageStrings {
|
||||
in-out property <string> read: "Read";
|
||||
in-out property <string> empty: "No announcements";
|
||||
in-out property <string> select-message: "Select an announcement to read";
|
||||
in-out property <string> refresh: "Refresh";
|
||||
in-out property <string> loading: "Loading…";
|
||||
in-out property <string> unavailable: "Unable to sync. Retry or check server support.";
|
||||
in-out property <string> offline: "Offline. Reading status is saved on this device.";
|
||||
in-out property <string> read-save-error: "Unable to save reading status locally. Open the announcement again to retry.";
|
||||
in-out property <string> close: "Close";
|
||||
in-out property <string> delete-message: "Delete";
|
||||
in-out property <string> delete-title: "Delete announcement";
|
||||
in-out property <string> delete-confirmation: "Delete this announcement?";
|
||||
in-out property <string> delete-error: "Unable to delete this announcement. Please try again.";
|
||||
in-out property <string> cancel: "Cancel";
|
||||
}
|
||||
|
||||
export component MessagePanel inherits DialogSurface {
|
||||
in property <[AnnouncementItem]> items;
|
||||
in property <int> unread-count;
|
||||
in property <int> total;
|
||||
in property <int> offset;
|
||||
in property <bool> loading;
|
||||
in property <bool> failed;
|
||||
in property <bool> read-save-failed;
|
||||
in property <bool> delete-failed;
|
||||
in property <bool> loaded;
|
||||
in property <bool> connected;
|
||||
in property <string> selected-id;
|
||||
in property <string> selected-title;
|
||||
in property <string> selected-body;
|
||||
in property <styled-text> selected-body;
|
||||
in property <string> selected-date;
|
||||
in property <bool> selected-read;
|
||||
callback open-message(string, string);
|
||||
callback refresh;
|
||||
callback change-page(int);
|
||||
callback open-link(string);
|
||||
callback delete-message(string, string);
|
||||
callback load-more;
|
||||
callback close;
|
||||
private property <string> notice-text: root.delete-failed ? MessageStrings.delete-error
|
||||
: root.read-save-failed ? MessageStrings.read-save-error : "";
|
||||
private property <length> content-top: root.notice-text == "" ? 42px : 78px;
|
||||
private property <string> pending-delete-id;
|
||||
private property <string> pending-delete-revision;
|
||||
function request-delete(id: string, revision: string) {
|
||||
root.pending-delete-id = id;
|
||||
root.pending-delete-revision = revision;
|
||||
confirmation-keys.focus();
|
||||
}
|
||||
confirmation-keys := FocusScope {
|
||||
width: 0px; height: 0px;
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape {
|
||||
if root.pending-delete-id != "" { root.pending-delete-id = ""; }
|
||||
else { root.close(); }
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
background: #f7f8fa;
|
||||
TouchArea { }
|
||||
Rectangle {
|
||||
x: 1px; y: 1px; width: parent.width - 2px; height: 30px;
|
||||
background: #e7ebf0; border-radius: 7px;
|
||||
Rectangle { y: parent.height - 7px; height: 7px; background: #e7ebf0; }
|
||||
Text { x: 12px; width: parent.width - 54px; height: parent.height; text: MessageStrings.messages + " · " + MessageStrings.unread + " " + root.unread-count; font-size: 13px; font-weight: 600; color: #202d3d; vertical-alignment: center; overflow: elide; }
|
||||
message-heading := Text {
|
||||
x: 12px; width: parent.width - 100px; height: parent.height;
|
||||
text: MessageStrings.messages;
|
||||
font-size: 14px; font-weight: 400; color: #111827;
|
||||
vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
if root.unread-count > 0: Rectangle {
|
||||
x: message-heading.x + message-heading.preferred-width + 8px;
|
||||
y: 5px; height: 20px;
|
||||
width: max(20px, unread-count-label.preferred-width + 12px);
|
||||
border-radius: 10px; background: #dbeafe;
|
||||
accessible-role: text;
|
||||
accessible-label: MessageStrings.unread + " " + root.unread-count;
|
||||
unread-count-label := Text {
|
||||
text: "" + root.unread-count; color: #1d4ed8;
|
||||
font-size: 12px; 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: #4b5563; font-size: 12px;
|
||||
horizontal-alignment: right; vertical-alignment: center;
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - 31px; y: 3px; width: 25px; height: 24px;
|
||||
border-radius: 3px;
|
||||
@@ -60,46 +108,114 @@ export component MessagePanel inherits DialogSurface {
|
||||
close-touch := TouchArea { mouse-cursor: pointer; clicked => { root.close(); } }
|
||||
}
|
||||
}
|
||||
if root.notice-text != "": Rectangle {
|
||||
x: 12px; y: 40px; width: parent.width - 24px; height: 28px;
|
||||
border-radius: 4px;
|
||||
background: #fef2f2;
|
||||
Text {
|
||||
x: 8px; width: parent.width - 16px; height: parent.height;
|
||||
text: root.notice-text; font-size: 12px; overflow: elide;
|
||||
color: #b42318;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
x: 12px; y: 42px; width: 180px; height: parent.height - 100px;
|
||||
x: 12px; y: root.content-top; width: 180px; height: parent.height - self.y - 12px;
|
||||
background: white; border-width: 1px; border-color: #e2e5e9;
|
||||
ScrollView {
|
||||
announcements-scroll := ScrollView {
|
||||
width: parent.width; height: parent.height;
|
||||
VerticalLayout {
|
||||
width: 160px; spacing: 3px; padding: 5px;
|
||||
for item in root.items: Rectangle {
|
||||
height: 72px; width: 150px;
|
||||
height: 56px; width: 150px;
|
||||
background: root.selected-id == item.id ? #eaf2ff : entry-touch.has-hover ? #f1f5fa : white;
|
||||
border-radius: 4px;
|
||||
Text { x: 8px; y: 5px; width: parent.width - 16px; height: 22px; text: item.title; overflow: elide; font-weight: item.read ? 400 : 600; color: #202d3d; vertical-alignment: center; }
|
||||
Text { x: 8px; y: 32px; width: parent.width - 16px; text: item.read ? MessageStrings.read : MessageStrings.unread; color: item.read ? #718096 : #2463c7; font-size: 11px; overflow: elide; }
|
||||
Text { x: 8px; y: 50px; width: parent.width - 16px; text: item.date; color: #718096; font-size: 10px; overflow: elide; }
|
||||
Text { x: 8px; y: 5px; width: parent.width - 28px; height: 22px; text: item.title; overflow: elide; font-size: 14px; font-weight: item.read ? 400 : 600; color: item.read ? #374151 : #111827; vertical-alignment: center; }
|
||||
if !item.read: Rectangle {
|
||||
x: parent.width - 18px; y: 13px; width: 6px; height: 6px;
|
||||
border-radius: 3px; background: #2563eb;
|
||||
accessible-role: none;
|
||||
}
|
||||
Text { x: 8px; y: 32px; width: parent.width - 42px; text: item.date; color: #4b5563; font-size: 12px; overflow: elide; }
|
||||
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 - 25px; y: 29px; width: 20px; height: 20px;
|
||||
icon: FontAwesomeIcons.xmark; icon-size: 12px;
|
||||
icon-color: #667085; danger: true;
|
||||
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 root.items.length == 0 && root.loaded && !root.loading: Text { text: MessageStrings.empty; color: #718096; horizontal-alignment: center; vertical-alignment: center; }
|
||||
if root.items.length == 0 && root.total == 0 && root.loaded && root.connected && !root.loading && !root.failed: Text { x: 8px; width: parent.width - 16px; text: MessageStrings.empty; wrap: word-wrap; color: #4b5563; font-size: 14px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
}
|
||||
Rectangle {
|
||||
x: 202px; y: 42px; width: parent.width - 214px; height: parent.height - 100px;
|
||||
x: 202px; y: root.content-top; width: parent.width - 214px; height: parent.height - self.y - 12px;
|
||||
background: white; border-width: 1px; border-color: #e2e5e9;
|
||||
if root.selected-id == "": Text { x: 14px; width: parent.width - 28px; text: MessageStrings.select-message; color: #718096; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; }
|
||||
if root.selected-id == "" && root.items.length > 0: Text { x: 14px; width: parent.width - 28px; text: MessageStrings.select-message; color: #4b5563; font-size: 14px; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; }
|
||||
if root.selected-id != "": ScrollView {
|
||||
width: parent.width; height: parent.height;
|
||||
VerticalLayout {
|
||||
width: root.width - 234px; padding: 12px; spacing: 10px; alignment: start;
|
||||
Text { vertical-stretch: 0; text: root.selected-title; wrap: word-wrap; font-size: 14px; font-weight: 600; color: #202d3d; }
|
||||
Text { vertical-stretch: 0; text: root.selected-date + " · " + (root.selected-read ? MessageStrings.read : MessageStrings.unread); color: #718096; font-size: 11px; wrap: word-wrap; }
|
||||
Text { vertical-stretch: 0; text: root.selected-body; wrap: word-wrap; color: #303b4a; font-size: ImGuiFontStyle.body; }
|
||||
Text { vertical-stretch: 0; text: root.selected-title; wrap: word-wrap; font-size: 16px; font-weight: 600; color: #202124; }
|
||||
Text { vertical-stretch: 0; text: root.selected-date; accessible-label: root.selected-date + " · " + (root.selected-read ? MessageStrings.read : MessageStrings.unread); color: #4b5563; font-size: 12px; wrap: word-wrap; }
|
||||
StyledText {
|
||||
vertical-stretch: 0;
|
||||
text: root.selected-body;
|
||||
default-color: #202124; default-font-size: 14px;
|
||||
link-color: #2563eb;
|
||||
link-clicked(url) => { root.open-link(url); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
interval: 100ms;
|
||||
running: root.connected && !root.loading && !root.failed
|
||||
&& root.pending-delete-id == "" && root.items.length > 0
|
||||
&& root.items.length < root.total && announcements-scroll.viewport-height > 0px
|
||||
&& announcements-scroll.viewport-height + announcements-scroll.viewport-y <= announcements-scroll.height + 80px;
|
||||
triggered => { root.load-more(); }
|
||||
}
|
||||
if root.pending-delete-id != "": Rectangle {
|
||||
background: #00000042; border-radius: 8px; z: 30;
|
||||
TouchArea { clicked => { confirmation-keys.focus(); } }
|
||||
DialogSurface {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: 230px; height: confirmation-content.preferred-height;
|
||||
TouchArea { clicked => { confirmation-keys.focus(); } }
|
||||
confirmation-content := VerticalLayout {
|
||||
padding: 16px;
|
||||
padding-top: 26px;
|
||||
padding-bottom: 26px;
|
||||
spacing: 20px;
|
||||
Text { text: MessageStrings.delete-confirmation; font-size: 14px; color: #202124; wrap: word-wrap; horizontal-alignment: center; }
|
||||
HorizontalLayout {
|
||||
alignment: center; spacing: 8px;
|
||||
cancel-delete-button := CompactButton {
|
||||
width: 72px; text-size: 13px; 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-size: 13px; text: MessageStrings.delete-message; primary: true;
|
||||
accessible-role: button; accessible-label: MessageStrings.delete-title;
|
||||
accessible-action-default => { confirm-delete-button.clicked(); }
|
||||
clicked => {
|
||||
root.delete-message(root.pending-delete-id, root.pending-delete-revision);
|
||||
root.pending-delete-id = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CompactButton { x: 12px; y: parent.height - 50px; width: 30px; text: "‹"; enabled: root.connected && root.offset > 0 && !root.loading; clicked => { root.change-page(root.offset - 20); } }
|
||||
Text { x: 46px; y: parent.height - 50px; width: 112px; height: 24px; text: (root.total > 0 ? root.offset + 1 : 0) + "–" + min(root.offset + 20, root.total) + " / " + root.total; color: #718096; horizontal-alignment: center; vertical-alignment: center; font-size: 11px; }
|
||||
CompactButton { x: 162px; y: parent.height - 50px; width: 30px; text: "›"; enabled: root.connected && root.offset + 20 < root.total && !root.loading; clicked => { root.change-page(root.offset + 20); } }
|
||||
CompactButton { x: parent.width - 82px; y: parent.height - 50px; width: 70px; text: MessageStrings.refresh; enabled: root.connected && !root.loading; clicked => { root.refresh(); } }
|
||||
Text { x: 12px; y: parent.height - 25px; width: parent.width - 24px; height: 22px; text: root.read-save-failed ? MessageStrings.read-save-error : !root.connected ? MessageStrings.offline : root.loading ? MessageStrings.loading : root.failed ? MessageStrings.unavailable : ""; color: root.failed || root.read-save-failed ? #ba342b : #718096; font-size: 11px; overflow: elide; }
|
||||
}
|
||||
|
||||
@@ -32,12 +32,14 @@ inline int ApplyMainWindowStrings(
|
||||
messages.set_read(Text(localization::message_read[language]));
|
||||
messages.set_empty(Text(localization::message_empty[language]));
|
||||
messages.set_select_message(Text(localization::message_select[language]));
|
||||
messages.set_refresh(Text(localization::message_refresh[language]));
|
||||
messages.set_loading(Text(localization::message_loading[language]));
|
||||
messages.set_unavailable(Text(localization::message_unavailable[language]));
|
||||
messages.set_offline(Text(localization::message_offline[language]));
|
||||
messages.set_read_save_error(Text(localization::message_read_save_error[language]));
|
||||
messages.set_close(Text(localization::message_close[language]));
|
||||
messages.set_delete_message(Text(localization::message_delete[language]));
|
||||
messages.set_delete_title(Text(localization::message_delete_title[language]));
|
||||
messages.set_delete_confirmation(Text(localization::message_delete_confirmation[language]));
|
||||
messages.set_delete_error(Text(localization::message_delete_error[language]));
|
||||
messages.set_cancel(Text(localization::cancel[language]));
|
||||
|
||||
strings.set_local_desktop(Text(localization::local_desktop[language]));
|
||||
strings.set_remote_desktop(Text(localization::remote_desktop[language]));
|
||||
@@ -95,6 +97,7 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
|
||||
strings.set_update_available(
|
||||
Text(localization::new_version_available[language]));
|
||||
strings.set_new_version(Text(localization::new_version[language]));
|
||||
strings.set_check_for_updates(Text(localization::check_for_updates[language]));
|
||||
strings.set_checking_for_updates(
|
||||
Text(localization::checking_for_updates[language]));
|
||||
|
||||
Reference in New Issue
Block a user