mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[feat] add standalone DeskHub window and resizable device table
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@ private:
|
|||||||
void InitializeUi();
|
void InitializeUi();
|
||||||
void InitializeSystemTray();
|
void InitializeSystemTray();
|
||||||
bool MinimizeMainWindowToTray();
|
bool MinimizeMainWindowToTray();
|
||||||
void BindMainCallbacks();
|
void BindMainCallbacks(bool gallery_window = false);
|
||||||
void BindStreamCallbacks();
|
void BindStreamCallbacks();
|
||||||
void BindServerCallbacks();
|
void BindServerCallbacks();
|
||||||
void Tick();
|
void Tick();
|
||||||
@@ -60,16 +60,21 @@ private:
|
|||||||
void HandlePasswordChangeResult();
|
void HandlePasswordChangeResult();
|
||||||
void HandleCredentialRecovery();
|
void HandleCredentialRecovery();
|
||||||
void SyncMainWindow();
|
void SyncMainWindow();
|
||||||
void ChangeMainMode(bool gallery);
|
void ShowGalleryWindow();
|
||||||
|
void HideGalleryWindow();
|
||||||
|
void SyncWindowGeometry(bool gallery_window);
|
||||||
void ToggleGalleryFullscreen();
|
void ToggleGalleryFullscreen();
|
||||||
void RestoreMainWindowGeometry(bool before_show = false);
|
void RestoreWindowGeometry(bool before_show = false,
|
||||||
|
bool gallery_window = false);
|
||||||
void SyncGallery();
|
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,
|
bool AddGalleryDevice(const std::string& id, const std::string& password,
|
||||||
const std::string& group, bool remember_password);
|
const std::string& group, bool remember_password);
|
||||||
bool ImportGalleryRecent(const std::string& group);
|
bool ImportGalleryRecent(const std::string& group);
|
||||||
void DeviceGroupAction(const std::string& action, const std::string& value);
|
void DeviceGroupAction(const std::string& action, const std::string& value,
|
||||||
bool SaveDeviceGroups(const std::string& new_group);
|
bool gallery_window);
|
||||||
|
bool SaveDeviceGroups(const std::string& new_group, bool gallery_window);
|
||||||
void ReleaseControl();
|
void ReleaseControl();
|
||||||
void SyncAudioPlayback();
|
void SyncAudioPlayback();
|
||||||
void SaveGalleryWindowState();
|
void SaveGalleryWindowState();
|
||||||
@@ -89,9 +94,9 @@ private:
|
|||||||
void SyncXWaylandWindowActivation();
|
void SyncXWaylandWindowActivation();
|
||||||
#endif
|
#endif
|
||||||
void UpdateLocalization();
|
void UpdateLocalization();
|
||||||
void ResetSettingsUi();
|
void ResetSettingsUi(bool gallery_window = false);
|
||||||
bool ValidateSelfHostedSettingsFromUi();
|
bool ValidateSelfHostedSettingsFromUi(bool gallery_window = false);
|
||||||
bool SaveSettingsFromUi();
|
bool SaveSettingsFromUi(bool gallery_window = false);
|
||||||
void ApplySettingsFromConfig(bool reconnect);
|
void ApplySettingsFromConfig(bool reconnect);
|
||||||
#if _WIN32 && CROSSDESK_PORTABLE
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
void CheckPortableWindowsService();
|
void CheckPortableWindowsService();
|
||||||
|
|||||||
@@ -22,26 +22,6 @@ struct WindowState {
|
|||||||
std::optional<slint::LogicalSize> wall_size;
|
std::optional<slint::LogicalSize> wall_size;
|
||||||
std::optional<slint::PhysicalPosition> wall_position, connection_position;
|
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,
|
void Observe(bool wall, slint::PhysicalPosition position,
|
||||||
slint::PhysicalSize size, float scale, bool fullscreen,
|
slint::PhysicalSize size, float scale, bool fullscreen,
|
||||||
bool maximized, float compact_height = 486) {
|
bool maximized, float compact_height = 486) {
|
||||||
|
|||||||
@@ -82,7 +82,39 @@ struct Workspace {
|
|||||||
if (wall_ids_.count(m.id) && (group.empty() || m.group == group) &&
|
if (wall_ids_.count(m.id) && (group.empty() || m.group == group) &&
|
||||||
seen.insert(m.id).second)
|
seen.insert(m.id).second)
|
||||||
ids.push_back(m.id);
|
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.
|
// History filtering uses group assignments, independently of wall enrollment.
|
||||||
// nullopt means all devices; an empty name means no named group.
|
// nullopt means all devices; an empty name means no named group.
|
||||||
@@ -147,6 +179,7 @@ struct Workspace {
|
|||||||
if (group.empty()) {
|
if (group.empty()) {
|
||||||
// Removing from the wall leaves history's group assignments intact.
|
// Removing from the wall leaves history's group assignments intact.
|
||||||
wall_ids_.erase(id);
|
wall_ids_.erase(id);
|
||||||
|
std::erase(device_order_, id);
|
||||||
} else if (MatchesGroup(id, group)) {
|
} else if (MatchesGroup(id, group)) {
|
||||||
// Removing the last named group leaves an ungrouped wall member.
|
// Removing the last named group leaves an ungrouped wall member.
|
||||||
// Reuse the editor path so enrollment and card order stay unchanged.
|
// Reuse the editor path so enrollment and card order stay unchanged.
|
||||||
@@ -198,9 +231,19 @@ struct Workspace {
|
|||||||
if (ValidId(id) && loaded.HasAssignments(id))
|
if (ValidId(id) && loaded.HasAssignments(id))
|
||||||
loaded.wall_ids_.insert(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);
|
members = std::move(loaded.members);
|
||||||
groups = std::move(loaded.groups);
|
groups = std::move(loaded.groups);
|
||||||
wall_ids_ = std::move(loaded.wall_ids_);
|
wall_ids_ = std::move(loaded.wall_ids_);
|
||||||
|
device_order_ = std::move(loaded.device_order_);
|
||||||
stream_settings = loaded.stream_settings;
|
stream_settings = loaded.stream_settings;
|
||||||
return true;
|
return true;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
@@ -212,6 +255,7 @@ struct Workspace {
|
|||||||
nlohmann::json doc{{"version", 2},
|
nlohmann::json doc{{"version", 2},
|
||||||
{"members", nlohmann::json::array()},
|
{"members", nlohmann::json::array()},
|
||||||
{"wall_ids", wall_ids_},
|
{"wall_ids", wall_ids_},
|
||||||
|
{"device_order", device_order_},
|
||||||
{"stream_settings", stream_settings.ToJson()}};
|
{"stream_settings", stream_settings.ToJson()}};
|
||||||
for (const auto &m : members)
|
for (const auto &m : members)
|
||||||
doc["members"].push_back({{"id", m.id}, {"group", m.group}});
|
doc["members"].push_back({{"id", m.id}, {"group", m.group}});
|
||||||
@@ -223,6 +267,7 @@ struct Workspace {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::set<std::string> wall_ids_;
|
std::set<std::string> wall_ids_;
|
||||||
|
std::vector<std::string> device_order_;
|
||||||
|
|
||||||
bool HasAssignments(const std::string &id) const {
|
bool HasAssignments(const std::string &id) const {
|
||||||
return std::any_of(members.begin(), members.end(),
|
return std::any_of(members.begin(), members.end(),
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ export component PrimaryButton inherits Rectangle {
|
|||||||
|
|
||||||
export component SecondaryButton inherits Rectangle {
|
export component SecondaryButton inherits Rectangle {
|
||||||
in property <string> text;
|
in property <string> text;
|
||||||
|
in property <length> text-size: ImGuiFontStyle.body;
|
||||||
in property <bool> enabled: true;
|
in property <bool> enabled: true;
|
||||||
callback clicked;
|
callback clicked;
|
||||||
|
|
||||||
@@ -419,7 +420,7 @@ export component SecondaryButton inherits Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
text: root.text;
|
text: root.text;
|
||||||
color: root.enabled ? #24272d : #949aa5;
|
color: root.enabled ? #24272d : #949aa5;
|
||||||
font-size: ImGuiFontStyle.body;
|
font-size: root.text-size;
|
||||||
horizontal-alignment: center;
|
horizontal-alignment: center;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,18 @@ component NetworkRow inherits Rectangle {
|
|||||||
identity := Rectangle {
|
identity := Rectangle {
|
||||||
horizontal-stretch: 1;
|
horizontal-stretch: 1;
|
||||||
device-name := Text {
|
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;
|
width: parent.width; text: root.device.name;
|
||||||
font-size: root.heading ? 11px : 12px; font-weight: 600;
|
font-size: root.heading ? 11px : 12px; font-weight: 600;
|
||||||
color: root.heading ? #6b7785 : #293746;
|
color: root.heading ? #6b7785 : #293746;
|
||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
name-hover := TouchArea {
|
name-hover := TouchArea {
|
||||||
y: 11px; height: 20px; width: parent.width;
|
y: 8px; height: 20px; width: parent.width;
|
||||||
enabled: !root.heading;
|
enabled: !root.heading;
|
||||||
}
|
}
|
||||||
if !root.heading: HorizontalLayout {
|
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 {
|
id-text := Text {
|
||||||
text: root.device.id; font-size: 10px; color: #7a8490;
|
text: root.device.id; font-size: 10px; color: #7a8490;
|
||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
@@ -77,7 +77,7 @@ component NetworkRow inherits Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: max(108px, root.width * 0.19);
|
width: max(108px, root.width * 0.19);
|
||||||
Text {
|
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;
|
width: parent.width;
|
||||||
text: root.heading ? GalleryStrings.tr("画面", "Video", "Видео") : root.device.resolution;
|
text: root.heading ? GalleryStrings.tr("画面", "Video", "Видео") : root.device.resolution;
|
||||||
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
|
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;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
if !root.heading: Text {
|
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;
|
text: root.device.fps; font-size: 11px; color: #7a8490;
|
||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
@@ -99,20 +99,20 @@ component NetworkRow inherits Rectangle {
|
|||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
if !root.heading: Text {
|
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;
|
text: "↓"; color: #2676b9; font-size: 12px; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
if !root.heading: Text {
|
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;
|
text: root.device.inbound; color: #293746; font-size: 12px;
|
||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
if !root.heading: Text {
|
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;
|
text: "↑"; color: #7a8490; font-size: 11px; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
if !root.heading: Text {
|
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;
|
text: root.device.outbound; color: #7a8490; font-size: 11px;
|
||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ component NetworkRow inherits Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: max(94px, root.width * 0.16);
|
width: max(94px, root.width * 0.16);
|
||||||
Text {
|
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;
|
width: parent.width;
|
||||||
text: root.heading ? GalleryStrings.tr("网络质量", "Network quality", "Качество сети") : "RTT " + root.device.latency;
|
text: root.heading ? GalleryStrings.tr("网络质量", "Network quality", "Качество сети") : "RTT " + root.device.latency;
|
||||||
font-size: 11px; font-weight: root.heading ? 600 : 400;
|
font-size: 11px; font-weight: root.heading ? 600 : 400;
|
||||||
@@ -128,7 +128,7 @@ component NetworkRow inherits Rectangle {
|
|||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
}
|
}
|
||||||
if !root.heading: Text {
|
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;
|
text: GalleryStrings.tr("丢包 ", "Loss ", "Потери ") + root.device.loss;
|
||||||
font-size: 11px; color: #7a8490;
|
font-size: 11px; color: #7a8490;
|
||||||
vertical-alignment: center; overflow: elide;
|
vertical-alignment: center; overflow: elide;
|
||||||
@@ -162,14 +162,16 @@ component NetworkEmptyState inherits Rectangle {
|
|||||||
|
|
||||||
export component GalleryNetworkDialog inherits Rectangle {
|
export component GalleryNetworkDialog inherits Rectangle {
|
||||||
in property <[GalleryNetworkDevice]> devices;
|
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();
|
callback close();
|
||||||
background: #00000042;
|
background: #00000042;
|
||||||
TouchArea { clicked => { root.close(); } }
|
TouchArea { clicked => { root.close(); } }
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: (parent.width - self.width) / 2;
|
x: (parent.width - self.width) / 2;
|
||||||
y: (parent.height - self.height) / 2;
|
y: (parent.height - self.height) / 2;
|
||||||
width: min(960px, parent.width - 32px);
|
width: min(760px, parent.width - 32px);
|
||||||
height: min(560px, parent.height - 24px);
|
height: min(120px + root.list-height, parent.height - 32px);
|
||||||
background: white; border-radius: 8px;
|
background: white; border-radius: 8px;
|
||||||
border-width: 1px; border-color: #c6cdd5;
|
border-width: 1px; border-color: #c6cdd5;
|
||||||
TouchArea {}
|
TouchArea {}
|
||||||
@@ -210,10 +212,10 @@ export component GalleryNetworkDialog inherits Rectangle {
|
|||||||
x: 12px; y: 108px; width: parent.width - 24px;
|
x: 12px; y: 108px; width: parent.width - 24px;
|
||||||
height: parent.height - self.y - 12px;
|
height: parent.height - self.y - 12px;
|
||||||
viewport-width: self.width - 14px;
|
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;
|
horizontal-scrollbar-policy: always-off;
|
||||||
for device[index] in root.devices: NetworkRow {
|
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;
|
device: device; background: mod(index, 2) == 0 ? white : #f8fafc;
|
||||||
Rectangle { y: parent.height - 1px; height: 1px; background: #edf0f4; }
|
Rectangle { y: parent.height - 1px; height: 1px; background: #edf0f4; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { DeviceCardActions, DeviceCardStyle } from "device_card_actions.slint";
|
|||||||
import { GalleryMenuButton } from "gallery_menu_button.slint";
|
import { GalleryMenuButton } from "gallery_menu_button.slint";
|
||||||
import { GroupFilter } from "group_filter.slint";
|
import { GroupFilter } from "group_filter.slint";
|
||||||
import { ScrollView, LineEdit } from "std-widgets.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 {
|
export struct GalleryTile {
|
||||||
id: string,
|
id: string,
|
||||||
@@ -29,21 +29,72 @@ export struct GalleryActionDevice {
|
|||||||
connected: bool,
|
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 {
|
export component GalleryView inherits Rectangle {
|
||||||
in property <bool> controlling: false;
|
in property <bool> controlling: false;
|
||||||
in property <bool> simplified: false;
|
in property <bool> simplified: false;
|
||||||
private property <length> grid-gap: root.simplified ? 6px : 12px;
|
in property <bool> window-active: true;
|
||||||
private property <length> rail-card-height: root.simplified ? 92px : 76px;
|
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> focused-id: root.focused.id;
|
||||||
private property <string> device-tooltip-id;
|
private property <string> device-tooltip-id;
|
||||||
private property <string> device-tooltip-text;
|
private property <string> device-tooltip-text;
|
||||||
private property <length> device-tooltip-x;
|
private property <length> device-tooltip-x;
|
||||||
private property <length> device-tooltip-y;
|
private property <length> device-tooltip-y;
|
||||||
private property <bool> device-tooltip-visible: false;
|
private property <bool> device-tooltip-visible: false;
|
||||||
changed focused-id => { root.device-tooltip-id = ""; }
|
private property <string> drag-source;
|
||||||
changed visible => { root.device-tooltip-id = ""; if !root.visible { add-menu.close-menu(); batch-menu.close-menu(); } }
|
private property <GalleryTile> dragged-tile;
|
||||||
changed width => { root.device-tooltip-id = ""; }
|
private property <bool> dragging: false;
|
||||||
changed height => { root.device-tooltip-id = ""; }
|
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 => {
|
changed device-tooltip-id => {
|
||||||
root.device-tooltip-visible = false;
|
root.device-tooltip-visible = false;
|
||||||
device-tooltip-timer.running = root.device-tooltip-id != "";
|
device-tooltip-timer.running = root.device-tooltip-id != "";
|
||||||
@@ -65,8 +116,9 @@ export component GalleryView inherits Rectangle {
|
|||||||
in property <[GalleryTile]> tiles;
|
in property <[GalleryTile]> tiles;
|
||||||
in property <[string]> groups;
|
in property <[string]> groups;
|
||||||
in-out property <int> group-index: 0;
|
in-out property <int> group-index: 0;
|
||||||
in property <int> columns: 3;
|
// Pagination and rendering share the same fixed-aspect cell geometry.
|
||||||
in property <int> rows: 2;
|
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> page: 0;
|
||||||
in property <int> pages: 1;
|
in property <int> pages: 1;
|
||||||
in property <int> count: 0;
|
in property <int> count: 0;
|
||||||
@@ -91,6 +143,7 @@ export component GalleryView inherits Rectangle {
|
|||||||
private property <string> action-code;
|
private property <string> action-code;
|
||||||
in property <[GalleryActionDevice]> selected-devices;
|
in property <[GalleryActionDevice]> selected-devices;
|
||||||
callback action(string, string);
|
callback action(string, string);
|
||||||
|
callback swap-devices(string, string) -> bool;
|
||||||
callback add-device(string, string, string, bool) -> bool;
|
callback add-device(string, string, string, bool) -> bool;
|
||||||
callback import-recent(string) -> bool;
|
callback import-recent(string) -> bool;
|
||||||
callback edit-alias(string, string);
|
callback edit-alias(string, string);
|
||||||
@@ -100,10 +153,89 @@ export component GalleryView inherits Rectangle {
|
|||||||
changed adding => { if root.adding { root.action("release", ""); } }
|
changed adding => { if root.adding { root.action("release", ""); } }
|
||||||
changed importing => { root.action(root.importing ? "release" : "recent-close", ""); }
|
changed importing => { root.action(root.importing ? "release" : "recent-close", ""); }
|
||||||
public function reset-transient() {
|
public function reset-transient() {
|
||||||
|
root.resizing = false;
|
||||||
|
root.cancel-device-drag();
|
||||||
root.batch = false; root.adding = false; root.importing = false; root.confirming = false;
|
root.batch = false; root.adding = false; root.importing = false; root.confirming = false;
|
||||||
root.device-tooltip-id = ""; add-menu.close-menu(); batch-menu.close-menu();
|
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) {
|
function confirm-action(code: string, name: string) {
|
||||||
root.action-code = code; root.action-name = name; root.confirming = true;
|
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;
|
x: root.simplified ? 12px : toolbar.x;
|
||||||
y: root.simplified ? 12px : toolbar.y + toolbar.height + 12px;
|
y: root.simplified ? 12px : toolbar.y + toolbar.height + 12px;
|
||||||
width: parent.width - self.x * 2;
|
width: parent.width - self.x * 2;
|
||||||
height: footer.y - 12px - self.y;
|
height: footer.y - 12px - self.y;
|
||||||
visible: root.focused.id == "";
|
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 {
|
for tile[index] in root.tiles: tile-card := Rectangle {
|
||||||
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
|
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
|
||||||
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
: 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-role: button; accessible-label: self.details;
|
||||||
accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
|
accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
|
||||||
changed details => { if tile-touch.has-hover { tile-touch.show-details(); } }
|
changed details => { if tile-touch.has-hover { tile-touch.show-details(); } }
|
||||||
x: mod(index, root.columns) * (grid.width + root.grid-gap) / root.columns;
|
visible: index < root.columns * root.rows;
|
||||||
y: floor(index / root.columns) * (grid.height + root.grid-gap) / root.rows;
|
x: mod(index, root.columns) * root.card-width;
|
||||||
width: (grid.width - (root.columns - 1) * root.grid-gap) / root.columns;
|
y: floor(index / root.columns) * root.card-height;
|
||||||
height: (grid.height - (root.rows - 1) * root.grid-gap) / root.rows;
|
width: root.card-width;
|
||||||
border-radius: root.simplified ? 0px : 6px;
|
height: root.card-height;
|
||||||
background: !tile.online ? DeviceCardStyle.offline-background : root.simplified ? #24272d : tile.selected ? #eaf3ff : #eff0f2;
|
opacity: root.dragging && root.drag-source == tile.id ? 0.45 : 1;
|
||||||
border-width: root.simplified ? 0px : tile.selected ? 2px : 1px;
|
background: !tile.online ? DeviceCardStyle.offline-background
|
||||||
border-color: tile.selected ? #76aeea : root.batch && tile-touch.has-hover ? #94bdeb : !tile.online ? DeviceCardStyle.offline-border : #c9cdd2;
|
: tile.selected ? #eaf3ff : root.batch && tile-touch.has-hover ? #f3f8fe : white;
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: root.simplified ? 0px : 6px; y: self.x;
|
x: root.card-inset; y: self.x;
|
||||||
width: parent.width - self.x * 2;
|
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;
|
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 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 {
|
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;
|
horizontal-alignment: center; vertical-alignment: center;
|
||||||
wrap: word-wrap;
|
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 {
|
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);
|
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 {
|
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;
|
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;
|
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;
|
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;
|
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() {
|
function show-details() {
|
||||||
// Avoid covering the per-device action buttons with a tooltip.
|
// 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; }
|
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; root.device-tooltip-text = tile-card.details;
|
root.device-tooltip-id = tile.id;
|
||||||
root.device-tooltip-x = grid.x + tile-card.x;
|
|
||||||
root.device-tooltip-y = grid.y + tile-card.y + tile-card.height + 4px;
|
|
||||||
}
|
}
|
||||||
changed has-hover => {
|
changed has-hover => {
|
||||||
if self.has-hover { self.show-details(); }
|
if self.has-hover { self.show-details(); }
|
||||||
else if root.device-tooltip-id == tile.id { root.device-tooltip-id = ""; }
|
else if root.device-tooltip-id == tile.id { root.device-tooltip-id = ""; }
|
||||||
}
|
}
|
||||||
moved => { self.show-details(); }
|
private property <bool> over-actions: grid.mouse-y - tile-card.y >= tile-card.height - 30px;
|
||||||
clicked => { root.device-tooltip-id = ""; root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
|
changed over-actions => { if self.has-hover { self.show-details(); } }
|
||||||
if tile-touch.has-hover && !root.batch && !root.simplified: DeviceCardActions {
|
if tile-touch.has-hover && !root.dragging && !root.resizing && !root.batch && !root.simplified: DeviceCardActions {
|
||||||
x: parent.width - self.width - 6px; y: parent.height - 26px;
|
x: parent.width - self.width; y: parent.height - 23px;
|
||||||
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub");
|
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub");
|
||||||
connected: tile.connected;
|
connected: tile.connected;
|
||||||
edit-alias => { root.action("release", ""); root.edit-alias(tile.id, tile.name); }
|
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 {
|
if root.count == 0: Text {
|
||||||
text: root.simplified
|
text: root.simplified
|
||||||
? GalleryStrings.tr("暂无设备,退出精简模式后可添加", "No devices. Exit compact mode to add devices", "Нет устройств. Выйдите из компактного режима, чтобы добавить их")
|
? 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.
|
// Collapse the focused tile and its gap without changing the page model.
|
||||||
width: tile.id == root.focused.id ? 0px : 144px;
|
width: tile.id == root.focused.id ? 0px : 144px;
|
||||||
height: root.rail-card-height; visible: tile.id != root.focused.id;
|
height: root.rail-card-height; visible: tile.id != root.focused.id;
|
||||||
Rectangle {
|
rail-card := Rectangle {
|
||||||
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
|
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
|
||||||
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
|
||||||
private property <string> details: tile.name + "\nID: " + tile.id + "\n" + self.presence;
|
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); }
|
accessible-action-default => { root.action("focus", tile.id); }
|
||||||
changed details => { if rail-touch.has-hover { rail-touch.show-details(); } }
|
changed details => { if rail-touch.has-hover { rail-touch.show-details(); } }
|
||||||
Rectangle {
|
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 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 {
|
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;
|
text: tile.status; color: tile.online ? #e2e6ed : DeviceCardStyle.offline-placeholder; font-size: 10px; overflow: elide;
|
||||||
horizontal-alignment: center; vertical-alignment: center;
|
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 {
|
Text {
|
||||||
visible: !root.simplified;
|
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;
|
text: tile.name; color: tile.online ? #303943 : DeviceCardStyle.offline-text; font-size: 11px;
|
||||||
overflow: elide; vertical-alignment: center;
|
overflow: elide; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
rail-touch := TouchArea {
|
rail-touch := TouchArea {
|
||||||
function show-details() {
|
function show-details() {
|
||||||
root.device-tooltip-id = tile.id;
|
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 => {
|
changed has-hover => {
|
||||||
if self.has-hover { self.show-details(); }
|
if self.has-hover { self.show-details(); }
|
||||||
@@ -367,29 +622,19 @@ export component GalleryView inherits Rectangle {
|
|||||||
status-area := Rectangle {
|
status-area := Rectangle {
|
||||||
x: 0px; y: 0px; width: parent.width - 124px; height: parent.height;
|
x: 0px; y: 0px; width: parent.width - 124px; height: parent.height;
|
||||||
visible: !root.batch || root.simplified;
|
visible: !root.batch || root.simplified;
|
||||||
border-radius: 3px; background: network-touch.has-hover ? #edf0f4 : transparent;
|
connection-count := Text {
|
||||||
accessible-role: button;
|
x: 0px; width: min(self.preferred-width, parent.width - network-button.width - 12px); height: parent.height;
|
||||||
accessible-label: GalleryStrings.tr("网络状态", "Network", "Сеть") + ": " + root.connected-summary;
|
text: 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", "Сеть");
|
|
||||||
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
|
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
|
||||||
}
|
}
|
||||||
Text {
|
network-button := SecondaryButton {
|
||||||
x: parent.width - 13px; width: 12px; height: parent.height;
|
x: connection-count.x + connection-count.width + 12px;
|
||||||
text: FontAwesomeIcons.angle-up; font-family: "Font Awesome 6 Free"; font-weight: 900;
|
min-width: 64px; width: 64px; height: parent.height;
|
||||||
font-size: 9px; color: #858c96; vertical-alignment: center;
|
border-color: #c8cbd0; border-radius: 3px;
|
||||||
}
|
text: GalleryStrings.tr("网络状态", "Network", "Сеть");
|
||||||
network-touch := TouchArea {
|
accessible-role: button;
|
||||||
mouse-cursor: pointer;
|
accessible-label: self.text;
|
||||||
changed has-hover => {
|
accessible-action-default => { self.clicked(); }
|
||||||
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 = ""; }
|
|
||||||
}
|
|
||||||
clicked => { root.device-tooltip-id = ""; root.network-open(); }
|
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;
|
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; }
|
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.
|
for edge in 2: Rectangle {
|
||||||
if root.device-tooltip-visible && root.device-tooltip-id != "": 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);
|
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);
|
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;
|
width: min(320px, parent.width - 16px); height: device-tooltip-label.preferred-height + 16px;
|
||||||
|
|||||||
@@ -265,19 +265,20 @@ component PasswordInput inherits Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export component MainWindow inherits Window {
|
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");
|
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||||
preferred-width: 640px;
|
preferred-width: 640px;
|
||||||
preferred-height: 486px + root.titlebar-height;
|
preferred-height: root.minimum-window-height;
|
||||||
min-width: 640px;
|
min-width: 640px;
|
||||||
max-width: root.gallery-mode ? 16384px : 640px;
|
max-width: root.gallery-mode ? 16384px : 640px;
|
||||||
min-height: 486px + root.titlebar-height;
|
min-height: root.minimum-window-height;
|
||||||
max-height: root.gallery-mode ? 16384px : 486px + root.titlebar-height;
|
max-height: root.gallery-mode ? 16384px : root.minimum-window-height;
|
||||||
no-frame: root.custom-titlebar;
|
no-frame: root.custom-titlebar;
|
||||||
background: root.wayland-titlebar ? transparent : white;
|
background: root.wayland-titlebar ? transparent : white;
|
||||||
default-font-size: ImGuiFontStyle.base;
|
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;
|
in-out property <bool> gallery-simplified: false;
|
||||||
changed gallery-simplified => {
|
changed gallery-simplified => {
|
||||||
root.reset-gallery-transient();
|
root.reset-gallery-transient();
|
||||||
@@ -316,8 +317,10 @@ export component MainWindow inherits Window {
|
|||||||
in property <[GalleryTile]> gallery-tiles;
|
in property <[GalleryTile]> gallery-tiles;
|
||||||
in property <[string]> gallery-groups;
|
in property <[string]> gallery-groups;
|
||||||
in-out property <int> gallery-group-index: 0;
|
in-out property <int> gallery-group-index: 0;
|
||||||
in property <int> gallery-columns: 3;
|
out property <int> gallery-columns: gallery.columns;
|
||||||
in property <int> gallery-rows: 2;
|
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-page: 0;
|
||||||
in property <int> gallery-pages: 1;
|
in property <int> gallery-pages: 1;
|
||||||
in property <int> gallery-count: 0;
|
in property <int> gallery-count: 0;
|
||||||
@@ -335,11 +338,10 @@ export component MainWindow inherits Window {
|
|||||||
in property <[GalleryRecentDevice]> gallery-recent-devices;
|
in property <[GalleryRecentDevice]> gallery-recent-devices;
|
||||||
in property <int> gallery-recent-selected: 0;
|
in property <int> gallery-recent-selected: 0;
|
||||||
in property <string> gallery-import-group;
|
in property <string> gallery-import-group;
|
||||||
out property <float> gallery-width: root.width / 1px;
|
callback open-gallery();
|
||||||
out property <float> gallery-height: gallery.height / 1px;
|
|
||||||
callback change-mode(bool);
|
|
||||||
callback toggle-gallery-fullscreen();
|
callback toggle-gallery-fullscreen();
|
||||||
callback gallery-action(string, string);
|
callback gallery-action(string, string);
|
||||||
|
callback gallery-swap(string, string) -> bool;
|
||||||
callback gallery-add-device(string, string, string, bool) -> bool;
|
callback gallery-add-device(string, string, string, bool) -> bool;
|
||||||
callback gallery-import-recent(string) -> bool;
|
callback gallery-import-recent(string) -> bool;
|
||||||
in-out property <bool> device-group-open: false;
|
in-out property <bool> device-group-open: false;
|
||||||
@@ -367,7 +369,9 @@ export component MainWindow inherits Window {
|
|||||||
in property <bool> window-active: true;
|
in property <bool> window-active: true;
|
||||||
private property <length> titlebar-height:
|
private property <length> titlebar-height:
|
||||||
root.custom-titlebar && !root.gallery-fullscreen ? (root.wayland-titlebar ? 32px : 31px) : 0px;
|
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;
|
private property <length> wayland-corner-radius: 12px;
|
||||||
in property <string> local-id: "";
|
in property <string> local-id: "";
|
||||||
in property <string> local-password: "";
|
in property <string> local-password: "";
|
||||||
@@ -585,7 +589,7 @@ export component MainWindow inherits Window {
|
|||||||
x: 80px;
|
x: 80px;
|
||||||
width: parent.width - 160px;
|
width: parent.width - 160px;
|
||||||
height: parent.height;
|
height: parent.height;
|
||||||
text: "CrossDesk";
|
text: root.title;
|
||||||
color: root.window-active ? #3d3d3d : #747474;
|
color: root.window-active ? #3d3d3d : #747474;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -686,7 +690,7 @@ export component MainWindow inherits Window {
|
|||||||
x: 30px;
|
x: 30px;
|
||||||
width: parent.width - 122px;
|
width: parent.width - 122px;
|
||||||
height: parent.height;
|
height: parent.height;
|
||||||
text: "CrossDesk";
|
text: root.title;
|
||||||
color: #202124;
|
color: #202124;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
@@ -804,15 +808,19 @@ export component MainWindow inherits Window {
|
|||||||
z: 7;
|
z: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App actions live in the content header; window controls and dragging
|
// The connection page shares its section heading row with the menu.
|
||||||
// remain in the native or Windows-specific title bar above it.
|
// Only DeskHub needs a separate toolbar above its content.
|
||||||
menu-button := IconButton {
|
menu-button := IconButton {
|
||||||
x: parent.width - mode-switch.x - self.width;
|
visible: !root.gallery-mode;
|
||||||
y: mode-switch.y;
|
x: parent.width - 29px - self.width;
|
||||||
width: mode-switch.height;
|
y: 10.5px;
|
||||||
height: mode-switch.height;
|
width: 28px;
|
||||||
|
height: self.width;
|
||||||
icon: FontAwesomeIcons.bars;
|
icon: FontAwesomeIcons.bars;
|
||||||
icon-size: 14px;
|
icon-size: 14px;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #c9cdd2;
|
||||||
|
border-radius: 4px;
|
||||||
hover-background: #e1e5eb;
|
hover-background: #e1e5eb;
|
||||||
pressed-background: #cbd1d9;
|
pressed-background: #cbd1d9;
|
||||||
tooltip: root.announcement-unread > 0
|
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; }
|
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
||||||
}
|
}
|
||||||
// Unread messages take precedence so the menu has only one indicator.
|
// 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;
|
x: menu-button.x + menu-button.width - self.width + 4px;
|
||||||
y: menu-button.y - 1px;
|
y: menu-button.y - 1px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -863,7 +871,7 @@ export component MainWindow inherits Window {
|
|||||||
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
|
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;
|
x: menu-button.x + menu-button.width - self.width + 4px;
|
||||||
y: menu-button.y - 1px;
|
y: menu-button.y - 1px;
|
||||||
width: max(16px, unread-badge-label.preferred-width + 8px);
|
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;
|
x: 29px; y: root.content-header-height - self.height; width: 164px; height: 24px;
|
||||||
background: #e2e4e7;
|
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
|
||||||
border-width: 1px; border-color: #c8cbd0; border-radius: 3px;
|
color: #24272d; font-weight: 600; font-size: 12px;
|
||||||
Rectangle {
|
vertical-alignment: center;
|
||||||
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); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if root.gallery-mode: CompactButton {
|
if root.gallery-mode: CompactButton {
|
||||||
x: menu-button.x - 24px - 16px - self.width;
|
x: parent.width - 29px - 24px - 8px - self.width;
|
||||||
y: mode-switch.y; width: 94px; height: mode-switch.height;
|
y: root.content-header-height - self.height; width: 94px; height: 24px;
|
||||||
primary: root.gallery-simplified;
|
primary: root.gallery-simplified;
|
||||||
text: root.gallery-simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид")
|
text: root.gallery-simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид")
|
||||||
: GalleryStrings.tr("精简模式", "Compact", "Компактно");
|
: GalleryStrings.tr("精简模式", "Compact", "Компактно");
|
||||||
clicked => { root.gallery-simplified = !root.gallery-simplified; }
|
clicked => { root.gallery-simplified = !root.gallery-simplified; }
|
||||||
}
|
}
|
||||||
if root.gallery-mode: IconButton {
|
if root.gallery-mode: IconButton {
|
||||||
x: menu-button.x - self.width - 8px;
|
x: parent.width - 29px - self.width;
|
||||||
y: mode-switch.y; width: 24px; height: mode-switch.height;
|
y: root.content-header-height - self.height; width: 24px; height: 24px;
|
||||||
icon: root.gallery-fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
|
icon: root.gallery-fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
|
||||||
tooltip: root.gallery-fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
|
tooltip: root.gallery-fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
|
||||||
clicked => { root.toggle-gallery-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; }
|
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 {
|
Rectangle {
|
||||||
x: 356px;
|
x: 356px;
|
||||||
y: 42px;
|
y: 42px;
|
||||||
@@ -1125,8 +1115,23 @@ export component MainWindow inherits Window {
|
|||||||
|
|
||||||
Rectangle { y: 0px; height: 1px; background: ImGuiLineStyle.border; }
|
Rectangle { y: 0px; height: 1px; background: ImGuiLineStyle.border; }
|
||||||
|
|
||||||
SectionTitle { x: 29px; y: 7px; height: 28px; text: UiStrings.recent-connections; }
|
SectionTitle { x: 29px; y: 7px; width: open-gallery-button.x - self.x - 12px; height: 28px; text: UiStrings.recent-connections; }
|
||||||
GroupFilter {
|
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;
|
x: parent.width - self.width - 29px; y: 8px;
|
||||||
width: 155px; height: 28px;
|
width: 155px; height: 28px;
|
||||||
model: root.recent-groups;
|
model: root.recent-groups;
|
||||||
@@ -1338,9 +1343,11 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
gallery := GalleryView {
|
gallery := GalleryView {
|
||||||
y: root.content-header-height; width: parent.width;
|
y: root.content-header-height; width: parent.width;
|
||||||
height: parent.height - self.y - 24px;
|
height: parent.height - self.y;
|
||||||
visible: root.gallery-mode;
|
visible: root.gallery-mode;
|
||||||
simplified: root.gallery-simplified;
|
simplified: root.gallery-simplified;
|
||||||
|
window-active: root.window-active;
|
||||||
|
tile-width <=> root.gallery-tile-width;
|
||||||
network-open => { root.gallery-network-open = true; }
|
network-open => { root.gallery-network-open = true; }
|
||||||
controlling: root.gallery-controlling;
|
controlling: root.gallery-controlling;
|
||||||
pointer-input(button, kind, x, y) => { root.gallery-pointer(button, kind, x, y); }
|
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); }
|
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;
|
tiles: root.gallery-tiles; groups: root.gallery-groups;
|
||||||
group-index <=> root.gallery-group-index;
|
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;
|
page: root.gallery-page; pages: root.gallery-pages; count: root.gallery-count;
|
||||||
connected-count: root.gallery-connected; selected-count: root.gallery-selected;
|
connected-count: root.gallery-connected; selected-count: root.gallery-selected;
|
||||||
selected-audio-available: root.gallery-selected-audio-available;
|
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;
|
importing <=> root.gallery-importing; recent-devices: root.gallery-recent-devices;
|
||||||
recent-selected: root.gallery-recent-selected; import-group: root.gallery-import-group;
|
recent-selected: root.gallery-recent-selected; import-group: root.gallery-import-group;
|
||||||
action(code, value) => { root.gallery-action(code, value); }
|
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) }
|
add-device(id, password, group, remember) => { root.gallery-add-device(id, password, group, remember) }
|
||||||
import-recent(group) => { root.gallery-import-recent(group) }
|
import-recent(group) => { root.gallery-import-recent(group) }
|
||||||
edit-alias(id, name) => {
|
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;
|
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.
|
// Server status and reconnect actions belong to the connection window.
|
||||||
Rectangle {
|
if !root.gallery-mode: Rectangle {
|
||||||
y: parent.height - 24px;
|
y: parent.height - 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
@@ -1481,7 +1488,7 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dismiss the title menu when clicking anywhere outside of it.
|
// 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;
|
x: 0px;
|
||||||
y: 0px;
|
y: 0px;
|
||||||
width: parent.width;
|
width: parent.width;
|
||||||
@@ -1494,7 +1501,7 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Title menu.
|
// Title menu.
|
||||||
if root.menu-open: Rectangle {
|
if !root.gallery-mode && root.menu-open: Rectangle {
|
||||||
x: menu-button.x + menu-button.width - self.width;
|
x: menu-button.x + menu-button.width - self.width;
|
||||||
y: menu-button.y + menu-button.height + 2px;
|
y: menu-button.y + menu-button.height + 2px;
|
||||||
width: max(80px, max(menu-settings-label.preferred-width,
|
width: max(80px, max(menu-settings-label.preferred-width,
|
||||||
@@ -1953,19 +1960,14 @@ export component MainWindow inherits Window {
|
|||||||
DialogSurface {
|
DialogSurface {
|
||||||
x: (parent.width - self.width) / 2;
|
x: (parent.width - self.width) / 2;
|
||||||
y: (parent.height - self.height) / 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;
|
background: #f8fafc; border-radius: 14px; border-color: #e1e7ef;
|
||||||
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
|
drop-shadow-color: #14294424; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px;
|
||||||
TouchArea { clicked => { focus-sink.focus(); } }
|
TouchArea { clicked => { focus-sink.focus(); } }
|
||||||
Text {
|
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;
|
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 {
|
Rectangle {
|
||||||
x: 18px; y: 56px; width: parent.width - 36px; height: 40px;
|
x: 18px; y: 56px; width: parent.width - 36px; height: 40px;
|
||||||
background: #eaf0f6; border-radius: 9px;
|
background: #eaf0f6; border-radius: 9px;
|
||||||
@@ -1995,7 +1997,7 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if root.settings-tab == 0: settings-scroll := ScrollView {
|
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;
|
viewport-y <=> root.settings-scroll-y;
|
||||||
VerticalLayout {
|
VerticalLayout {
|
||||||
width: settings-scroll.width - 12px;
|
width: settings-scroll.width - 12px;
|
||||||
@@ -2049,15 +2051,15 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if root.settings-tab == 1: GallerySettings {
|
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;
|
resolution-index <=> root.gallery-resolution-draft;
|
||||||
frame-rate-index <=> root.gallery-frame-rate-draft;
|
frame-rate-index <=> root.gallery-frame-rate-draft;
|
||||||
bandwidth-index <=> root.gallery-bandwidth-draft;
|
bandwidth-index <=> root.gallery-bandwidth-draft;
|
||||||
error: root.gallery-settings-error;
|
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 {
|
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 {
|
if root.settings-tab == 1: SettingsButton {
|
||||||
width: 110px; quiet: true;
|
width: 110px; quiet: true;
|
||||||
text: GalleryStrings.tr("恢复默认", "Reset defaults", "По умолчанию");
|
text: GalleryStrings.tr("恢复默认", "Reset defaults", "По умолчанию");
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
__weak NSWindow *main_window = nil;
|
__weak NSWindow *gallery_window = nil;
|
||||||
bool main_fullscreen = false;
|
bool gallery_fullscreen = false;
|
||||||
bool main_gallery = false;
|
NSRect gallery_restore_frame = NSZeroRect;
|
||||||
NSRect main_restore_frame = NSZeroRect;
|
NSWindowStyleMask gallery_restore_style = NSWindowStyleMaskTitled;
|
||||||
NSWindowStyleMask main_restore_style = NSWindowStyleMaskTitled;
|
NSApplicationPresentationOptions gallery_restore_presentation;
|
||||||
NSApplicationPresentationOptions main_restore_presentation;
|
|
||||||
__weak NSWindow *stream_window = nil;
|
__weak NSWindow *stream_window = nil;
|
||||||
__weak NSView *stream_slint_view = nil;
|
__weak NSView *stream_slint_view = nil;
|
||||||
CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil;
|
CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil;
|
||||||
@@ -118,9 +117,8 @@ bool ConfigureMainWindowChrome(void *opaque_slint_view, bool gallery) {
|
|||||||
NSButton *zoom_button =
|
NSButton *zoom_button =
|
||||||
[window standardWindowButton:NSWindowZoomButton];
|
[window standardWindowButton:NSWindowZoomButton];
|
||||||
if (zoom_button == nil) return false;
|
if (zoom_button == nil) return false;
|
||||||
main_window = window;
|
if (gallery) gallery_window = window;
|
||||||
main_gallery = gallery;
|
if (gallery && gallery_fullscreen) return true;
|
||||||
if (main_fullscreen) return true;
|
|
||||||
if (gallery)
|
if (gallery)
|
||||||
window.styleMask |= NSWindowStyleMaskResizable;
|
window.styleMask |= NSWindowStyleMaskResizable;
|
||||||
else
|
else
|
||||||
@@ -149,63 +147,63 @@ bool ConfigureMainWindowChrome(void *opaque_slint_view, bool gallery) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SetMainWindowFullscreen(bool fullscreen) {
|
bool SetGalleryWindowFullscreen(bool fullscreen) {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
if (!main_window || (fullscreen && !main_gallery)) return false;
|
if (!gallery_window) return false;
|
||||||
if (main_fullscreen == fullscreen) return true;
|
if (gallery_fullscreen == fullscreen) return true;
|
||||||
if (fullscreen) {
|
if (fullscreen) {
|
||||||
NSScreen *screen = main_window.screen ?: NSScreen.mainScreen;
|
NSScreen *screen = gallery_window.screen ?: NSScreen.mainScreen;
|
||||||
if (!screen) return false;
|
if (!screen) return false;
|
||||||
main_restore_frame = main_window.frame;
|
gallery_restore_frame = gallery_window.frame;
|
||||||
main_restore_style = main_window.styleMask;
|
gallery_restore_style = gallery_window.styleMask;
|
||||||
main_restore_presentation = NSApp.presentationOptions;
|
gallery_restore_presentation = NSApp.presentationOptions;
|
||||||
main_fullscreen = true;
|
gallery_fullscreen = true;
|
||||||
main_window.styleMask |= NSWindowStyleMaskFullSizeContentView;
|
gallery_window.styleMask |= NSWindowStyleMaskFullSizeContentView;
|
||||||
main_window.titleVisibility = NSWindowTitleHidden;
|
gallery_window.titleVisibility = NSWindowTitleHidden;
|
||||||
main_window.movable = NO;
|
gallery_window.movable = NO;
|
||||||
SetTitlebarButtonsHidden(main_window, YES);
|
SetTitlebarButtonsHidden(gallery_window, YES);
|
||||||
NSApp.presentationOptions |=
|
NSApp.presentationOptions |=
|
||||||
NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar;
|
NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar;
|
||||||
// Let the next event-loop frame paint the updated Slint layout. A
|
// Let the next event-loop frame paint the updated Slint layout. A
|
||||||
// synchronous draw here exposes stale grid geometry during the resize.
|
// synchronous draw here exposes stale grid geometry during the resize.
|
||||||
[main_window setFrame:screen.frame display:NO animate:NO];
|
[gallery_window setFrame:screen.frame display:NO animate:NO];
|
||||||
} else {
|
} else {
|
||||||
main_fullscreen = false;
|
gallery_fullscreen = false;
|
||||||
NSApp.presentationOptions = main_restore_presentation;
|
NSApp.presentationOptions = gallery_restore_presentation;
|
||||||
main_window.styleMask = main_restore_style;
|
gallery_window.styleMask = gallery_restore_style;
|
||||||
main_window.titleVisibility = NSWindowTitleVisible;
|
gallery_window.titleVisibility = NSWindowTitleVisible;
|
||||||
main_window.movable = YES;
|
gallery_window.movable = YES;
|
||||||
[main_window standardWindowButton:NSWindowCloseButton].hidden = NO;
|
[gallery_window standardWindowButton:NSWindowCloseButton].hidden = NO;
|
||||||
[main_window standardWindowButton:NSWindowMiniaturizeButton].hidden = NO;
|
[gallery_window standardWindowButton:NSWindowMiniaturizeButton].hidden = NO;
|
||||||
// The main window uses the in-app fullscreen action in both modes.
|
// DeskHub uses its in-app fullscreen action.
|
||||||
HideMainWindowZoomButton(main_window);
|
HideMainWindowZoomButton(gallery_window);
|
||||||
[main_window setFrame:main_restore_frame display:NO animate:NO];
|
[gallery_window setFrame:gallery_restore_frame display:NO animate:NO];
|
||||||
}
|
}
|
||||||
main_window.contentView.needsDisplay = YES;
|
gallery_window.contentView.needsDisplay = YES;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool IsMainWindowFullscreen() { return main_fullscreen; }
|
bool IsGalleryWindowFullscreen() { return gallery_fullscreen; }
|
||||||
bool IsMainWindowActive() { return main_window && [NSApp isActive] && [main_window isKeyWindow]; }
|
bool IsGalleryWindowActive() { return gallery_window && [NSApp isActive] && [gallery_window isKeyWindow]; }
|
||||||
|
|
||||||
bool MaximizeMainWindowWithoutAnimation() {
|
bool MaximizeGalleryWindowWithoutAnimation() {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
if (!main_window || !main_gallery || main_fullscreen) return false;
|
if (!gallery_window || gallery_fullscreen) return false;
|
||||||
NSScreen *screen = main_window.screen ?: NSScreen.mainScreen;
|
NSScreen *screen = gallery_window.screen ?: NSScreen.mainScreen;
|
||||||
if (!screen) return false;
|
if (!screen) return false;
|
||||||
[main_window setFrame:screen.visibleFrame display:YES animate:NO];
|
[gallery_window setFrame:screen.visibleFrame display:YES animate:NO];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsMainWindowMaximized() {
|
bool IsGalleryWindowMaximized() {
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
if (!main_window || !main_gallery || main_fullscreen) return false;
|
if (!gallery_window || gallery_fullscreen) return false;
|
||||||
NSScreen *screen = main_window.screen;
|
NSScreen *screen = gallery_window.screen;
|
||||||
if (!screen) return false;
|
if (!screen) return false;
|
||||||
// isZoomed can report true for a constrained compact window. Only treat
|
// isZoomed can report true for a constrained compact window. Only treat
|
||||||
// the ordinary window as maximized when it fills the usable screen.
|
// the ordinary window as maximized when it fills the usable screen.
|
||||||
const NSRect frame = main_window.frame;
|
const NSRect frame = gallery_window.frame;
|
||||||
const NSRect available = screen.visibleFrame;
|
const NSRect available = screen.visibleFrame;
|
||||||
return std::abs(frame.origin.x - available.origin.x) < 1 &&
|
return std::abs(frame.origin.x - available.origin.x) < 1 &&
|
||||||
std::abs(frame.origin.y - available.origin.y) < 1 &&
|
std::abs(frame.origin.y - available.origin.y) < 1 &&
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ namespace crossdesk {
|
|||||||
// left-mouse-down event. Returns false when no suitable native event exists.
|
// left-mouse-down event. Returns false when no suitable native event exists.
|
||||||
bool StartNativeWindowDrag();
|
bool StartNativeWindowDrag();
|
||||||
|
|
||||||
// Sets a white native titlebar and hides its zoom button in both main-window
|
// Configures the connection or DeskHub window with a white native titlebar.
|
||||||
// modes. DeskHub remains resizable. |slint_view| is its AppKit NSView*.
|
// Only DeskHub is resizable. |slint_view| is its AppKit NSView*.
|
||||||
// Returns false while the native window or its titlebar controls are not ready.
|
// Returns false while the native window or its titlebar controls are not ready.
|
||||||
bool ConfigureMainWindowChrome(void* slint_view, bool gallery = false);
|
bool ConfigureMainWindowChrome(void* slint_view, bool gallery = false);
|
||||||
// Winit's initial macOS position is a content origin, while subsequent moves
|
// Winit's initial macOS position is a content origin, while subsequent moves
|
||||||
// use the outer frame. Return the logical titlebar inset for initial placement.
|
// use the outer frame. Return the logical titlebar inset for initial placement.
|
||||||
float MainWindowInitialPositionYOffset();
|
float MainWindowInitialPositionYOffset();
|
||||||
bool SetMainWindowFullscreen(bool fullscreen);
|
bool SetGalleryWindowFullscreen(bool fullscreen);
|
||||||
bool IsMainWindowFullscreen();
|
bool IsGalleryWindowFullscreen();
|
||||||
bool IsMainWindowActive();
|
bool IsGalleryWindowActive();
|
||||||
// AppKit's animated zoom spins a nested run loop and cannot be called from a
|
// AppKit's animated zoom spins a nested run loop and cannot be called from a
|
||||||
// Slint timer. Restore maximization by setting the visible screen frame directly.
|
// Slint timer. Restore maximization by setting the visible screen frame directly.
|
||||||
bool MaximizeMainWindowWithoutAnimation();
|
bool MaximizeGalleryWindowWithoutAnimation();
|
||||||
bool IsMainWindowMaximized();
|
bool IsGalleryWindowMaximized();
|
||||||
|
|
||||||
// Configures live resize and replaces the stream window's native Space
|
// Configures live resize and replaces the stream window's native Space
|
||||||
// fullscreen action with an immediate, single-window fullscreen transition.
|
// fullscreen action with an immediate, single-window fullscreen transition.
|
||||||
|
|||||||
+13
-10
@@ -1,6 +1,8 @@
|
|||||||
# 桌面墙(DeskHub)
|
# 桌面墙(DeskHub)
|
||||||
|
|
||||||
主窗口标题栏下方可以切换“连接”和“桌面墙”,同一行右侧保留纯文字“精简模式”按钮、全屏图标和菜单。设备管理栏集中显示分组、设备数量、“选择”和“添加”入口。每次启动软件默认进入“连接”页,不恢复上次退出时的页面;保存的位置会在窗口显示前恢复。连接页保留原有布局和紧凑尺寸;桌面墙可以调整尺寸及全屏。普通窗口切换模式时保持左上角位置,仅分别记忆尺寸。切回连接页会退出全屏并恢复普通窗口位置与紧凑尺寸,再进入桌面墙会恢复它的尺寸及全屏状态。已断开的显示器所对应的位置会修正到可用屏幕内。
|
主窗口“近期连接”标题行右侧、分组筛选左边的“桌面墙”按钮打开独立的桌面墙窗口。菜单与上方桌面标题共用一行,连接窗口保持紧凑尺寸。重复点击按钮会唤起已有桌面墙窗口,最小化时恢复显示,不创建重复窗口。每次启动软件只显示连接窗口;桌面墙首次打开时恢复保存的位置、尺寸及全屏状态;没有已保存尺寸时默认采用 960 × 720 逻辑像素(含标题栏,当前 macOS 的内容区为 960 × 688),两个窗口分别记忆位置。关闭桌面墙只隐藏该窗口并释放远程操控,保留设备连接;再次点击“桌面墙”即可打开。已断开的显示器所对应的位置会修正到可用屏幕内。
|
||||||
|
|
||||||
|
桌面墙窗口顶部显示“桌面墙”标题,右侧保留纯文字“精简模式”按钮和全屏图标,不显示菜单及消息、更新角标。设备管理栏集中显示分组、设备数量、“选择”和“添加”入口。桌面墙可以单独调整尺寸及全屏,不改变连接窗口的尺寸和位置。全局设置从主窗口菜单进入;设备操作和桌面墙连接的密码弹窗显示在桌面墙窗口内。
|
||||||
|
|
||||||
全屏按钮通过短暂淡出、等待窗口尺寸稳定、淡入完成切换,常规总时长约 0.2 秒;过渡期间屏蔽设备操作与重复全屏点击。窗口尺寸变化会立即更新网格,避免在新窗口中先显示旧的行列布局。过渡复用现有 UI 更新循环,不启动嵌套事件循环;失败或超时会恢复内容与交互。
|
全屏按钮通过短暂淡出、等待窗口尺寸稳定、淡入完成切换,常规总时长约 0.2 秒;过渡期间屏蔽设备操作与重复全屏点击。窗口尺寸变化会立即更新网格,避免在新窗口中先显示旧的行列布局。过渡复用现有 UI 更新循环,不启动嵌套事件循环;失败或超时会恢复内容与交互。
|
||||||
|
|
||||||
@@ -12,19 +14,20 @@
|
|||||||
- “添加 → 从近期连接添加”支持多选、全选和统一指定分组。导入后只加入桌面墙,使用“选择 → 连接”再发起连接。同一分组内重复设备会跳过;不指定分组时,已在桌面墙中的设备不会重复加入。
|
- “添加 → 从近期连接添加”支持多选、全选和统一指定分组。导入后只加入桌面墙,使用“选择 → 连接”再发起连接。同一分组内重复设备会跳过;不指定分组时,已在桌面墙中的设备不会重复加入。
|
||||||
- 连接成功后立即记录在连接页的“最近连接”中,无需等到断开,也不要求记住密码。失败的连接不会新增最近记录。
|
- 连接成功后立即记录在连接页的“最近连接”中,无需等到断开,也不要求记住密码。失败的连接不会新增最近记录。
|
||||||
- “全部设备”显示保存的成员及当前活动会话。分组成员与连接相互独立:断开后成员仍在,重启后不会自动连接。
|
- “全部设备”显示保存的成员及当前活动会话。分组成员与连接相互独立:断开后成员仍在,重启后不会自动连接。
|
||||||
- 卡片名称栏只显示设备名,长名称省略,不再显示状态圆点。未在线的设备(离线或在线状态未知)整张卡片使用灰色,包括预览背景、边框和名称;在线但尚未连接的设备保持正常样式。选中边框、复选框和操作入口仍可用。悬停画面可查看完整名称、ID 和在线状态,精简模式也支持。在线状态订阅包含全部分组中的设备;尚未收到结果、结果过期或服务器断连时显示未知,已有活动连接的设备仍显示在线。
|
- 卡片名称栏只显示设备名,长名称省略,不再显示状态圆点。未在线的设备(离线或在线状态未知)整张卡片使用灰色,包括预览背景、边框和名称;在线但尚未连接的设备保持正常样式。选中边框、复选框和操作入口仍可用。悬停画面可查看设备名称、ID 和在线状态,精简模式也支持。在线状态订阅包含全部分组中的设备;尚未收到结果、结果过期或服务器断连时显示未知,已有活动连接的设备仍显示在线。
|
||||||
- 鼠标悬停时,名称栏右侧显示与近期连接一致的改名、修改分组、移除、连接/断开操作条,并显示按钮提示。两处的连接按钮均随实际会话状态切换:未连接时显示连接箭头,已连接时显示叉号,点击断开该设备;点卡片仍可查看设备。名称与近期连接共用别名;移除会先确认,只移除当前分组或桌面墙中的成员,不删除近期记录或断开会话。批量选择时隐藏操作条。
|
- 鼠标悬停时,名称栏右侧显示与近期连接一致的改名、修改分组、移除、连接/断开操作条,并显示按钮提示。两处的连接按钮均随实际会话状态切换:未连接时显示连接箭头,已连接时显示叉号,点击断开该设备;点卡片仍可查看设备。名称与近期连接共用别名;移除会先确认,只移除当前分组或桌面墙中的成员,不删除近期记录或断开会话。批量选择时隐藏操作条。
|
||||||
- 两处的“修改分组”共用操作条组件、分组弹窗和保存逻辑。可以选择多个已有分组、填写新分组,或选“未分组”清除分组归属。分组与桌面墙成员独立保存:近期连接修改分组不会自动加入桌面墙,也不会自动连接;通过“添加”菜单导入或按设备 ID 添加,才保存为桌面墙成员,未指定新分组时保留已有分组。从“全部设备”移除成员后仍保留其分组信息;在特定分组中移除则取消对应分组归属,移除最后一个分组后仍保留在“全部设备”中。修改分组保留墙内设备的顺序,保存失败时保留弹窗和输入。
|
- 两处的“修改分组”共用操作条组件、分组弹窗和保存逻辑。可以选择多个已有分组、填写新分组,或选“未分组”清除分组归属。分组与桌面墙成员独立保存:近期连接修改分组不会自动加入桌面墙,也不会自动连接;通过“添加”菜单导入或按设备 ID 添加,才保存为桌面墙成员,未指定新分组时保留已有分组。从“全部设备”移除成员后仍保留其分组信息;在特定分组中移除则取消对应分组归属,移除最后一个分组后仍保留在“全部设备”中。修改分组保留墙内设备的顺序,保存失败时保留弹窗和输入。
|
||||||
- 近期连接卡片使用与桌面墙相同的离线灰色,不再在缩略图上叠加在线/离线标签。不在线时减淡历史缩略图并将名称栏置灰;没有缩略图时,平台占位也使用相同灰色。在线或已连接的设备正常显示,悬停仍可查看在线状态和操作设备。
|
- 近期连接卡片使用与桌面墙相同的离线灰色,不再在缩略图上叠加在线/离线标签。不在线时减淡历史缩略图并将名称栏置灰;没有缩略图时,平台占位也使用相同灰色。在线或已连接的设备正常显示,悬停仍可查看在线状态和操作设备。
|
||||||
- 近期连接的设备提示浮窗显示所属分组;多个分组并列显示并自动换行,没有分组时显示“未分组”。分组变更后自动刷新。
|
- 近期连接的设备提示浮窗显示所属分组;多个分组并列显示并自动换行,没有分组时显示“未分组”。分组变更后自动刷新。
|
||||||
- “近期连接”标题行右侧可按“全部设备”、未分组或已有分组筛选;一个设备可出现在它所属的多个分组中。筛选与桌面墙当前分组独立,不改变历史记录、在线状态订阅或桌面墙成员。分组修改后自动更新结果;当前分组被清空删除时回到全部设备。
|
- “近期连接”标题行右侧可按“全部设备”、未分组或已有分组筛选;一个设备可出现在它所属的多个分组中。筛选与桌面墙当前分组独立,不改变历史记录、在线状态订阅或桌面墙成员。分组修改后自动更新结果;当前分组被清空删除时回到全部设备。
|
||||||
- 网格随窗口大小增加行列,紧凑窗口为 3 × 2;翻页不关闭连接。分组内顺序不会按在线状态自动重排。
|
- 网格中每台设备的预览区域统一固定为 16:9,普通模式的设备名称栏位于画面下方;其他比例的远端桌面留边完整显示。设备信息提示显示在该设备预览区域内部左下角,宽高受画面范围限制;过长的名称省略显示,保留 ID 和在线状态,不遮挡相邻设备或底部操作栏,精简模式及放大视图的缩略设备同样适用。设备直接放在连续表格的单元格中,单元格之间没有卡片间距,共用贯通整行、整列的单条分隔线;空单元格保留表格线,不再为每台设备绘制独立边框和右下角拖拽标记。拖动任意横向、竖向分隔线或交点,可同步放大、缩小全部单元格,预览区域始终保持 16:9;分隔线两侧的拖动方向一致。普通和精简模式共用单元格宽度,表格按实际尺寸自动换行、调整分页容量;窗口较小时限制格子大小,避免超出可用区域,窗口恢复后继续使用原尺寸。宽度至少为 184 逻辑像素、至多为 960 逻辑像素,并受当前可用区域限制;松开鼠标后保存尺寸,重启后恢复。拖动分隔线不会触发展开、选择或设备操作。翻页不关闭连接,分组内顺序不会按在线状态自动重排。
|
||||||
- 顶部“精简模式”切换为仅画面的设备卡片,隐藏名称、在线状态、状态文字和悬停操作条,同时收起设备管理工具栏;画面完整等比显示,不裁切。顶部固定宽度的文字按钮高亮显示“退出精简”,不带图标;全屏及菜单入口仍可见。最底部保留与连接页一致的独立服务器状态栏,其上方保留连接计数、网络详情入口和分页。点击图片仍可展开查看和接管操控,返回后保持精简模式;模式切换会释放操控、退出批量选择并返回网格,不改变连接、分组和设备顺序。
|
- 按住单元格内的设备画面或名称拖动,可与另一台设备交换位置;目标单元格高亮,鼠标旁显示设备缩略图和松开提示。跨页时保持按住鼠标,将设备拖到窗口内容区左、右边缘内侧 32 逻辑像素范围,或上一页、下一页按钮,悬停 700 毫秒后翻页;边缘显示方向提示并在触发区域高亮,继续停留可连续翻页,到首尾页自动停止。翻页期间保留拖动源,末页设备不足一整页时也可继续拖动,移到目标设备后松开完成交换;离开翻页区域会重新计算悬停时间,在可翻页的边缘区域或按钮上松开只取消拖动,不会误与边缘下方的设备换位;窗口外不触发翻页。普通、精简及批量选择模式均支持,短距离点击仍执行原来的查看或选择操作。拖到自身、空单元格、分隔线或表格外不会换位;Esc、窗口失焦、切换分组或调整窗口大小会取消拖动。交换后的顺序保存到本地,并应用于所有分组视图,不修改分组、批量选择、连接或静音状态;保存失败时保持原顺序并提示重试。仅活动会话也可换位,排序不会自动将它加入保存的桌面墙成员。
|
||||||
- 菜单中的全局“设置”顶部并排提供“常规”和“桌面墙”两个选项卡;“桌面墙”可统一配置桌面墙设备的传输分辨率上限和帧率,在连接页面以及桌面墙普通、精简、展开和全屏视图中均可打开。切换选项卡保留未保存修改,底部统一保存或取消。分辨率支持自动、320 × 180、640 × 360、1280 × 720、1920 × 1080、2560 × 1440、3840 × 2160和原始分辨率;固定上限适用于缩略图和展开画面,保持比例且不放大源画面。帧率支持跟随设备设置、15、30、60 FPS。保存后立即更新当前桌面墙会话,也用于后续桌面墙连接,不影响普通连接,跨分组和分页生效;取消不应用修改,恢复默认需保存后生效。配置保存在本地,重启后保留;保存失败时保留面板和输入。打开设置会释放远程操控。
|
- 顶部“精简模式”切换为仅画面的设备卡片,隐藏名称、在线状态、状态文字和悬停操作条,同时收起设备管理工具栏;画面完整等比显示,不裁切。顶部固定宽度的文字按钮高亮显示“退出精简”,不带图标;全屏入口仍可见。底部保留连接计数、网络详情入口和分页,不显示服务器状态栏。点击图片仍可展开查看和接管操控,返回后保持精简模式;模式切换会释放操控、退出批量选择并返回网格,不改变连接、分组和设备顺序。
|
||||||
- 窗口最底部常驻与连接页一致的服务器状态栏,普通、精简、展开、批量选择和全屏视图均显示连接指示灯、服务器连接状态及断开时的重连入口。桌面墙操作栏位于其上方,显示已连接设备数量和“网络状态”;点击可打开当前已建立连接的设备列表。网络列表不受桌面墙分组和分页限制;离线、未连接和连接中的设备不列入,断开后自动移除。列表显示设备名称、ID、直连/中继、实际协商的 SRTP 加密状态、往返延迟(RTT)、控制端接收与发送速率、视频丢包率、实际接收分辨率与帧率,打开期间每秒刷新。加密状态紧随连接方式显示为“已加密/未加密”,尚无统计或报告超过 3 秒未更新时显示“待确认”。分辨率与帧率在解码交付时统计,墙内预览和后台会话均可用;尚未收到画面时显示“—”,画面停止更新超过 1 秒时帧率显示 0 FPS。网络数值尚无统计或报告超过 3 秒未更新时显示“—”。可通过关闭按钮、Esc 或点击列表外关闭;打开列表会释放远程操控。
|
- 主窗口菜单中的全局“设置”顶部并排提供“常规”和“桌面墙”两个选项卡;“桌面墙”可统一配置桌面墙设备的传输分辨率上限和帧率,桌面墙处于普通、精简、展开或全屏视图时,也可通过主窗口菜单打开设置。切换选项卡保留未保存修改,底部统一保存或取消。分辨率支持自动、320 × 180、640 × 360、1280 × 720、1920 × 1080、2560 × 1440、3840 × 2160和原始分辨率;固定上限适用于缩略图和展开画面,保持比例且不放大源画面。帧率支持跟随设备设置、15、30、60 FPS。保存后立即更新当前桌面墙会话,也用于后续桌面墙连接,不影响普通连接,跨分组和分页生效;取消不应用修改,恢复默认需保存后生效。配置保存在本地,重启后保留;保存失败时保留面板和输入。打开设置会释放远程操控。
|
||||||
|
- 服务器连接状态、指示灯和重连入口仅在主窗口显示,桌面墙的普通、精简、展开、批量选择和全屏视图均不显示服务器状态栏。桌面墙底部操作栏左侧以普通文字显示已连接设备数量,旁边是紧凑的“网络状态”按钮;点击按钮可打开当前已建立连接的设备列表,空白区域不响应点击,也不显示整条悬停提示。弹窗采用紧凑宽度,高度随设备数量调整,最多同时显示 6 行,更多设备通过滚动查看。网络列表不受桌面墙分组和分页限制;离线、未连接和连接中的设备不列入,断开后自动移除。列表显示设备名称、ID、直连/中继、实际协商的 SRTP 加密状态、往返延迟(RTT)、控制端接收与发送速率、视频丢包率、实际接收分辨率与帧率,打开期间每秒刷新。加密状态紧随连接方式显示为“已加密/未加密”,尚无统计或报告超过 3 秒未更新时显示“待确认”。分辨率与帧率在解码交付时统计,墙内预览和后台会话均可用;尚未收到画面时显示“—”,画面停止更新超过 1 秒时帧率显示 0 FPS。网络数值尚无统计或报告超过 3 秒未更新时显示“—”。可通过关闭按钮、Esc 或点击列表外关闭;打开列表会释放远程操控。
|
||||||
- 点卡片进入只读放大视图,点“接管操控”才开始发送键鼠。点“释放操控”或按 Ctrl/⌘ + Shift + Esc 可以释放。切换设备、翻页、打开本地操作面板、切换模式及窗口失焦也会释放。
|
- 点卡片进入只读放大视图,点“接管操控”才开始发送键鼠。点“释放操控”或按 Ctrl/⌘ + Shift + Esc 可以释放。切换设备、翻页、打开本地操作面板、切换模式及窗口失焦也会释放。
|
||||||
- 桌面墙连接默认静音,包含新建连接、重连、复用已有连接、添加或导入已有会话,以及切入桌面墙时已有的连接。展开、全屏或接管操控不会自动开启声音;需要声音时,可通过批量“取消静音”或在独立串流窗口主动开启。两个入口共用同一静音状态,主动开启的声音在桌面墙可见时播放,翻页、展开及释放操控保留声音设置;每台设备使用独立音频流同时播放,静音一台不会清空其他设备的声音。新版本对端按会话关闭音频订阅;旧版本发送的音频由控制端丢弃,不通过停止对端的全局声音采集影响其他控制端。
|
- 桌面墙连接默认静音,包含新建连接、重连、复用已有连接、添加或导入已有会话,以及切入桌面墙时已有的连接。展开、全屏或接管操控不会自动开启声音;需要声音时,可通过批量“取消静音”或在独立串流窗口主动开启。两个入口共用同一静音状态,主动开启的声音在桌面墙可见时播放,翻页、展开及释放操控保留声音设置;每台设备使用独立音频流同时播放,静音一台不会清空其他设备的声音。新版本对端按会话关闭音频订阅;旧版本发送的音频由控制端丢弃,不通过停止对端的全局声音采集影响其他控制端。
|
||||||
- 放大视图使用一行设备操作栏,只保留返回、设备名称、状态和接管/释放操控按钮;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看完整名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通模式缩略图栏高 90px,在保留画面大小的同时将空间留给主预览。普通、精简、宽窗口及全屏均保持上下布局,调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。
|
- 放大视图使用一行设备操作栏,只保留返回、设备名称、状态和接管/释放操控按钮;选中的桌面在下方放大显示,同页其他桌面的缩略图统一在上方横向排列,超出宽度时可横向滚动。点击上方缩略图可切换下方桌面,当前桌面不在缩略图中重复显示;同页只有一台设备时隐藏缩略图栏。普通模式缩略图下方以一行显示设备名,长名称省略,不显示状态圆点;不在线设备的缩略图和下方放大预览统一使用灰色,精简模式同样生效;悬停可查看设备名称、ID 和在线状态,精简模式也支持悬停查看。缺少画面或画面停滞时保留状态占位;滚动条单独留出空间。普通和精简模式的缩略图预览区域均固定为 16:9,普通模式的设备名称单独显示在画面下方。普通、精简、宽窗口及全屏均保持上下布局,调整尺寸后仍保留当前设备所在页。主预览和缩略图均完整等比显示,鼠标坐标按实际预览区域映射。
|
||||||
- 在“近期连接”中点击已连接设备的卡片画面,可打开原有独立串流窗口,使用声音、显示器切换、隐私屏、文件传输等完整单会话工具。关闭墙内设备的独立窗口不会断开墙中的会话。
|
- 在“近期连接”中点击已连接设备的卡片画面,可打开原有独立串流窗口,使用声音、显示器切换、隐私屏、文件传输等完整单会话工具。关闭墙内设备的独立窗口不会断开墙中的会话。
|
||||||
- 点击“选择”进入批量模式,“完成”退出;批量选择复用固定底栏,设备卡片的位置和尺寸不变。底栏中的已选数量和“全选/取消全选”使用固定宽度,切换选中数量或按钮文字时,后续按钮和分页保持原位。点击“全选”会选中当前筛选下的所有设备,跨页生效;全部选中后显示“取消全选”。翻页保留选择,切换分组清空选择;已选数量与跨页数量、连接、断开、“更多”及分页控件在同一底栏显示;静音和文件分发收进“更多”菜单。声音菜单项根据已选且已连接设备的状态切换:全部静音时显示“取消静音”,有设备开启声音时显示“静音”,无已连接设备时禁用。点击后统一设置所有已选的已连接设备,包含跨页选择,跳过未连接设备,保留当前选择。断开和文件分发会先列出完整目标;提交后的操作目标不会跟随之后的选择变化。
|
- 点击“选择”进入批量模式,“完成”退出;批量选择复用固定底栏,设备卡片的位置和尺寸不变。底栏中的已选数量和“全选/取消全选”使用固定宽度,切换选中数量或按钮文字时,后续按钮和分页保持原位。点击“全选”会选中当前筛选下的所有设备,跨页生效;全部选中后显示“取消全选”。翻页保留选择,切换分组清空选择;已选数量与跨页数量、连接、断开、“更多”及分页控件在同一底栏显示;静音和文件分发收进“更多”菜单。声音菜单项根据已选且已连接设备的状态切换:全部静音时显示“取消静音”,有设备开启声音时显示“静音”,无已连接设备时禁用。点击后统一设置所有已选的已连接设备,包含跨页选择,跳过未连接设备,保留当前选择。断开和文件分发会先列出完整目标;提交后的操作目标不会跟随之后的选择变化。
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@
|
|||||||
|
|
||||||
15 帧订阅使用编码前发送节流和稳定的逐显示流时间预算,并采用保持分辨率策略,避免低帧率触发交互视频的画质降级。30/60 帧选项使用编码器自身的交互帧率控制。仅向声明 `supports_deskhub_fps15` 的对端发送 `fps=15`;缺少该能力的 v1/v2 对端使用原有 `fps=60, frame_rate=30` 格式回退到 30 FPS,未声明预览能力的更旧版本保持原行为。协议仍接受旧控制端的 1/8 帧请求,但设置界面不再提供这两个档位。
|
15 帧订阅使用编码前发送节流和稳定的逐显示流时间预算,并采用保持分辨率策略,避免低帧率触发交互视频的画质降级。30/60 帧选项使用编码器自身的交互帧率控制。仅向声明 `supports_deskhub_fps15` 的对端发送 `fps=15`;缺少该能力的 v1/v2 对端使用原有 `fps=60, frame_rate=30` 格式回退到 30 FPS,未声明预览能力的更旧版本保持原行为。协议仍接受旧控制端的 1/8 帧请求,但设置界面不再提供这两个档位。
|
||||||
|
|
||||||
预览在解码回调中随收到的画面生成有界缩略图,可见桌面墙随 UI 刷新显示最新帧。UI 不处理全分辨率转换,只保留当前可见设备的最新预览,超过 5 秒未更新会标记停帧。画面保持比例,以完整矩形显示;外层圆角卡片与画面之间保留内边距,不裁切桌面内容或画面边角。实际显示帧率受设备性能与网络状况影响。
|
预览在解码回调中随收到的画面生成有界缩略图,可见桌面墙随 UI 刷新显示最新帧。UI 不处理全分辨率转换,只保留当前可见设备的最新预览,超过 5 秒未更新会标记停帧。画面保持比例,以完整矩形显示;表格单元格与画面之间保留内边距,不裁切桌面内容或画面边角。实际显示帧率受设备性能与网络状况影响。
|
||||||
|
|
||||||
连接旧版被控端时,若仅声明 `supports_deskhub_preview`,控制端发送原有 v1 订阅;若未声明预览能力,则不发送该订阅,沿用其视频策略。旧控制端连接新版被控端时继续使用原分辨率;v1 订阅也会清除此前的尺寸限制。只有同时声明 `supports_deskhub_preview` 与 `supports_deskhub_resolution` 的被控端才会收到 v2 尺寸请求,旧版本不承诺发送端节省资源。
|
连接旧版被控端时,若仅声明 `supports_deskhub_preview`,控制端发送原有 v1 订阅;若未声明预览能力,则不发送该订阅,沿用其视频策略。旧控制端连接新版被控端时继续使用原分辨率;v1 订阅也会清除此前的尺寸限制。只有同时声明 `supports_deskhub_preview` 与 `supports_deskhub_resolution` 的被控端才会收到 v2 尺寸请求,旧版本不承诺发送端节省资源。
|
||||||
|
|
||||||
@@ -66,9 +69,9 @@ MiniRTC 新增 `UpdateVideoResolution(peer, remote_id, remote_id_size, width, he
|
|||||||
|
|
||||||
连接和文件分发最多并行启动 4 个操作。尚未开始连接的设备卡片显示“排队中”,轮到后自动切换为实际连接状态。断开立即执行,同时取消该设备排队中的操作及重连请求,不等待文件传输完成。单台等待密码不会阻塞其他设备,可以从对应设备的放大视图填写密码。连接状态显示在设备卡片上,文件传输详情可在该设备的独立串流窗口中查看。
|
连接和文件分发最多并行启动 4 个操作。尚未开始连接的设备卡片显示“排队中”,轮到后自动切换为实际连接状态。断开立即执行,同时取消该设备排队中的操作及重连请求,不等待文件传输完成。单台等待密码不会阻塞其他设备,可以从对应设备的放大视图填写密码。连接状态显示在设备卡片上,文件传输详情可在该设备的独立串流窗口中查看。
|
||||||
|
|
||||||
批量操作仅在本次运行中执行,不提供任务列表、操作历史或重启后的恢复。成员资料与窗口状态保存在应用缓存目录的 `deskhub.json` 和 `deskhub.json.window` 中,不含设备密码和文件内容。
|
批量操作仅在本次运行中执行,不提供任务列表、操作历史或重启后的恢复。成员资料与窗口状态(含共享格子宽度)保存在应用缓存目录的 `deskhub.json` 和 `deskhub.json.window` 中,不含设备密码和文件内容。
|
||||||
|
|
||||||
`deskhub.json` 第 2 版分别保存分组记录和桌面墙设备 ID,并通过可选的 `stream_settings` 保存分辨率预设及帧率。缺少或无效的设置使用自动分辨率和跟随设备帧率,不影响设备成员。已保存的 1/8 FPS 自动迁移为 15 FPS,保留原分辨率预设;原有 30/60 FPS 设置保持不变。第 1 版配置仍可读取,原有桌面墙成员、分组和顺序会保留;旧版数据未记录加入来源,因此不会自动移除已有成员。
|
`deskhub.json` 第 2 版分别保存分组记录和桌面墙设备 ID,通过可选的 `device_order` 保存手动排列顺序,并通过可选的 `stream_settings` 保存分辨率预设及帧率。没有手动顺序的旧配置继续使用原有排列。缺少或无效的设置使用自动分辨率和跟随设备帧率,不影响设备成员。已保存的 1/8 FPS 自动迁移为 15 FPS,保留原分辨率预设;原有 30/60 FPS 设置保持不变。第 1 版配置仍可读取,原有桌面墙成员、分组和顺序会保留;旧版数据未记录加入来源,因此不会自动移除已有成员。
|
||||||
|
|
||||||
批量远程命令、重启尚未提供。实际大规模并发容量需要用真实设备测量。
|
批量远程命令、重启尚未提供。实际大规模并发容量需要用真实设备测量。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user