[feat] add standalone DeskHub window and resizable device table

This commit is contained in:
dijunkun
2026-10-14 02:37:06 +08:00
parent fd86dba212
commit 5199b2df11
11 changed files with 1113 additions and 620 deletions
File diff suppressed because it is too large Load Diff
@@ -50,7 +50,7 @@ private:
void InitializeUi();
void InitializeSystemTray();
bool MinimizeMainWindowToTray();
void BindMainCallbacks();
void BindMainCallbacks(bool gallery_window = false);
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
@@ -60,16 +60,21 @@ private:
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
void ChangeMainMode(bool gallery);
void ShowGalleryWindow();
void HideGalleryWindow();
void SyncWindowGeometry(bool gallery_window);
void ToggleGalleryFullscreen();
void RestoreMainWindowGeometry(bool before_show = false);
void RestoreWindowGeometry(bool before_show = false,
bool gallery_window = false);
void SyncGallery();
void GalleryAction(const std::string& action, const std::string& value);
void GalleryAction(const std::string& action, const std::string& value,
bool gallery_window = true);
bool AddGalleryDevice(const std::string& id, const std::string& password,
const std::string& group, bool remember_password);
bool ImportGalleryRecent(const std::string& group);
void DeviceGroupAction(const std::string& action, const std::string& value);
bool SaveDeviceGroups(const std::string& new_group);
void DeviceGroupAction(const std::string& action, const std::string& value,
bool gallery_window);
bool SaveDeviceGroups(const std::string& new_group, bool gallery_window);
void ReleaseControl();
void SyncAudioPlayback();
void SaveGalleryWindowState();
@@ -89,9 +94,9 @@ private:
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization();
void ResetSettingsUi();
bool ValidateSelfHostedSettingsFromUi();
bool SaveSettingsFromUi();
void ResetSettingsUi(bool gallery_window = false);
bool ValidateSelfHostedSettingsFromUi(bool gallery_window = false);
bool SaveSettingsFromUi(bool gallery_window = false);
void ApplySettingsFromConfig(bool reconnect);
#if _WIN32 && CROSSDESK_PORTABLE
void CheckPortableWindowsService();
@@ -22,26 +22,6 @@ struct WindowState {
std::optional<slint::LogicalSize> wall_size;
std::optional<slint::PhysicalPosition> wall_position, connection_position;
void BeginModeChange(bool wall, slint::PhysicalPosition position,
slint::PhysicalSize size, float scale, bool fullscreen,
bool maximized, float compact_height = 486) {
// Never capture a half-applied mode transition as the user's wall geometry.
Observe(!wall, position, size, scale, fullscreen, maximized,
compact_height);
if (wall) {
// Modes share the current desktop location. An old wall position must
// not undo a move made while the connection page was visible.
if (connection_position)
wall_position = connection_position;
} else {
// Fullscreen/maximized coordinates describe the screen, not the ordinary
// window. Return to the last normal anchor before restoring compact size.
if (wall_position)
connection_position = wall_position;
}
BeginRestore();
}
void Observe(bool wall, slint::PhysicalPosition position,
slint::PhysicalSize size, float scale, bool fullscreen,
bool maximized, float compact_height = 486) {
@@ -82,7 +82,39 @@ struct Workspace {
if (wall_ids_.count(m.id) && (group.empty() || m.group == group) &&
seen.insert(m.id).second)
ids.push_back(m.id);
return ids;
return OrderIds(ids);
}
// Order is independent of membership, group assignments and live sessions.
std::vector<std::string> OrderIds(const std::vector<std::string> &ids) const {
if (device_order_.empty())
return ids;
std::set<std::string> remaining(ids.begin(), ids.end());
std::vector<std::string> ordered;
ordered.reserve(ids.size());
for (const auto &id : device_order_)
if (remaining.erase(id)) ordered.push_back(id);
for (const auto &id : ids)
if (remaining.erase(id)) ordered.push_back(id);
return ordered;
}
bool SwapPositions(const std::string &source, const std::string &target,
const std::vector<std::string> &visible_ids) {
if (source == target || !ValidId(source) || !ValidId(target) ||
std::find(visible_ids.begin(), visible_ids.end(), source) == visible_ids.end() ||
std::find(visible_ids.begin(), visible_ids.end(), target) == visible_ids.end())
return false;
auto ordered = device_order_;
std::set<std::string> known(ordered.begin(), ordered.end());
// Include hidden groups before applying a swap in a filtered view.
for (const auto &id : Ids())
if (known.insert(id).second) ordered.push_back(id);
// Active-only devices can be arranged without enrolling them in the wall.
for (const auto &id : visible_ids)
if (ValidId(id) && known.insert(id).second) ordered.push_back(id);
std::iter_swap(std::find(ordered.begin(), ordered.end(), source),
std::find(ordered.begin(), ordered.end(), target));
device_order_ = std::move(ordered);
return true;
}
// History filtering uses group assignments, independently of wall enrollment.
// nullopt means all devices; an empty name means no named group.
@@ -147,6 +179,7 @@ struct Workspace {
if (group.empty()) {
// Removing from the wall leaves history's group assignments intact.
wall_ids_.erase(id);
std::erase(device_order_, id);
} else if (MatchesGroup(id, group)) {
// Removing the last named group leaves an ungrouped wall member.
// Reuse the editor path so enrollment and card order stay unchanged.
@@ -198,9 +231,19 @@ struct Workspace {
if (ValidId(id) && loaded.HasAssignments(id))
loaded.wall_ids_.insert(id);
}
if (doc.contains("device_order") && doc["device_order"].is_array()) {
std::set<std::string> seen;
for (const auto &value : doc["device_order"]) {
if (!value.is_string()) continue;
const auto id = value.get<std::string>();
if (ValidId(id) && seen.insert(id).second)
loaded.device_order_.push_back(id);
}
}
members = std::move(loaded.members);
groups = std::move(loaded.groups);
wall_ids_ = std::move(loaded.wall_ids_);
device_order_ = std::move(loaded.device_order_);
stream_settings = loaded.stream_settings;
return true;
} catch (...) {
@@ -212,6 +255,7 @@ struct Workspace {
nlohmann::json doc{{"version", 2},
{"members", nlohmann::json::array()},
{"wall_ids", wall_ids_},
{"device_order", device_order_},
{"stream_settings", stream_settings.ToJson()}};
for (const auto &m : members)
doc["members"].push_back({{"id", m.id}, {"group", m.group}});
@@ -223,6 +267,7 @@ struct Workspace {
private:
std::set<std::string> wall_ids_;
std::vector<std::string> device_order_;
bool HasAssignments(const std::string &id) const {
return std::any_of(members.begin(), members.end(),
+2 -1
View File
@@ -403,6 +403,7 @@ export component PrimaryButton inherits Rectangle {
export component SecondaryButton inherits Rectangle {
in property <string> text;
in property <length> text-size: ImGuiFontStyle.body;
in property <bool> enabled: true;
callback clicked;
@@ -419,7 +420,7 @@ export component SecondaryButton inherits Rectangle {
Text {
text: root.text;
color: root.enabled ? #24272d : #949aa5;
font-size: ImGuiFontStyle.body;
font-size: root.text-size;
horizontal-alignment: center;
vertical-alignment: center;
}
@@ -27,18 +27,18 @@ component NetworkRow inherits Rectangle {
identity := Rectangle {
horizontal-stretch: 1;
device-name := Text {
y: root.heading ? 0px : 11px; height: root.heading ? parent.height : 20px;
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width; text: root.device.name;
font-size: root.heading ? 11px : 12px; font-weight: 600;
color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide;
}
name-hover := TouchArea {
y: 11px; height: 20px; width: parent.width;
y: 8px; height: 20px; width: parent.width;
enabled: !root.heading;
}
if !root.heading: HorizontalLayout {
x: 0px; y: 34px; width: parent.width; height: 17px; spacing: 4px;
x: 0px; y: 30px; width: parent.width; height: 17px; spacing: 4px;
id-text := Text {
text: root.device.id; font-size: 10px; color: #7a8490;
vertical-alignment: center; overflow: elide;
@@ -77,7 +77,7 @@ component NetworkRow inherits Rectangle {
Rectangle {
width: max(108px, root.width * 0.19);
Text {
y: root.heading ? 0px : 11px; height: root.heading ? parent.height : 20px;
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width;
text: root.heading ? GalleryStrings.tr("画面", "Video", "Видео") : root.device.resolution;
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
@@ -85,7 +85,7 @@ component NetworkRow inherits Rectangle {
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
y: 34px; height: 17px; width: parent.width;
y: 30px; height: 17px; width: parent.width;
text: root.device.fps; font-size: 11px; color: #7a8490;
vertical-alignment: center; overflow: elide;
}
@@ -99,20 +99,20 @@ component NetworkRow inherits Rectangle {
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
x: 0px; y: 11px; height: 20px; width: 14px;
x: 0px; y: 8px; height: 20px; width: 14px;
text: "↓"; color: #2676b9; font-size: 12px; vertical-alignment: center;
}
if !root.heading: Text {
x: 17px; y: 11px; height: 20px; width: parent.width - 17px;
x: 17px; y: 8px; height: 20px; width: parent.width - 17px;
text: root.device.inbound; color: #293746; font-size: 12px;
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
x: 0px; y: 34px; height: 17px; width: 14px;
x: 0px; y: 30px; height: 17px; width: 14px;
text: "↑"; color: #7a8490; font-size: 11px; vertical-alignment: center;
}
if !root.heading: Text {
x: 17px; y: 34px; height: 17px; width: parent.width - 17px;
x: 17px; y: 30px; height: 17px; width: parent.width - 17px;
text: root.device.outbound; color: #7a8490; font-size: 11px;
vertical-alignment: center; overflow: elide;
}
@@ -120,7 +120,7 @@ component NetworkRow inherits Rectangle {
Rectangle {
width: max(94px, root.width * 0.16);
Text {
y: root.heading ? 0px : 11px; height: root.heading ? parent.height : 20px;
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width;
text: root.heading ? GalleryStrings.tr("网络质量", "Network quality", "Качество сети") : "RTT " + root.device.latency;
font-size: 11px; font-weight: root.heading ? 600 : 400;
@@ -128,7 +128,7 @@ component NetworkRow inherits Rectangle {
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
y: 34px; height: 17px; width: parent.width;
y: 30px; height: 17px; width: parent.width;
text: GalleryStrings.tr("丢包 ", "Loss ", "Потери ") + root.device.loss;
font-size: 11px; color: #7a8490;
vertical-alignment: center; overflow: elide;
@@ -162,14 +162,16 @@ component NetworkEmptyState inherits Rectangle {
export component GalleryNetworkDialog inherits Rectangle {
in property <[GalleryNetworkDevice]> devices;
private property <length> row-height: 56px;
private property <length> list-height: max(2, min(root.devices.length, 6)) * root.row-height;
callback close();
background: #00000042;
TouchArea { clicked => { root.close(); } }
Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(960px, parent.width - 32px);
height: min(560px, parent.height - 24px);
width: min(760px, parent.width - 32px);
height: min(120px + root.list-height, parent.height - 32px);
background: white; border-radius: 8px;
border-width: 1px; border-color: #c6cdd5;
TouchArea {}
@@ -210,10 +212,10 @@ export component GalleryNetworkDialog inherits Rectangle {
x: 12px; y: 108px; width: parent.width - 24px;
height: parent.height - self.y - 12px;
viewport-width: self.width - 14px;
viewport-height: max(self.height, root.devices.length * 62px);
viewport-height: max(self.height, root.devices.length * root.row-height);
horizontal-scrollbar-policy: always-off;
for device[index] in root.devices: NetworkRow {
y: index * 62px; width: list.viewport-width; height: 62px;
y: index * root.row-height; width: list.viewport-width; height: root.row-height;
device: device; background: mod(index, 2) == 0 ? white : #f8fafc;
Rectangle { y: parent.height - 1px; height: 1px; background: #edf0f4; }
}
+352 -65
View File
@@ -3,7 +3,7 @@ import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
import { GalleryMenuButton } from "gallery_menu_button.slint";
import { GroupFilter } from "group_filter.slint";
import { ScrollView, LineEdit } from "std-widgets.slint";
import { CompactButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint";
import { CompactButton, SecondaryButton, FontAwesomeIcons, ImGuiCheckBox } from "common.slint";
export struct GalleryTile {
id: string,
@@ -29,21 +29,72 @@ export struct GalleryActionDevice {
connected: bool,
}
component DeviceDetailsTip inherits Rectangle {
in property <string> device-name;
in property <string> device-id;
in property <string> presence;
in property <length> available-height;
height: min(root.available-height, 58px);
background: #22252bf2; border-width: 1px; border-color: #ffffff38; border-radius: 5px;
clip: true;
tip-name := Text {
x: 8px; y: 8px; width: parent.width - 16px;
height: 14px;
text: root.device-name; color: white; font-size: 11px; overflow: elide; vertical-alignment: top;
}
Text {
x: 8px; y: tip-name.y + tip-name.height; width: parent.width - 16px; height: 14px;
text: "ID: " + root.device-id; color: white; font-size: 11px; overflow: elide;
}
Text {
x: 8px; y: tip-name.y + tip-name.height + 14px; width: parent.width - 16px; height: 14px;
text: root.presence; color: white; font-size: 11px; overflow: elide;
}
}
export component GalleryView inherits Rectangle {
in property <bool> controlling: false;
in property <bool> simplified: false;
private property <length> grid-gap: root.simplified ? 6px : 12px;
private property <length> rail-card-height: root.simplified ? 92px : 76px;
in property <bool> window-active: true;
in-out property <float> tile-width: 216;
out property <bool> resizing: false;
private property <float> preview-aspect-ratio: 16 / 9;
private property <length> card-inset: 6px;
private property <length> card-chrome-height: root.simplified ? root.card-inset * 2 : 36px;
private property <length> max-card-width: max(184px, min(960px, grid.width,
(grid.height - root.card-chrome-height) * root.preview-aspect-ratio + root.card-inset * 2));
private property <length> card-width: clamp(root.tile-width * 1px, 184px, root.max-card-width);
private property <length> card-preview-height: (root.card-width - root.card-inset * 2) / root.preview-aspect-ratio;
private property <length> card-height: root.card-preview-height + root.card-chrome-height;
private property <length> rail-preview-height: 126px / root.preview-aspect-ratio;
private property <length> rail-card-height: root.rail-preview-height + (root.simplified ? 10px : 24px);
private property <string> focused-id: root.focused.id;
private property <string> device-tooltip-id;
private property <string> device-tooltip-text;
private property <length> device-tooltip-x;
private property <length> device-tooltip-y;
private property <bool> device-tooltip-visible: false;
changed focused-id => { root.device-tooltip-id = ""; }
changed visible => { root.device-tooltip-id = ""; if !root.visible { add-menu.close-menu(); batch-menu.close-menu(); } }
changed width => { root.device-tooltip-id = ""; }
changed height => { root.device-tooltip-id = ""; }
private property <string> drag-source;
private property <GalleryTile> dragged-tile;
private property <bool> dragging: false;
private property <length> drag-start-x;
private property <length> drag-start-y;
private property <length> drag-x;
private property <length> drag-y;
private property <int> drag-target: root.drop-index(root.drag-x, root.drag-y);
private property <int> drag-page: -1;
private property <length> drag-edge-width: 32px;
private property <int> drag-page-direction: root.page-direction(root.drag-x, root.drag-y);
private property <bool> swap-save-failed: false;
changed focused-id => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
changed visible => { root.device-tooltip-id = ""; if !root.visible { root.resizing = false; root.cancel-device-drag(); add-menu.close-menu(); batch-menu.close-menu(); } }
changed width => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
changed height => { root.device-tooltip-id = ""; root.resizing = false; root.cancel-device-drag(); }
changed window-active => { if !root.window-active { root.resizing = false; root.cancel-device-drag(); } }
changed page => { if root.page != root.drag-page { root.cancel-device-drag(); } }
changed group-index => { root.cancel-device-drag(); }
changed card-width => { root.cancel-device-drag(); }
changed card-height => { root.cancel-device-drag(); }
changed device-tooltip-id => {
root.device-tooltip-visible = false;
device-tooltip-timer.running = root.device-tooltip-id != "";
@@ -65,8 +116,9 @@ export component GalleryView inherits Rectangle {
in property <[GalleryTile]> tiles;
in property <[string]> groups;
in-out property <int> group-index: 0;
in property <int> columns: 3;
in property <int> rows: 2;
// Pagination and rendering share the same fixed-aspect cell geometry.
out property <int> columns: max(1, floor(grid.width / root.card-width));
out property <int> rows: max(1, floor(grid.height / root.card-height));
in property <int> page: 0;
in property <int> pages: 1;
in property <int> count: 0;
@@ -91,6 +143,7 @@ export component GalleryView inherits Rectangle {
private property <string> action-code;
in property <[GalleryActionDevice]> selected-devices;
callback action(string, string);
callback swap-devices(string, string) -> bool;
callback add-device(string, string, string, bool) -> bool;
callback import-recent(string) -> bool;
callback edit-alias(string, string);
@@ -100,10 +153,89 @@ export component GalleryView inherits Rectangle {
changed adding => { if root.adding { root.action("release", ""); } }
changed importing => { root.action(root.importing ? "release" : "recent-close", ""); }
public function reset-transient() {
root.resizing = false;
root.cancel-device-drag();
root.batch = false; root.adding = false; root.importing = false; root.confirming = false;
root.device-tooltip-id = ""; add-menu.close-menu(); batch-menu.close-menu();
}
changed batch => { root.device-tooltip-id = ""; batch-menu.close-menu(); }
changed batch => { root.device-tooltip-id = ""; root.cancel-device-drag(); batch-menu.close-menu(); }
function cancel-device-drag() {
root.dragging = false; root.drag-source = ""; root.drag-page = -1;
}
function cell-index(x: length, y: length) -> int {
if x < 0px || y < 0px || x >= grid.table-width || y >= grid.table-height { return -1; }
let column = floor(x / root.card-width);
let row = floor(y / root.card-height);
let index = row * root.columns + column;
if index >= root.tiles.length { return -1; }
// Separators remain resize targets; drop on the device inside a cell.
let cell-x = x - column * root.card-width;
let cell-y = y - row * root.card-height;
if cell-x < 6px || cell-x >= root.card-width - 6px || cell-y < 6px || cell-y >= root.card-height - 6px { return -1; }
return index;
}
function drop-index(x: length, y: length) -> int {
let index = root.cell-index(x, y);
return !root.dragging || root.drag-page-direction != 0 || index < 0 || root.tiles[index].id == root.drag-source ? -1 : index;
}
function page-direction(x: length, y: length) -> int {
if !root.dragging { return 0; }
let view-x = grid.x + x;
let view-y = grid.y + y;
if view-x < 0px || view-x >= root.width || view-y < 0px || view-y >= root.height { return 0; }
if root.page > 0 && view-x < root.drag-edge-width { return -1; }
if root.page + 1 < root.pages && view-x >= root.width - root.drag-edge-width { return 1; }
// Keep the pager buttons as additional hover targets.
let pager-x = view-x - footer.x - pager.x;
let pager-y = view-y - footer.y - pager.y;
if pager-y < 0px || pager-y >= pager.height { return 0; }
if root.page > 0 && pager-x >= previous-page.x && pager-x < previous-page.x + previous-page.width { return -1; }
if root.page + 1 < root.pages && pager-x >= next-page.x && pager-x < next-page.x + next-page.width { return 1; }
return 0;
}
changed drag-page-direction => {
if root.drag-page-direction != 0 { drag-page-timer.restart(); }
}
drag-page-timer := Timer {
interval: 700ms;
running: root.dragging && root.drag-page-direction != 0 && root.page == root.drag-page;
triggered => {
if !root.dragging || root.drag-page-direction == 0 || root.page != root.drag-page { return; }
root.drag-page = root.page + root.drag-page-direction;
root.action("page", root.drag-page-direction > 0 ? "next" : "previous");
}
}
function move-device-drag(x: length, y: length) {
if root.drag-source == "" { return; }
root.drag-x = x; root.drag-y = y;
if abs(x - root.drag-start-x) >= 6px || abs(y - root.drag-start-y) >= 6px {
root.dragging = true; root.device-tooltip-id = "";
}
}
function finish-device-drag() {
let source = root.drag-source;
let target = root.drag-target >= 0 ? root.tiles[root.drag-target].id : "";
// Clear transient state before the callback can replace the page model.
root.cancel-device-drag();
if source != "" && target != "" {
root.swap-save-failed = !root.swap-devices(source, target);
if root.swap-save-failed { swap-error-timer.restart(); }
}
}
drag-focus := FocusScope {
x: 0px; y: 0px; width: 0px; height: 0px;
key-pressed(event) => {
if event.text == Key.Escape && root.drag-source != "" {
root.cancel-device-drag(); return accept;
}
reject
}
focus-lost => { root.cancel-device-drag(); }
}
swap-error-timer := Timer {
interval: 4s; running: false;
triggered => { root.swap-save-failed = false; self.running = false; }
}
function confirm-action(code: string, name: string) {
root.action-code = code; root.action-name = name; root.confirming = true;
}
@@ -138,12 +270,104 @@ export component GalleryView inherits Rectangle {
}
}
}
grid := Rectangle {
grid := TouchArea {
x: root.simplified ? 12px : toolbar.x;
y: root.simplified ? 12px : toolbar.y + toolbar.height + 12px;
width: parent.width - self.x * 2;
height: footer.y - 12px - self.y;
visible: root.focused.id == "";
private property <length> table-width: root.columns * root.card-width;
private property <length> table-height: root.rows * root.card-height;
Rectangle {
visible: root.count > 0;
x: 0px; y: 0px;
width: grid.table-width; height: grid.table-height;
background: white;
}
// One stable pointer owner survives page model replacements and cell resizing.
private property <int> column-line: clamp(round(self.mouse-x / root.card-width), 0, root.columns);
private property <int> row-line: clamp(round(self.mouse-y / root.card-height), 0, root.rows);
private property <bool> over-table: root.count > 0
&& self.mouse-x >= 0px && self.mouse-x <= grid.table-width
&& self.mouse-y >= 0px && self.mouse-y <= grid.table-height;
// Both sides of a shared separator must resize in the same direction.
private property <int> edge-x: !self.over-table || abs(self.mouse-x - self.column-line * root.card-width) > 6px
? 0 : self.column-line == 0 ? -1 : 1;
private property <int> edge-y: !self.over-table || abs(self.mouse-y - self.row-line * root.card-height) > 6px
? 0 : self.row-line == 0 ? -1 : 1;
private property <int> drag-x;
private property <int> drag-y;
private property <int> drag-columns;
private property <int> drag-rows;
private property <length> start-x;
private property <length> start-y;
private property <length> start-width;
private property <int> cursor-x: root.resizing ? self.drag-x : self.edge-x;
private property <int> cursor-y: root.resizing ? self.drag-y : self.edge-y;
private property <int> hover-index: root.cell-index(self.mouse-x, self.mouse-y);
mouse-cursor: root.dragging ? (root.drag-target >= 0 || root.drag-page-direction != 0 ? MouseCursor.grabbing : MouseCursor.not-allowed)
: self.cursor-x != 0 && self.cursor-y != 0
? (self.cursor-x == self.cursor-y ? MouseCursor.nwse-resize : MouseCursor.nesw-resize)
: self.cursor-x != 0 ? MouseCursor.ew-resize : self.cursor-y != 0 ? MouseCursor.ns-resize
: self.hover-index >= 0 ? MouseCursor.grab : MouseCursor.default;
function show-hint() {
if self.has-hover && !self.pressed && (self.edge-x != 0 || self.edge-y != 0) {
root.device-tooltip-id = "resize-grid";
root.device-tooltip-text = GalleryStrings.tr("拖动表格分隔线,同步调整所有设备大小", "Drag a table divider to resize all devices", "Потяните за разделитель таблицы, чтобы изменить размер всех устройств");
root.device-tooltip-x = grid.x + self.mouse-x;
root.device-tooltip-y = grid.y + self.mouse-y + 16px;
} else if root.device-tooltip-id == "resize-grid" { root.device-tooltip-id = ""; }
}
function resize-cells() {
if !root.resizing { return; }
// Distribute the movement across preceding cells so the chosen
// divider follows the pointer while every cell stays equal.
let dx = (self.mouse-x - self.start-x) * self.drag-x / self.drag-columns;
let dy = (self.mouse-y - self.start-y) * self.drag-y / self.drag-rows;
let delta = self.drag-x == 0 ? dy * root.preview-aspect-ratio
: self.drag-y == 0 ? dx
: (dx + dy / root.preview-aspect-ratio) / (1 + 1 / (root.preview-aspect-ratio * root.preview-aspect-ratio));
root.tile-width = clamp(self.start-width + delta, 184px, root.max-card-width) / 1px;
}
changed has-hover => { self.show-hint(); }
changed mouse-x => { self.show-hint(); }
changed mouse-y => { self.show-hint(); }
pointer-event(event) => {
if event.button == PointerEventButton.left && event.kind == PointerEventKind.down {
root.cancel-device-drag();
if self.edge-x != 0 || self.edge-y != 0 {
self.drag-x = self.edge-x; self.drag-y = self.edge-y;
self.drag-columns = max(1, self.column-line);
self.drag-rows = max(1, self.row-line);
self.start-x = self.mouse-x; self.start-y = self.mouse-y;
self.start-width = root.card-width;
root.resizing = true; root.device-tooltip-id = "";
} else if self.hover-index >= 0 {
drag-focus.focus();
root.dragged-tile = root.tiles[self.hover-index];
root.drag-source = root.dragged-tile.id; root.drag-page = root.page;
root.drag-start-x = self.mouse-x; root.drag-start-y = self.mouse-y;
root.drag-x = self.mouse-x; root.drag-y = self.mouse-y;
root.swap-save-failed = false; root.device-tooltip-id = "";
}
} else if event.kind == PointerEventKind.cancel {
root.resizing = false; root.cancel-device-drag();
} else if event.button == PointerEventButton.left && event.kind == PointerEventKind.up {
if root.resizing { self.resize-cells(); root.resizing = false; }
else if root.drag-source != "" {
root.move-device-drag(self.mouse-x, self.mouse-y);
if !root.dragging && self.hover-index >= 0 && root.tiles[self.hover-index].id == root.drag-source {
let id = root.drag-source;
root.cancel-device-drag();
root.action(root.batch && !root.simplified ? "select" : "focus", id);
} else { root.finish-device-drag(); }
}
}
}
moved => {
if root.resizing { self.resize-cells(); }
else if root.drag-source != "" { root.move-device-drag(self.mouse-x, self.mouse-y); }
}
for tile[index] in root.tiles: tile-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
@@ -151,18 +375,18 @@ export component GalleryView inherits Rectangle {
accessible-role: button; accessible-label: self.details;
accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
changed details => { if tile-touch.has-hover { tile-touch.show-details(); } }
x: mod(index, root.columns) * (grid.width + root.grid-gap) / root.columns;
y: floor(index / root.columns) * (grid.height + root.grid-gap) / root.rows;
width: (grid.width - (root.columns - 1) * root.grid-gap) / root.columns;
height: (grid.height - (root.rows - 1) * root.grid-gap) / root.rows;
border-radius: root.simplified ? 0px : 6px;
background: !tile.online ? DeviceCardStyle.offline-background : root.simplified ? #24272d : tile.selected ? #eaf3ff : #eff0f2;
border-width: root.simplified ? 0px : tile.selected ? 2px : 1px;
border-color: tile.selected ? #76aeea : root.batch && tile-touch.has-hover ? #94bdeb : !tile.online ? DeviceCardStyle.offline-border : #c9cdd2;
visible: index < root.columns * root.rows;
x: mod(index, root.columns) * root.card-width;
y: floor(index / root.columns) * root.card-height;
width: root.card-width;
height: root.card-height;
opacity: root.dragging && root.drag-source == tile.id ? 0.45 : 1;
background: !tile.online ? DeviceCardStyle.offline-background
: tile.selected ? #eaf3ff : root.batch && tile-touch.has-hover ? #f3f8fe : white;
Rectangle {
x: root.simplified ? 0px : 6px; y: self.x;
x: root.card-inset; y: self.x;
width: parent.width - self.x * 2;
height: root.simplified ? parent.height : parent.height - 36px;
height: root.card-preview-height;
background: tile.online ? #24272d : DeviceCardStyle.offline-preview;
if tile.has-frame && tile.online: Image { x: 0px; y: 0px; width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale ? 0.45 : 1; }
if !root.simplified && (!tile.has-frame || !tile.connected || tile.stale): Text {
@@ -170,14 +394,19 @@ export component GalleryView inherits Rectangle {
horizontal-alignment: center; vertical-alignment: center;
wrap: word-wrap;
}
if root.device-tooltip-visible && root.device-tooltip-id == tile.id: DeviceDetailsTip {
x: 4px; y: parent.height - self.height - 4px;
width: min(320px, parent.width - 8px); available-height: parent.height - 8px;
device-name: tile.name; device-id: tile.id; presence: tile-card.presence;
}
}
Text {
visible: !root.simplified; x: 9px; y: parent.height - 28px;
visible: !root.simplified; x: 9px; y: parent.height - 30px;
width: parent.width - self.x - (tile-touch.has-hover && !root.batch ? DeviceCardStyle.toolbar-width + 12px : root.batch ? 35px : 9px);
height: 28px; text: tile.name; color: tile.online ? #24272d : DeviceCardStyle.offline-text; font-size: 12px; overflow: elide; vertical-alignment: center;
height: 24px; text: tile.name; color: tile.online ? #24272d : DeviceCardStyle.offline-text; font-size: 12px; overflow: elide; vertical-alignment: center;
}
if root.batch && !root.simplified: Rectangle {
x: parent.width - 27px; y: parent.height - 23px;
x: parent.width - 27px; y: parent.height - 27px;
width: 18px; height: 18px; border-radius: 5px; border-width: 1px;
background: tile.selected ? (tile-touch.pressed ? #5c9fdf : tile-touch.has-hover ? #659fe0 : #6dabea) : tile-touch.has-hover ? #f3f8fe : white;
border-color: tile.selected ? #6dabea : tile-touch.has-hover ? #76aeea : #b7bec8;
@@ -188,22 +417,23 @@ export component GalleryView inherits Rectangle {
horizontal-alignment: center; vertical-alignment: center;
}
}
tile-touch := TouchArea {
tile-touch := Rectangle {
x: 6px; y: 6px; width: parent.width - 12px; height: parent.height - 12px;
private property <bool> has-hover: grid.has-hover && grid.hover-index == index;
private property <bool> pressed: grid.pressed && root.drag-source == tile.id;
function show-details() {
// Avoid covering the per-device action buttons with a tooltip.
if self.mouse-y >= tile-card.height - 28px && !root.simplified { root.device-tooltip-id = ""; return; }
root.device-tooltip-id = tile.id; root.device-tooltip-text = tile-card.details;
root.device-tooltip-x = grid.x + tile-card.x;
root.device-tooltip-y = grid.y + tile-card.y + tile-card.height + 4px;
if root.resizing || root.drag-source != "" || (grid.mouse-y - tile-card.y >= tile-card.height - 30px && !root.simplified) { root.device-tooltip-id = ""; return; }
root.device-tooltip-id = tile.id;
}
changed has-hover => {
if self.has-hover { self.show-details(); }
else if root.device-tooltip-id == tile.id { root.device-tooltip-id = ""; }
}
moved => { self.show-details(); }
clicked => { root.device-tooltip-id = ""; root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
if tile-touch.has-hover && !root.batch && !root.simplified: DeviceCardActions {
x: parent.width - self.width - 6px; y: parent.height - 26px;
private property <bool> over-actions: grid.mouse-y - tile-card.y >= tile-card.height - 30px;
changed over-actions => { if self.has-hover { self.show-details(); } }
if tile-touch.has-hover && !root.dragging && !root.resizing && !root.batch && !root.simplified: DeviceCardActions {
x: parent.width - self.width; y: parent.height - 23px;
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub");
connected: tile.connected;
edit-alias => { root.action("release", ""); root.edit-alias(tile.id, tile.name); }
@@ -214,6 +444,29 @@ export component GalleryView inherits Rectangle {
}
}
}
// Draw each table line once, continuously across occupied and empty cells.
for column in root.columns + 1: Rectangle {
visible: root.count > 0;
x: min(column * root.card-width, grid.table-width - 1px);
y: 0px;
width: 1px; height: grid.table-height;
background: root.resizing || (grid.has-hover && grid.edge-x != 0 && grid.column-line == column)
? #76aeea : #c9cdd2;
}
for row in root.rows + 1: Rectangle {
visible: root.count > 0;
x: 0px;
y: min(row * root.card-height, grid.table-height - 1px);
width: grid.table-width; height: 1px;
background: root.resizing || (grid.has-hover && grid.edge-y != 0 && grid.row-line == row)
? #76aeea : #c9cdd2;
}
if root.drag-target >= 0: Rectangle {
x: mod(root.drag-target, root.columns) * root.card-width + 1px;
y: floor(root.drag-target / root.columns) * root.card-height + 1px;
width: root.card-width - 2px; height: root.card-height - 2px;
background: #76aeea26; border-width: 2px; border-color: #4a94de;
}
if root.count == 0: Text {
text: root.simplified
? GalleryStrings.tr("暂无设备,退出精简模式后可添加", "No devices. Exit compact mode to add devices", "Нет устройств. Выйдите из компактного режима, чтобы добавить их")
@@ -313,7 +566,7 @@ export component GalleryView inherits Rectangle {
// Collapse the focused tile and its gap without changing the page model.
width: tile.id == root.focused.id ? 0px : 144px;
height: root.rail-card-height; visible: tile.id != root.focused.id;
Rectangle {
rail-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
private property <string> details: tile.name + "\nID: " + tile.id + "\n" + self.presence;
@@ -324,25 +577,27 @@ export component GalleryView inherits Rectangle {
accessible-action-default => { root.action("focus", tile.id); }
changed details => { if rail-touch.has-hover { rail-touch.show-details(); } }
Rectangle {
x: 5px; y: 5px; width: 126px; height: root.simplified ? 82px : 52px; background: tile.online ? #24272d : DeviceCardStyle.offline-preview;
x: 5px; y: 5px; width: 126px; height: root.rail-preview-height; background: tile.online ? #24272d : DeviceCardStyle.offline-preview;
if tile.has-frame && tile.online: Image { width: parent.width; height: parent.height; source: tile.frame; image-fit: contain; opacity: tile.stale || !tile.connected ? 0.4 : 1; }
if !root.simplified && (!tile.has-frame || tile.stale || !tile.connected): Text {
text: tile.status; color: tile.online ? #e2e6ed : DeviceCardStyle.offline-placeholder; font-size: 10px; overflow: elide;
horizontal-alignment: center; vertical-alignment: center;
}
if root.device-tooltip-visible && root.device-tooltip-id == tile.id: DeviceDetailsTip {
x: 4px; y: parent.height - self.height - 4px;
width: parent.width - 8px; available-height: parent.height - 8px;
device-name: tile.name; device-id: tile.id; presence: rail-card.presence;
}
}
Text {
visible: !root.simplified;
x: 7px; y: 58px; width: 122px; height: 16px;
x: 7px; y: root.rail-preview-height + 6px; width: 122px; height: 16px;
text: tile.name; color: tile.online ? #303943 : DeviceCardStyle.offline-text; font-size: 11px;
overflow: elide; vertical-alignment: center;
}
rail-touch := TouchArea {
function show-details() {
root.device-tooltip-id = tile.id;
root.device-tooltip-text = parent.details;
root.device-tooltip-x = focus-view.x + rail.x + rail.viewport-x + rail-slot.x;
root.device-tooltip-y = focus-view.y + rail.y + rail.height + 4px;
}
changed has-hover => {
if self.has-hover { self.show-details(); }
@@ -367,29 +622,19 @@ export component GalleryView inherits Rectangle {
status-area := Rectangle {
x: 0px; y: 0px; width: parent.width - 124px; height: parent.height;
visible: !root.batch || root.simplified;
border-radius: 3px; background: network-touch.has-hover ? #edf0f4 : transparent;
accessible-role: button;
accessible-label: GalleryStrings.tr("网络状态", "Network", "Сеть") + ": " + root.connected-summary;
accessible-action-default => { root.network-open(); }
Text {
x: 0px; width: parent.width - 20px; height: parent.height;
text: root.connected-summary + " · " + GalleryStrings.tr("网络状态", "Network", "Сеть");
connection-count := Text {
x: 0px; width: min(self.preferred-width, parent.width - network-button.width - 12px); height: parent.height;
text: root.connected-summary;
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
}
Text {
x: parent.width - 13px; width: 12px; height: parent.height;
text: FontAwesomeIcons.angle-up; font-family: "Font Awesome 6 Free"; font-weight: 900;
font-size: 9px; color: #858c96; vertical-alignment: center;
}
network-touch := TouchArea {
mouse-cursor: pointer;
changed has-hover => {
if self.has-hover {
root.device-tooltip-id = "network-status";
root.device-tooltip-text = root.connected-summary + "\n" + GalleryStrings.tr("点击查看网络状态", "Click to view network status", "Нажмите для просмотра состояния сети");
root.device-tooltip-x = footer.x; root.device-tooltip-y = footer.y;
} else if root.device-tooltip-id == "network-status" { root.device-tooltip-id = ""; }
}
network-button := SecondaryButton {
x: connection-count.x + connection-count.width + 12px;
min-width: 64px; width: 64px; height: parent.height;
border-color: #c8cbd0; border-radius: 3px;
text: GalleryStrings.tr("网络状态", "Network", "Сеть");
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { self.clicked(); }
clicked => { root.device-tooltip-id = ""; root.network-open(); }
}
}
@@ -432,15 +677,57 @@ export component GalleryView inherits Rectangle {
}
}
}
HorizontalLayout {
pager := HorizontalLayout {
x: parent.width - self.width; y: 0px; width: 112px; height: 24px; spacing: 6px;
CompactButton { width: 24px; text: "‹"; text-size: 16px; enabled: root.page > 0; clicked => { root.action("page", "previous"); } }
previous-page := CompactButton { primary: root.drag-page-direction < 0; width: 24px; text: "‹"; text-size: 16px; enabled: root.page > 0; clicked => { root.action("page", "previous"); } }
Text { text: (root.page + 1) + " / " + root.pages; min-width: 0px; overflow: elide; horizontal-alignment: center; vertical-alignment: center; }
CompactButton { width: 24px; text: "›"; text-size: 16px; enabled: root.page + 1 < root.pages; clicked => { root.action("page", "next"); } }
next-page := CompactButton { primary: root.drag-page-direction > 0; width: 24px; text: "›"; text-size: 16px; enabled: root.page + 1 < root.pages; clicked => { root.action("page", "next"); } }
}
}
// Share one delayed tooltip across cards and keep it inside the view.
if root.device-tooltip-visible && root.device-tooltip-id != "": Rectangle {
for edge in 2: Rectangle {
visible: root.dragging && (edge == 0 ? root.page > 0 : root.page + 1 < root.pages);
private property <bool> active: root.drag-page-direction == (edge == 0 ? -1 : 1);
x: edge == 0 ? 0px : parent.width - self.width; y: 0px;
width: root.drag-edge-width; height: parent.height; z: 8;
background: self.active ? #76aeea38 : #76aeea14;
Text {
text: edge == 0 ? "‹" : "›"; font-size: 24px;
color: parent.active ? #357bc0 : #7ba2c9;
horizontal-alignment: center; vertical-alignment: center;
}
}
if root.dragging: Rectangle {
x: clamp(grid.x + root.drag-x + 14px, 8px, parent.width - self.width - 8px);
y: clamp(grid.y + root.drag-y + 14px, 8px, footer.y - self.height - 12px);
width: 224px; height: 54px; z: 10;
background: #fffffff2; border-width: 1px; border-color: #76aeea; border-radius: 4px;
Rectangle {
x: 8px; y: 9px; width: 64px; height: 36px;
background: root.dragged-tile.online ? #24272d : DeviceCardStyle.offline-preview;
if root.dragged-tile.has-frame && root.dragged-tile.online: Image {
x: 0px; y: 0px; width: parent.width; height: parent.height;
source: root.dragged-tile.frame; image-fit: contain;
}
}
Text { x: 80px; y: 8px; width: parent.width - 88px; height: 18px; text: root.dragged-tile.name; font-size: 12px; color: #303943; overflow: elide; }
Text {
x: 80px; y: 29px; width: parent.width - 88px; height: 16px;
text: root.drag-page-direction != 0 ? GalleryStrings.tr("停留翻页,松开取消", "Hold to turn page", "Удерживайте для перелистывания")
: root.drag-target >= 0 ? GalleryStrings.tr("松开鼠标交换位置", "Release to swap", "Отпустите для обмена")
: GalleryStrings.tr("拖到另一台设备上", "Drag onto another device", "На другое устройство");
font-size: 10px; color: #527ca8; overflow: elide;
}
}
if root.swap-save-failed: Rectangle {
x: (parent.width - self.width) / 2; y: footer.y - self.height - 14px;
width: min(420px, parent.width - 32px); height: 36px; z: 10;
background: #fff2f0; border-width: 1px; border-color: #e9b7b0; border-radius: 4px;
Text { x: 12px; y: 0px; width: parent.width - 24px; height: parent.height; font-size: 12px; color: #9d4339; vertical-alignment: center;
text: GalleryStrings.tr("换位未保存,请重试", "Couldn't save positions. Please try again.", "Не удалось сохранить порядок. Повторите попытку."); overflow: elide;
}
}
// Resize hints follow the pointer; device details stay inside their preview.
if root.device-tooltip-visible && root.device-tooltip-id == "resize-grid": Rectangle {
x: clamp(root.device-tooltip-x, 8px, parent.width - self.width - 8px);
y: clamp(root.device-tooltip-y + self.height > footer.y ? root.device-tooltip-y - self.height - 8px : root.device-tooltip-y, 4px, parent.height - self.height - 4px);
width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px;
+69 -67
View File
@@ -265,19 +265,20 @@ component PasswordInput inherits Rectangle {
}
export component MainWindow inherits Window {
title: "CrossDesk";
title: root.gallery-mode ? "CrossDesk · " + GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub") : "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
preferred-width: 640px;
preferred-height: 486px + root.titlebar-height;
preferred-height: root.minimum-window-height;
min-width: 640px;
max-width: root.gallery-mode ? 16384px : 640px;
min-height: 486px + root.titlebar-height;
max-height: root.gallery-mode ? 16384px : 486px + root.titlebar-height;
min-height: root.minimum-window-height;
max-height: root.gallery-mode ? 16384px : root.minimum-window-height;
no-frame: root.custom-titlebar;
background: root.wayland-titlebar ? transparent : white;
default-font-size: ImGuiFontStyle.base;
in-out property <bool> gallery-mode: false;
// Fixed role for this window instance; opening DeskHub creates another window.
in property <bool> gallery-mode: false;
in-out property <bool> gallery-simplified: false;
changed gallery-simplified => {
root.reset-gallery-transient();
@@ -316,8 +317,10 @@ export component MainWindow inherits Window {
in property <[GalleryTile]> gallery-tiles;
in property <[string]> gallery-groups;
in-out property <int> gallery-group-index: 0;
in property <int> gallery-columns: 3;
in property <int> gallery-rows: 2;
out property <int> gallery-columns: gallery.columns;
out property <int> gallery-rows: gallery.rows;
in-out property <float> gallery-tile-width: 216;
out property <bool> gallery-resizing: gallery.resizing;
in property <int> gallery-page: 0;
in property <int> gallery-pages: 1;
in property <int> gallery-count: 0;
@@ -335,11 +338,10 @@ export component MainWindow inherits Window {
in property <[GalleryRecentDevice]> gallery-recent-devices;
in property <int> gallery-recent-selected: 0;
in property <string> gallery-import-group;
out property <float> gallery-width: root.width / 1px;
out property <float> gallery-height: gallery.height / 1px;
callback change-mode(bool);
callback open-gallery();
callback toggle-gallery-fullscreen();
callback gallery-action(string, string);
callback gallery-swap(string, string) -> bool;
callback gallery-add-device(string, string, string, bool) -> bool;
callback gallery-import-recent(string) -> bool;
in-out property <bool> device-group-open: false;
@@ -367,7 +369,9 @@ export component MainWindow inherits Window {
in property <bool> window-active: true;
private property <length> titlebar-height:
root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px;
private property <length> content-header-height: 38px;
private property <length> content-header-height: root.gallery-mode ? 38px : 0px;
out property <length> minimum-window-height:
448px + root.content-header-height + root.titlebar-height;
private property <length> wayland-corner-radius: 12px;
in property <string> local-id: "";
in property <string> local-password: "";
@@ -585,7 +589,7 @@ export component MainWindow inherits Window {
x: 80px;
width: parent.width - 160px;
height: parent.height;
text: "CrossDesk";
text: root.title;
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
@@ -686,7 +690,7 @@ export component MainWindow inherits Window {
x: 30px;
width: parent.width - 122px;
height: parent.height;
text: "CrossDesk";
text: root.title;
color: #202124;
font-size: 12px;
vertical-alignment: center;
@@ -804,15 +808,19 @@ export component MainWindow inherits Window {
z: 7;
}
// App actions live in the content header; window controls and dragging
// remain in the native or Windows-specific title bar above it.
// The connection page shares its section heading row with the menu.
// Only DeskHub needs a separate toolbar above its content.
menu-button := IconButton {
x: parent.width - mode-switch.x - self.width;
y: mode-switch.y;
width: mode-switch.height;
height: mode-switch.height;
visible: !root.gallery-mode;
x: parent.width - 29px - self.width;
y: 10.5px;
width: 28px;
height: self.width;
icon: FontAwesomeIcons.bars;
icon-size: 14px;
border-width: 1px;
border-color: #c9cdd2;
border-radius: 4px;
hover-background: #e1e5eb;
pressed-background: #cbd1d9;
tooltip: root.announcement-unread > 0
@@ -822,7 +830,7 @@ export component MainWindow inherits Window {
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
}
// Unread messages take precedence so the menu has only one indicator.
if root.update-available && root.announcement-unread <= 0: Rectangle {
if !root.gallery-mode && 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;
@@ -863,7 +871,7 @@ export component MainWindow inherits Window {
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
}
}
if root.announcement-unread > 0: Rectangle {
if !root.gallery-mode && root.announcement-unread > 0: Rectangle {
x: menu-button.x + menu-button.width - self.width + 4px;
y: menu-button.y - 1px;
width: max(16px, unread-badge-label.preferred-width + 8px);
@@ -891,41 +899,23 @@ export component MainWindow inherits Window {
}
}
mode-switch := Rectangle {
if root.gallery-mode: Text {
x: 29px; y: root.content-header-height - self.height; width: 164px; height: 24px;
background: #e2e4e7;
border-width: 1px; border-color: #c8cbd0; border-radius: 3px;
Rectangle {
x: root.gallery-mode ? 82px : 2px;
y: 2px; width: 80px; height: parent.height - 4px;
background: #a8d0fa;
border-width: 1px; border-color: #9bc3ed; border-radius: 3px;
animate x { duration: 160ms; easing: ease-out; }
}
for label[index] in [GalleryStrings.tr("连接", "Connect", "Связь"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle {
x: 2px + index * 80px; y: 2px; width: 80px; height: parent.height - 4px;
Text {
text: label;
color: #24272d;
font-weight: (index == 1) == root.gallery-mode ? 600 : 400;
horizontal-alignment: center; vertical-alignment: center; font-size: 12px;
}
TouchArea {
clicked => { root.change-mode(index == 1); }
}
}
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
color: #24272d; font-weight: 600; font-size: 12px;
vertical-alignment: center;
}
if root.gallery-mode: CompactButton {
x: menu-button.x - 24px - 16px - self.width;
y: mode-switch.y; width: 94px; height: mode-switch.height;
x: parent.width - 29px - 24px - 8px - self.width;
y: root.content-header-height - self.height; width: 94px; height: 24px;
primary: root.gallery-simplified;
text: root.gallery-simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид")
: GalleryStrings.tr("精简模式", "Compact", "Компактно");
clicked => { root.gallery-simplified = !root.gallery-simplified; }
}
if root.gallery-mode: IconButton {
x: menu-button.x - self.width - 8px;
y: mode-switch.y; width: 24px; height: mode-switch.height;
x: parent.width - 29px - self.width;
y: root.content-header-height - self.height; width: 24px; height: 24px;
icon: root.gallery-fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.gallery-fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
clicked => { root.toggle-gallery-fullscreen(); }
@@ -1058,7 +1048,7 @@ export component MainWindow inherits Window {
Rectangle { x: parent.width / 2; y: 18px; width: 1px; height: 206px; background: ImGuiLineStyle.border; }
SectionTitle { x: 356px; y: 9px; width: 256px; height: 31px; text: UiStrings.remote-desktop; }
SectionTitle { x: 356px; y: 9px; width: menu-button.x - self.x - 8px; height: 31px; text: UiStrings.remote-desktop; }
Rectangle {
x: 356px;
y: 42px;
@@ -1125,8 +1115,23 @@ export component MainWindow inherits Window {
Rectangle { y: 0px; height: 1px; background: ImGuiLineStyle.border; }
SectionTitle { x: 29px; y: 7px; height: 28px; text: UiStrings.recent-connections; }
GroupFilter {
SectionTitle { x: 29px; y: 7px; width: open-gallery-button.x - self.x - 12px; height: 28px; text: UiStrings.recent-connections; }
open-gallery-button := SecondaryButton {
x: recent-group-filter.x - self.width - 8px;
y: recent-group-filter.y;
min-width: 60px;
width: root.compact-language ? 60px : 72px;
height: recent-group-filter.height;
text-size: ImGuiFontStyle.base;
border-color: #c9cdd2;
border-radius: 4px;
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { root.open-gallery(); }
clicked => { root.open-gallery(); }
}
recent-group-filter := GroupFilter {
x: parent.width - self.width - 29px; y: 8px;
width: 155px; height: 28px;
model: root.recent-groups;
@@ -1338,9 +1343,11 @@ export component MainWindow inherits Window {
}
gallery := GalleryView {
y: root.content-header-height; width: parent.width;
height: parent.height - self.y - 24px;
height: parent.height - self.y;
visible: root.gallery-mode;
simplified: root.gallery-simplified;
window-active: root.window-active;
tile-width <=> root.gallery-tile-width;
network-open => { root.gallery-network-open = true; }
controlling: root.gallery-controlling;
pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); }
@@ -1348,7 +1355,6 @@ export component MainWindow inherits Window {
key-input(text, pressed, ctrl, alt, shift, meta) => { root.gallery-key(text, pressed, ctrl, alt, shift, meta); }
tiles: root.gallery-tiles; groups: root.gallery-groups;
group-index <=> root.gallery-group-index;
columns: root.gallery-columns; rows: root.gallery-rows;
page: root.gallery-page; pages: root.gallery-pages; count: root.gallery-count;
connected-count: root.gallery-connected; selected-count: root.gallery-selected;
selected-audio-available: root.gallery-selected-audio-available;
@@ -1359,6 +1365,7 @@ export component MainWindow inherits Window {
importing <=> root.gallery-importing; recent-devices: root.gallery-recent-devices;
recent-selected: root.gallery-recent-selected; import-group: root.gallery-import-group;
action(code, value) => { root.gallery-action(code, value); }
swap-devices(source, target) => { root.gallery-swap(source, target) }
add-device(id, password, group, remember) => { root.gallery-add-device(id, password, group, remember) }
import-recent(group) => { root.gallery-import-recent(group) }
edit-alias(id, name) => {
@@ -1369,8 +1376,8 @@ export component MainWindow inherits Window {
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
}
}
// Shared server status bar stays visible in every connection and DeskHub view.
Rectangle {
// Server status and reconnect actions belong to the connection window.
if !root.gallery-mode: Rectangle {
y: parent.height - 24px;
height: 24px;
background: #fafbfc;
@@ -1481,7 +1488,7 @@ export component MainWindow inherits Window {
}
// Dismiss the title menu when clicking anywhere outside of it.
if root.menu-open: TouchArea {
if !root.gallery-mode && root.menu-open: TouchArea {
x: 0px;
y: 0px;
width: parent.width;
@@ -1494,7 +1501,7 @@ export component MainWindow inherits Window {
}
// Title menu.
if root.menu-open: Rectangle {
if !root.gallery-mode && root.menu-open: Rectangle {
x: menu-button.x + menu-button.width - self.width;
y: menu-button.y + menu-button.height + 2px;
width: max(80px, max(menu-settings-label.preferred-width,
@@ -1953,19 +1960,14 @@ export component MainWindow inherits Window {
DialogSurface {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: min(400px, parent.width - 40px); height: 446px;
width: min(400px, parent.width - 40px); height: min(446px, parent.height - 16px);
background: #f8fafc; border-radius: 14px; border-color: #e1e7ef;
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 18px; y: 18px; width: parent.width - 80px; height: 27px;
x: 18px; y: 18px; width: parent.width - 36px; height: 27px;
text: UiStrings.settings; font-size: 20px; font-weight: 600; color: #22324a;
}
SettingsButton {
x: parent.width - 90px; y: 18px; width: 76px; height: 28px; quiet: true;
text: GalleryStrings.tr("关闭", "Close", "Закрыть");
clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; }
}
Rectangle {
x: 18px; y: 56px; width: parent.width - 36px; height: 40px;
background: #eaf0f6; border-radius: 9px;
@@ -1995,7 +1997,7 @@ export component MainWindow inherits Window {
}
}
if root.settings-tab == 0: settings-scroll := ScrollView {
x: 18px; y: 108px; width: parent.width - 28px; height: 272px;
x: 18px; y: 108px; width: parent.width - 28px; height: parent.height - 174px;
viewport-y <=> root.settings-scroll-y;
VerticalLayout {
width: settings-scroll.width - 12px;
@@ -2049,15 +2051,15 @@ export component MainWindow inherits Window {
}
}
if root.settings-tab == 1: GallerySettings {
x: 18px; y: 108px; width: parent.width - 36px; height: 272px;
x: 18px; y: 108px; width: parent.width - 36px; height: parent.height - 174px;
resolution-index <=> root.gallery-resolution-draft;
frame-rate-index <=> root.gallery-frame-rate-draft;
bandwidth-index <=> root.gallery-bandwidth-draft;
error: root.gallery-settings-error;
}
Rectangle { x: 18px; y: 382px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
Rectangle { x: 18px; y: parent.height - 64px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
HorizontalLayout {
x: 18px; y: 396px; width: parent.width - 36px; height: 32px; spacing: 10px;
x: 18px; y: parent.height - 50px; width: parent.width - 36px; height: 32px; spacing: 10px;
if root.settings-tab == 1: SettingsButton {
width: 110px; quiet: true;
text: GalleryStrings.tr("恢复默认", "Reset defaults", "По умолчанию");