mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-24 13:48:51 +08:00
[feat] add desktop license and source information pages
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import { ScrollView, TextEdit } from "std-widgets.slint";
|
||||
import { SettingsDialog, SettingsButton } from "settings_controls.slint";
|
||||
import { WorkbenchStyle } from "workbench_controls.slint";
|
||||
import { UiRadius } from "ui_radius.slint";
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
import { FontAwesomeIcons } from "common.slint";
|
||||
import "../assets/fonts/message_spacing.ttf";
|
||||
|
||||
export enum AboutPage { overview, components, component, document, source }
|
||||
export struct AboutDocument { title: string, text: string }
|
||||
export struct AboutComponent {
|
||||
id: string,
|
||||
name: string,
|
||||
version: string,
|
||||
license: string,
|
||||
source-url: string,
|
||||
documents: [AboutDocument],
|
||||
}
|
||||
|
||||
export global AboutStrings {
|
||||
out property <string> software-license: GalleryStrings.tr("软件许可", "Software license", "Лицензия программы");
|
||||
out property <string> components: GalleryStrings.tr("开源组件", "Open-source components", "Компоненты с открытым кодом");
|
||||
out property <string> source: GalleryStrings.tr("源码与构建说明", "Source code and build instructions", "Исходный код и сборка");
|
||||
out property <string> notices: GalleryStrings.tr("许可与版权声明", "Licenses and copyright notices", "Лицензии и авторские права");
|
||||
out property <string> view-source: GalleryStrings.tr("查看源码", "View source code", "Открыть исходный код");
|
||||
out property <string> back: GalleryStrings.tr("返回", "Back", "Назад");
|
||||
}
|
||||
|
||||
export component AboutNavigationGroup inherits Rectangle {
|
||||
height: rows.preferred-height + 2px;
|
||||
background: WorkbenchStyle.surface;
|
||||
border-radius: UiRadius.control;
|
||||
border-width: 1px; border-color: WorkbenchStyle.separator;
|
||||
clip: true;
|
||||
rows := VerticalLayout {
|
||||
x: 1px; y: 1px; width: parent.width - 2px;
|
||||
height: self.preferred-height; padding: 0px; spacing: 0px;
|
||||
@children
|
||||
}
|
||||
}
|
||||
|
||||
export component AboutRowSeparator inherits Rectangle {
|
||||
height: 1px;
|
||||
Rectangle {
|
||||
x: 52px; width: parent.width - 68px; height: 1px;
|
||||
background: WorkbenchStyle.separator;
|
||||
}
|
||||
}
|
||||
|
||||
export component AboutNavigationRow inherits Rectangle {
|
||||
in property <string> title;
|
||||
in property <string> subtitle;
|
||||
in property <string> icon;
|
||||
in property <string> detail;
|
||||
in property <bool> grouped: false;
|
||||
in property <bool> external: false;
|
||||
callback clicked;
|
||||
min-width: 0px;
|
||||
min-height: max(44px, labels.preferred-height + 20px);
|
||||
background: touch.pressed ? WorkbenchStyle.pressed-surface
|
||||
: touch.has-hover ? WorkbenchStyle.hover-surface : WorkbenchStyle.surface;
|
||||
border-radius: root.grouped ? 0px : UiRadius.control;
|
||||
border-width: root.grouped ? 0px : 1px; border-color: WorkbenchStyle.separator;
|
||||
accessible-role: button;
|
||||
accessible-label: root.title + (root.subtitle != "" ? ", " + root.subtitle : "")
|
||||
+ (root.detail != "" ? ", " + root.detail : "");
|
||||
accessible-action-default => { root.clicked(); }
|
||||
if root.icon != "": Rectangle {
|
||||
x: 12px; y: (parent.height - self.height) / 2;
|
||||
width: 28px; height: 28px;
|
||||
border-radius: UiRadius.control;
|
||||
background: WorkbenchStyle.field-surface;
|
||||
Text {
|
||||
text: root.icon; font-family: "Font Awesome 6 Free"; font-weight: 900;
|
||||
font-size: 13px; color: WorkbenchStyle.muted;
|
||||
horizontal-alignment: center; vertical-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
}
|
||||
labels := VerticalLayout {
|
||||
x: root.icon != "" ? 52px : 14px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: parent.width - self.x - 36px - (root.detail != "" ? detail-label.width + 12px : 0px);
|
||||
height: self.preferred-height; spacing: 4px;
|
||||
Text { text: root.title; color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap; }
|
||||
if root.subtitle != "": Text {
|
||||
text: root.subtitle; color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
detail-label := Text {
|
||||
x: parent.width - self.width - 36px; height: parent.height;
|
||||
width: min(100px, self.preferred-width);
|
||||
text: root.detail; color: WorkbenchStyle.muted; font-size: 11px;
|
||||
vertical-alignment: center; overflow: elide;
|
||||
accessible-role: none;
|
||||
}
|
||||
Text {
|
||||
x: parent.width - 30px; width: 18px; height: parent.height;
|
||||
text: root.external ? FontAwesomeIcons.arrow-up-right-from-square : FontAwesomeIcons.angle-right;
|
||||
font-family: "Font Awesome 6 Free"; font-weight: 900;
|
||||
color: touch.has-hover ? WorkbenchStyle.accent : WorkbenchStyle.muted;
|
||||
font-size: 11px; vertical-alignment: center; horizontal-alignment: center;
|
||||
accessible-role: none;
|
||||
}
|
||||
touch := TouchArea { mouse-cursor: pointer; clicked => { root.clicked(); } }
|
||||
}
|
||||
|
||||
export component AboutPanel inherits SettingsDialog {
|
||||
show-close-button: false;
|
||||
in-out property <AboutPage> page: AboutPage.components;
|
||||
in-out property <int> component-index: 0;
|
||||
private property <int> document-index: 0;
|
||||
in property <[AboutComponent]> components;
|
||||
in property <int> openfec-index: -1;
|
||||
in property <string> revision;
|
||||
in property <string> source-url;
|
||||
in property <string> build-url;
|
||||
private property <AboutComponent> selected: root.components[root.component-index];
|
||||
callback back;
|
||||
callback open-link(string);
|
||||
title: root.page == AboutPage.components ? AboutStrings.components
|
||||
: root.page == AboutPage.source ? AboutStrings.source
|
||||
: root.page == AboutPage.document ? AboutStrings.notices
|
||||
: root.selected.name;
|
||||
|
||||
if root.page != AboutPage.document: ScrollView {
|
||||
x: 24px; y: root.content-top; width: parent.width - 48px + 22px;
|
||||
height: footer.y - self.y - 16px;
|
||||
viewport-width: self.visible-width;
|
||||
viewport-height: content.preferred-height;
|
||||
horizontal-scrollbar-policy: always-off;
|
||||
vertical-scrollbar-policy: as-needed;
|
||||
content := VerticalLayout {
|
||||
width: parent.width; height: self.preferred-height;
|
||||
padding: 0px; padding-right: 22px; spacing: 10px;
|
||||
if root.page == AboutPage.components: VerticalLayout {
|
||||
padding: 0px; spacing: 10px;
|
||||
Text {
|
||||
text: GalleryStrings.tr("以下列出本版本的主要开源组件。各组件遵循各自的开源许可。", "Main open-source components used in this build. Each component is covered by its own license.", "Основные открытые компоненты этой сборки. Каждый распространяется по своей лицензии.");
|
||||
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
|
||||
}
|
||||
Rectangle {
|
||||
height: notice.preferred-height + 32px;
|
||||
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
||||
notice := VerticalLayout {
|
||||
x: 16px; y: 16px; width: parent.width - 32px;
|
||||
height: self.preferred-height; spacing: 12px;
|
||||
Text { text: GalleryStrings.tr("OpenFEC 告知", "OpenFEC notice", "Уведомление OpenFEC"); color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; }
|
||||
Text {
|
||||
text: GalleryStrings.tr("本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。", "This application uses OpenFEC for forward error correction under CeCILL-C 1.0, which provides limited warranties and limits liability. See the full license for details.", "Приложение использует OpenFEC для прямого исправления ошибок по лицензии CeCILL-C 1.0 с ограниченной гарантией и ответственностью. Подробности — в тексте лицензии.");
|
||||
color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap;
|
||||
font-family: "CrossDesk Message Spacing";
|
||||
}
|
||||
Text {
|
||||
text: "(c) Copyright 2009 - 2012 INRIA - All rights reserved";
|
||||
color: WorkbenchStyle.muted; font-size: 11px; wrap: word-wrap;
|
||||
font-family: "CrossDesk Message Spacing";
|
||||
}
|
||||
if root.openfec-index >= 0: AboutNavigationRow {
|
||||
title: GalleryStrings.tr("OpenFEC 许可与源码", "OpenFEC license and source", "Лицензия и исходный код OpenFEC");
|
||||
clicked => { root.component-index = root.openfec-index; root.page = AboutPage.component; }
|
||||
}
|
||||
}
|
||||
}
|
||||
for component[index] in root.components: VerticalLayout {
|
||||
padding: 0px; spacing: 0px;
|
||||
if component.id != "crossdesk" && component.id != "openfec": AboutNavigationRow {
|
||||
title: component.name;
|
||||
subtitle: component.version + " · " + component.license;
|
||||
clicked => { root.component-index = index; root.page = AboutPage.component; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.page == AboutPage.component: VerticalLayout {
|
||||
padding: 0px; spacing: 12px;
|
||||
Text { text: root.selected.version + " · " + root.selected.license; color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap; }
|
||||
AboutNavigationRow {
|
||||
title: AboutStrings.view-source; external: true;
|
||||
clicked => { root.open-link(root.selected.source-url); }
|
||||
}
|
||||
Text { text: AboutStrings.notices; color: WorkbenchStyle.text; font-size: 13px; font-weight: 600; }
|
||||
for document[index] in root.selected.documents: AboutNavigationRow {
|
||||
title: document.title;
|
||||
clicked => { root.document-index = index; root.page = AboutPage.document; }
|
||||
}
|
||||
if root.selected.documents.length == 0: Text {
|
||||
text: GalleryStrings.tr("本组件未内置许可文本,请通过“查看源码”查阅许可与版权声明。", "License text is not bundled for this component. Use View source code to read its license and copyright notices.", "Текст лицензии не включён. Откройте исходный код для просмотра лицензии и авторских прав.");
|
||||
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
if root.page == AboutPage.source: VerticalLayout {
|
||||
padding: 0px; spacing: 12px;
|
||||
Text {
|
||||
text: root.revision != "" ? GalleryStrings.tr("源码版本:", "Source revision: ", "Ревизия исходного кода: ") + root.revision
|
||||
: GalleryStrings.tr("本版本源码信息暂不可用。", "Source revision is unavailable for this build.", "Ревизия исходного кода этой сборки недоступна.");
|
||||
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
|
||||
}
|
||||
AboutNavigationRow { title: AboutStrings.view-source; external: true; clicked => { root.open-link(root.source-url); } }
|
||||
AboutNavigationRow {
|
||||
title: GalleryStrings.tr("构建说明", "Build instructions", "Инструкции по сборке"); external: true;
|
||||
clicked => { root.open-link(root.build-url); }
|
||||
}
|
||||
AboutNavigationRow {
|
||||
title: GalleryStrings.tr("项目主页", "Project website", "Сайт проекта"); external: true;
|
||||
clicked => { root.open-link("https://crossdesk.cn"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.page == AboutPage.document: VerticalLayout {
|
||||
x: 24px; y: root.content-top; width: parent.width - 48px;
|
||||
height: footer.y - self.y - 16px; spacing: 12px;
|
||||
Text { text: root.selected.documents[root.document-index].title; color: WorkbenchStyle.text; font-size: 13px; font-weight: 600; wrap: word-wrap; }
|
||||
TextEdit {
|
||||
text: root.selected.documents[root.document-index].text;
|
||||
read-only: true; wrap: word-wrap; font-size: 14px;
|
||||
// Use the same 1.5em line spacing as other long-form reading views.
|
||||
font-family: "CrossDesk Message Spacing";
|
||||
vertical-stretch: 1;
|
||||
key-pressed(event) => {
|
||||
if event.text == Key.Escape { root.back(); return accept; }
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
}
|
||||
footer := VerticalLayout {
|
||||
x: 24px; y: parent.height - self.height - 24px;
|
||||
width: parent.width - 48px; height: self.preferred-height; spacing: 12px;
|
||||
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
||||
HorizontalLayout {
|
||||
alignment: end;
|
||||
SettingsButton { text: AboutStrings.back; clicked => { root.back(); } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,11 @@ export global FontAwesomeIcons {
|
||||
out property <string> angle-right: "\u{f105}";
|
||||
out property <string> angle-up: "\u{f106}";
|
||||
out property <string> arrow-right-long: "\u{f178}";
|
||||
out property <string> arrow-up-right-from-square: "\u{f35d}";
|
||||
out property <string> bars: "\u{f0c9}";
|
||||
out property <string> check: "\u{f00c}";
|
||||
out property <string> circle-arrow-up: "\u{f0aa}";
|
||||
out property <string> code: "\u{f121}";
|
||||
out property <string> compress: "\u{f066}";
|
||||
out property <string> computer-mouse: "\u{f8cc}";
|
||||
out property <string> copy: "\u{f0c5}";
|
||||
@@ -42,8 +44,10 @@ export global FontAwesomeIcons {
|
||||
out property <string> expand: "\u{f065}";
|
||||
out property <string> eye: "\u{f06e}";
|
||||
out property <string> eye-slash: "\u{f070}";
|
||||
out property <string> file-lines: "\u{f15c}";
|
||||
out property <string> folder-open: "\u{f07c}";
|
||||
out property <string> keyboard: "\u{f11c}";
|
||||
out property <string> layer-group: "\u{f5fd}";
|
||||
out property <string> minus: "\u{f068}";
|
||||
out property <string> pen: "\u{f304}";
|
||||
out property <string> rotate: "\u{f2f1}";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Single AOT entry point. Generating one header for the entire UI avoids
|
||||
// duplicate imported-component definitions when C++ uses all three windows.
|
||||
export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
||||
export { AboutComponent, AboutDocument, AboutPage } from "about_panel.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, AutostartStatus, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint";
|
||||
import { UiRadius } from "ui_radius.slint";
|
||||
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
|
||||
import { GalleryStrings } from "device_strings.slint";
|
||||
@@ -536,6 +537,20 @@ export component MainWindow inherits Window {
|
||||
if !root.self-host-settings-open { root.self-host-settings-error = ""; }
|
||||
}
|
||||
in-out property <bool> about-open: false;
|
||||
in-out property <AboutPage> about-page: overview;
|
||||
private property <length> about-scroll-y: 0px;
|
||||
in-out property <int> about-component-index: 0;
|
||||
in property <[AboutComponent]> about-components;
|
||||
in property <int> about-openfec-index: -1;
|
||||
in property <string> about-revision;
|
||||
in property <string> about-source-url;
|
||||
in property <string> about-build-url;
|
||||
callback open-about-link(string);
|
||||
function about-back() {
|
||||
if root.about-page == AboutPage.document { root.about-page = AboutPage.component; }
|
||||
else if root.about-page == AboutPage.component && root.about-component-index != 0 { root.about-page = AboutPage.components; }
|
||||
else { root.about-page = AboutPage.overview; }
|
||||
}
|
||||
in-out property <bool> sponsorship-open: false;
|
||||
// Automatic prompts must not interrupt another dialog or menu.
|
||||
private property <bool> sponsorship-unobstructed: !root.gallery-mode
|
||||
@@ -627,6 +642,15 @@ export component MainWindow inherits Window {
|
||||
|
||||
changed about-open => {
|
||||
if root.about-open { root.dialog-focus-pending = true; }
|
||||
else {
|
||||
root.about-page = AboutPage.overview;
|
||||
root.about-component-index = 0;
|
||||
root.about-scroll-y = 0px;
|
||||
}
|
||||
}
|
||||
changed about-page => {
|
||||
// Restore dialog shortcuts after leaving the selectable license text.
|
||||
if root.about-open { root.dialog-focus-pending = true; }
|
||||
}
|
||||
|
||||
changed sponsorship-open => {
|
||||
@@ -855,7 +879,10 @@ export component MainWindow inherits Window {
|
||||
return accept;
|
||||
}
|
||||
if root.about-open && !root.update-open {
|
||||
if event.text == Key.Escape { root.about-open = false; }
|
||||
if event.text == Key.Escape {
|
||||
if root.about-page != AboutPage.overview { root.about-back(); }
|
||||
else { root.about-open = false; }
|
||||
}
|
||||
return accept;
|
||||
}
|
||||
if root.sponsorship-open && root.sponsorship-unobstructed {
|
||||
@@ -2365,17 +2392,36 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
|
||||
if root.about-open: Rectangle {
|
||||
// Use the same bounds throughout the About navigation stack.
|
||||
private property <length> dialog-width: min(480px, self.width - 40px);
|
||||
private property <length> dialog-height: min(520px, self.height - 32px);
|
||||
background: #15223850;
|
||||
z: 46;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
SettingsDialog {
|
||||
if root.about-page != AboutPage.overview: AboutPanel {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: min(480px, parent.width - 40px);
|
||||
width: parent.dialog-width;
|
||||
min-height: 0px; max-height: parent.height - 32px;
|
||||
height: parent.dialog-height;
|
||||
page <=> root.about-page;
|
||||
component-index <=> root.about-component-index;
|
||||
components: root.about-components;
|
||||
openfec-index: root.about-openfec-index;
|
||||
revision: root.about-revision;
|
||||
source-url: root.about-source-url;
|
||||
build-url: root.about-build-url;
|
||||
back => { root.about-back(); }
|
||||
open-link(url) => { root.open-about-link(url); }
|
||||
}
|
||||
if root.about-page == AboutPage.overview: SettingsDialog {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: parent.dialog-width;
|
||||
// The overlay excludes any custom titlebar on every platform.
|
||||
min-height: 0px; max-height: parent.height - 32px;
|
||||
// Keep the dialog stable as update status and release details change.
|
||||
height: min(488px, parent.height - 32px);
|
||||
height: parent.dialog-height;
|
||||
title: UiStrings.about;
|
||||
close-label: MessageStrings.close;
|
||||
close => { root.about-open = false; }
|
||||
@@ -2388,6 +2434,8 @@ export component MainWindow inherits Window {
|
||||
height: about-footer.y - self.y - 16px;
|
||||
viewport-width: self.visible-width;
|
||||
viewport-height: about-content.preferred-height;
|
||||
// Keep the position when navigation recreates the overview.
|
||||
viewport-y <=> root.about-scroll-y;
|
||||
horizontal-scrollbar-policy: always-off;
|
||||
vertical-scrollbar-policy: as-needed;
|
||||
about-content := VerticalLayout {
|
||||
@@ -2465,6 +2513,26 @@ export component MainWindow inherits Window {
|
||||
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; }
|
||||
}
|
||||
AboutNavigationGroup {
|
||||
AboutNavigationRow {
|
||||
grouped: true; icon: FontAwesomeIcons.file-lines;
|
||||
title: AboutStrings.software-license;
|
||||
detail: "GPL v3";
|
||||
clicked => { root.about-component-index = 0; root.about-page = AboutPage.component; }
|
||||
}
|
||||
AboutRowSeparator { }
|
||||
AboutNavigationRow {
|
||||
grouped: true; icon: FontAwesomeIcons.layer-group;
|
||||
title: AboutStrings.components;
|
||||
clicked => { root.about-page = AboutPage.components; }
|
||||
}
|
||||
AboutRowSeparator { }
|
||||
AboutNavigationRow {
|
||||
grouped: true; icon: FontAwesomeIcons.code;
|
||||
title: AboutStrings.source;
|
||||
clicked => { root.about-page = AboutPage.source; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
about-footer := VerticalLayout {
|
||||
@@ -2472,17 +2540,14 @@ export component MainWindow inherits Window {
|
||||
width: parent.width - 48px; height: self.preferred-height;
|
||||
spacing: 16px;
|
||||
Rectangle { height: 1px; background: WorkbenchStyle.separator; }
|
||||
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;
|
||||
}
|
||||
HorizontalLayout {
|
||||
alignment: end; spacing: 8px;
|
||||
// Status updates use the spare footer space without reflowing the content.
|
||||
Text {
|
||||
min-width: 0px; height: 32px; horizontal-stretch: 1;
|
||||
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; overflow: elide;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if root.update-available: WorkbenchButton {
|
||||
width: max(80px, about-update-button-metrics.preferred-width + 24px);
|
||||
height: 32px;
|
||||
|
||||
Reference in New Issue
Block a user