[feat] add system language support across desktop and mobile

This commit is contained in:
dijunkun
2026-10-27 12:35:47 +08:00
parent b4acf0b597
commit b07f457e76
105 changed files with 21711 additions and 1627 deletions
+1 -1
View File
@@ -427,7 +427,7 @@ int main(int argc, char* argv[]) {
CSimpleIniA config;
config.SetUnicode(true);
config.LoadFile((paths.GetCachePath() / "config.ini").string().c_str());
console.SetLanguage(static_cast<int>(config.GetLongValue("Settings", "language", 0)));
console.SetLanguage(static_cast<int>(config.GetLongValue("Settings", "language", 10)));
std::cout << console.Text("console_cli_help");
return 0;
}
@@ -62,7 +62,7 @@ int ConfigCenter::Load() {
const long language_value =
ini_->GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_value > static_cast<long>(LANGUAGE::SYSTEM)) {
language_ = LANGUAGE::ENGLISH;
} else {
language_ = static_cast<LANGUAGE>(language_value);
@@ -261,7 +261,7 @@ int ConfigCenter::Save() {
int ConfigCenter::SetLanguage(LANGUAGE language) {
const int value = static_cast<int>(language);
if (value < 0 || value > 2 || StoreValues({{"language", std::to_string(value)}}) != 0) return -1;
if (value < 0 || value > static_cast<int>(LANGUAGE::SYSTEM) || StoreValues({{"language", std::to_string(value)}}) != 0) return -1;
language_ = language;
return 0;
}
@@ -20,7 +20,11 @@ namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class LANGUAGE {
CHINESE = 0, ENGLISH = 1, RUSSIAN = 2, TRADITIONAL_CHINESE = 3,
JAPANESE = 4, KOREAN = 5, GERMAN = 6, FRENCH = 7, SPANISH = 8,
BRAZILIAN_PORTUGUESE = 9, SYSTEM = 10
};
enum class CONNECTION_MODE { DEVICE_ID = 0, DIRECT_IP = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
enum class TURN_MODE {
@@ -93,7 +97,7 @@ class ConfigCenter {
std::unique_ptr<IniStorage> ini_;
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
LANGUAGE language_ = LANGUAGE::SYSTEM;
CONNECTION_MODE connection_mode_ = CONNECTION_MODE::DEVICE_ID;
bool enable_ip_direct_ = true;
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
@@ -1201,9 +1201,8 @@ void GuiApplication::InitializeSettings() {
settings_.LoadRecentConnectionAliases();
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
language_button_value_ = localization_language_index_;
localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
privacy_.SetText({
localization::privacy_screen_unlock_hint[localization_language_index_]});
}
@@ -1563,11 +1562,7 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
if (config_center_->SetConnectionMode(mode) != 0) {
LOG_WARN("Could not save connection mode preference");
main->set_offline_warning(UiText(
localization_language_index_ == 0
? "无法保存连接模式,下次启动可能不会恢复当前模式。"
: localization_language_index_ == 2
? "Не удалось сохранить режим подключения. При следующем запуске он может не восстановиться."
: "Could not save the connection mode. It may not be restored on the next launch."));
localization::Literal("无法保存连接模式,下次启动可能不会恢复当前模式。", localization_language_index_)));
}
});
main->on_toggle_gallery_fullscreen(
@@ -3078,13 +3073,9 @@ void GuiApplication::UpdateLocalization() {
return;
}
#if _WIN32
// All views refer to static literals; selecting the text needs no allocation
// and only runs when the language changes. Tick still checks connection state.
ui_->shutdown_reason = language == 0
? L"远程控制连接中,保留连接以确认或取消关机。"
: language == 2
? L"Удалённый сеанс активен. Соединение сохранено для подтверждения или отмены выключения."
: L"Remote session active. Keeping the connection available to confirm or cancel shutdown.";
static std::vector<std::wstring> shutdown_reasons(localization::GetSupportedLanguages().size());
ui_->shutdown_reason = localization::GetWindowsLocalizedLabel(
language, "shutdown_reason", L"Remote session active.", shutdown_reasons);
#endif
ui_localization::ApplyMainWindowStrings(ui_->main, language);
ui_localization::ApplyStreamWindowStrings(ui_->main, language);
@@ -3247,13 +3238,12 @@ void GuiApplication::SyncMainWindow() {
// Keep this filter separate from DeskHub's current group, keyed by name so
// adding/removing groups does not silently select a different one by index.
const auto tr_group = [this](const char* zh, const char* en, const char* ru) {
return UiText(localization_language_index_ == 0 ? zh
: localization_language_index_ == 2 ? ru : en);
const auto tr_group = [this](const char* zh) {
return UiText(localization::Literal(zh, localization_language_index_).c_str());
};
std::vector<slint::SharedString> recent_groups{
tr_group("全部设备", "All devices", "Все устройства"),
tr_group("未分组", "Ungrouped", "Без группы")};
tr_group("全部设备"),
tr_group("未分组")};
const bool lan_mode = ui_->main->get_lan_mode();
int recent_group_index = 0;
if (!lan_mode && ui_->recent_group_filter) {
@@ -4565,9 +4555,7 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
// common save succeeds. Restore the previous wall file if it fails.
if (!wall_settings || (wall_changed && !updated_wall.Save(ui_->gallery_path))) {
main->set_gallery_settings_error(UiText(
localization_language_index_ == 0 ? "保存失败,请重试"
: localization_language_index_ == 2 ? "Не удалось сохранить. Повторите попытку"
: "Could not save; try again"));
localization::Literal("保存失败,请重试", localization_language_index_)));
main->set_settings_tab(1);
return false;
}
@@ -4586,9 +4574,7 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
if (wall_changed && !ui_->gallery.Save(ui_->gallery_path))
LOG_ERROR("Failed to restore DeskHub settings after IP direct save failure");
main->set_settings_save_error(UiText(
localization_language_index_ == 0 ? "无法保存 IP 连接设置,请重试。"
: localization_language_index_ == 2 ? "Не удалось сохранить настройки подключения по IP. Повторите попытку."
: "Could not save IP connection settings. Try again."));
localization::Literal("无法保存 IP 连接设置,请重试。", localization_language_index_)));
return false;
}
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
@@ -4596,22 +4582,18 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
if (wall_changed && !ui_->gallery.Save(ui_->gallery_path))
LOG_ERROR("Failed to restore DeskHub settings after lock preference save failure");
main->set_settings_save_error(UiText(
localization_language_index_ == 0 ? "无法保存断开后锁屏设置,请重试。"
: localization_language_index_ == 2 ? "Не удалось сохранить настройку блокировки при отключении. Повторите попытку."
: "Could not save lock-on-disconnect settings. Try again."));
localization::Literal("无法保存断开后锁屏设置,请重试。", localization_language_index_)));
return false;
}
#endif
if (config_center_->SetPrivacyLocalUnlock(main->get_privacy_local_unlock_allowed()) != 0) {
if (wall_changed) ui_->gallery.Save(ui_->gallery_path);
main->set_settings_save_error(UiText(
localization_language_index_ == 0 ? "无法保存隐私屏解锁设置,请重试。"
: localization_language_index_ == 2 ? "Не удалось сохранить настройку разблокировки. Повторите попытку."
: "Could not save privacy unlock settings. Try again."));
localization::Literal("无法保存隐私屏解锁设置,请重试。", localization_language_index_)));
return false;
}
language_button_value_ =
localization::detail::ClampLanguageIndex(main->get_language_index());
std::clamp(main->get_language_index(), 0, localization::kSystemLanguage);
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
enable_self_hosted_ = main->get_self_hosted_enabled();
#if !defined(__OHOS__)
@@ -4621,7 +4603,7 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
localization_language_index_ = language_button_value_;
localization_language_index_ = localization::detail::ClampLanguageIndex(language_button_value_);
config_center_->SetLanguage(localization_language_);
privacy_.SetText(
{localization::privacy_screen_unlock_hint[localization_language_index_],
@@ -4773,10 +4755,7 @@ void GuiApplication::ConnectFromUi(const std::string& input) {
const std::string remote_id = CompactPeerId(input);
if (!gallery::Workspace::ValidId(remote_id)) {
ui_->main->set_connection_input_error(
UiText(localization_language_index_ == 0 ? "请输入有效的 9 位设备 ID"
: localization_language_index_ == 2
? "Введите корректный 9-значный ID устройства"
: "Enter a valid 9-digit device ID"));
UiText(localization::Literal("请输入有效的 9 位设备 ID", localization_language_index_)));
return;
}
ui_->main->set_connection_input_error("");
@@ -5330,41 +5309,31 @@ bool GuiApplication::AddGalleryDevice(const std::string& input,
const std::string& password,
const std::string& group,
bool remember_password) {
const auto error = [this](const char* zh, const char* en, const char* ru) {
const auto error = [this](const char* zh) {
ui_->wall->set_gallery_notice(UiText(
localization_language_index_ == 0 ? zh
: localization_language_index_ == 2 ? ru : en));
localization::Literal(zh, localization_language_index_).c_str()));
return false;
};
const auto id = gallery::Workspace::NormalizeId(input);
if (!gallery::Workspace::ValidId(id))
return error("请输入有效的 9 位设备 ID",
"Enter a valid 9-digit device ID",
"Введите корректный 9-значный ID устройства");
return error("请输入有效的 9 位设备 ID");
if (password.empty() || password.size() > 6 ||
password.find_first_not_of(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") !=
std::string::npos)
return error("请输入 1–6 位英文字母或数字密码",
"Use 1–6 letters (A–Z, a–z) or digits (0–9)",
"Пароль: 1–6 латинских букв или цифр");
return error("请输入 1–6 位英文字母或数字密码");
if (!signal_connected_)
return error("尚未连接服务器,请稍后重试",
"Not connected to the server; try again shortly",
"Нет соединения с сервером. Повторите позже");
return error("尚未连接服务器,请稍后重试");
using State = SlintUi::BatchOperation::State;
for (const auto& operation : ui_->batch_operations)
if (operation.id == id && operation.action == "connect" &&
(operation.state == State::Queued || operation.state == State::Running))
return error("该设备正在连接,请稍候",
"This device is already connecting",
"Устройство уже подключается");
return error("该设备正在连接,请稍候");
auto updated = ui_->gallery;
updated.Add(id, Trim(group));
if (!updated.Save(ui_->gallery_path))
return error("保存失败,请重试", "Could not save; try again",
"Не удалось сохранить. Повторите попытку");
return error("保存失败,请重试");
ui_->gallery = std::move(updated);
ui_->gallery_hidden_active_ids.erase(id);
if (auto props = FindRemoteSession(id)) {
@@ -5406,9 +5375,7 @@ bool GuiApplication::ImportGalleryRecent(const std::string& input_group) {
updated.AddMany(ids, group);
if (!updated.Save(ui_->gallery_path)) {
ui_->wall->set_gallery_notice(UiText(
localization_language_index_ == 0 ? "保存失败,请重试"
: localization_language_index_ == 2 ? "Не удалось сохранить. Повторите попытку"
: "Could not save; try again"));
localization::Literal("保存失败,请重试", localization_language_index_)));
return false;
}
ui_->gallery = std::move(updated);
@@ -5488,9 +5455,7 @@ bool GuiApplication::SaveDeviceGroups(const std::string& new_group,
auto updated = ui_->gallery;
if (!updated.SetGroups(id, names) || !updated.Save(ui_->gallery_path)) {
main->set_device_group_error(UiText(
localization_language_index_ == 0 ? "保存失败,请重试"
: localization_language_index_ == 2 ? "Не удалось сохранить. Повторите попытку"
: "Could not save; try again"));
localization::Literal("保存失败,请重试", localization_language_index_)));
return false;
}
// Removing an empty group shifts indices. Retain the filter by name instead
@@ -5528,10 +5493,8 @@ void GuiApplication::GalleryAction(const std::string& action,
ui_->gallery_network_synced = {};
return;
}
const auto tr = [this](const char* zh, const char* en, const char* ru) {
return UiText(localization_language_index_ == 0 ? zh
: localization_language_index_ == 2 ? ru
: en);
const auto tr = [this](const char* zh) {
return UiText(localization::Literal(zh, localization_language_index_).c_str());
};
if (action == "recent-open") {
ReleaseControl();
@@ -5749,7 +5712,7 @@ void GuiApplication::GalleryAction(const std::string& action,
SyncGallery();
};
if (action == "file") {
OpenFileDialog(std::string(tr("选择分发文件", "Select a file to send", "Выберите файл для отправки")),
OpenFileDialog(std::string(tr("选择分发文件")),
[enqueue](std::string file) {
if (!file.empty()) enqueue(file);
});
@@ -6026,11 +5989,8 @@ void GuiApplication::SyncGallery() {
ui_->wall->set_gallery_recent_available(available);
ui_->wall->set_gallery_recent_selected(selected);
}
const auto tr = [this](const char* zh, const char* en,
const char* ru) -> std::string {
return localization_language_index_ == 0 ? zh
: localization_language_index_ == 2 ? ru
: en;
const auto tr = [this](const char* zh) -> std::string {
return localization::Literal(zh, localization_language_index_).c_str();
};
const auto name = [this](const std::string& id) {
for (const auto& [_, recent] : recent_connections_)
@@ -6070,10 +6030,10 @@ void GuiApplication::SyncGallery() {
device.id = UiText(FormatPeerId(id.c_str()));
const auto mode = props->traversal_mode_.load();
device.mode = UiText(mode == TraversalMode::P2P
? tr("直连", "Direct", "Прямой")
? tr("直连")
: mode == TraversalMode::UnknownMode
? tr("等待统计", "Awaiting stats", "Ожидание статистики")
: tr("中继", "Relay", "Ретрансляция"));
? tr("等待统计")
: tr("中继"));
device.inbound = device.outbound = device.loss = "—";
device.video_latency = "—";
// Prefer the wall's own frame path. An independently displayed stream
@@ -6241,7 +6201,7 @@ void GuiApplication::SyncGallery() {
label += member.group;
}
std::vector<slint::SharedString> groups{
UiText(tr("全部设备", "All devices", "Все устройства"))};
UiText(tr("全部设备"))};
for (const auto& group : wall.groups) groups.push_back(UiText(group));
const auto old_groups = ui_->wall->get_gallery_groups();
bool changed_groups = !old_groups || old_groups->row_count() != groups.size();
@@ -6325,8 +6285,8 @@ void GuiApplication::SyncGallery() {
tile.online = tile.connected || online.value_or(false);
tile.presence_known = tile.connected || online.has_value();
tile.status = UiText(
tile.connected ? tr("等待画面", "Waiting for video", "Ожидание видео")
: tr("未连接", "Not connected", "Нет соединения"));
tile.connected ? tr("等待画面")
: tr("未连接"));
const bool queued = !tile.connected && std::any_of(
ui_->batch_operations.begin(), ui_->batch_operations.end(),
[&](const auto& operation) {
@@ -6335,9 +6295,9 @@ void GuiApplication::SyncGallery() {
});
// A queued retry takes precedence over errors from the previous session.
if (queued)
tile.status = UiText(tr("队列中", "Queued", "В очереди"));
tile.status = UiText(tr("队列中"));
else if (props && props->connection_auth_.Get().password_required)
tile.status = UiText(tr("需要密码", "Password required", "Нужен пароль"));
tile.status = UiText(tr("需要密码"));
else if (props && !tile.connected)
tile.status = UiText(ConnectionStatusText(
props->connection_status_.load(), localization_language_index_));
@@ -6365,8 +6325,8 @@ void GuiApplication::SyncGallery() {
tile.has_frame = true;
tile.stale = now - props->gallery_frame_at_ > 5s;
tile.status =
UiText(tile.stale ? tr("画面已停留", "Frame stale", "Кадр устарел")
: tr("只读预览", "View only", "Просмотр"));
UiText(tile.stale ? tr("画面已停留")
: tr("只读预览"));
}
}
return tile;
@@ -1,116 +1,91 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
#include "localization_data.h"
#include "system_language.h"
#if _WIN32
#include <Windows.h>
#endif
namespace crossdesk {
namespace localization {
namespace crossdesk {
namespace localization {
struct LanguageOption {
std::string code;
std::string display_name;
};
class LocalizedString {
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
{"en-US", "English"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
return kSupportedLanguages;
}
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", "简体中文"}, {"en-US", "English"}, {"ru-RU", "Русский"},
{"zh-Hant", "繁體中文"}, {"ja", "日本語"}, {"ko", "한국어"},
{"de", "Deutsch"}, {"fr", "Français"}, {"es", "Español"},
{"pt-BR", "Português (Brasil)"}};
return kSupportedLanguages;
}
namespace detail {
inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 &&
language_index < static_cast<int>(GetSupportedLanguages().size())) {
return language_index;
}
return 0;
if (language_index == kSystemLanguage) return SystemLanguage();
return language_index >= 0 && language_index < kSystemLanguage
? language_index : kEnglishLanguage;
}
using TranslationTable =
std::unordered_map<std::string,
std::unordered_map<std::string, std::string>>;
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
const TranslationRow& row) {
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
{"en-US", reinterpret_cast<const char*>(row.en)},
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
}
inline TranslationTable BuildTranslationTable() {
TranslationTable table;
for (const auto& row : kTranslationRows) {
table[row.key] = MakeLocalizedValues(row);
}
using TranslationTable = std::unordered_map<std::string, std::vector<std::string>>;
inline const TranslationTable& LiteralTable() {
static const TranslationTable table = [] {
TranslationTable result;
for (const auto& row : kTranslationRows)
result.emplace(row.key, std::vector<std::string>(std::begin(row.text), std::end(row.text)));
return result;
}();
return table;
}
inline const TranslationTable& GetTranslationTable() {
static const TranslationTable table = BuildTranslationTable();
return table;
}
inline const std::string& GetTranslatedText(const std::string& key,
int language_index) {
static const std::string kEmptyText = "";
const auto& table = GetTranslationTable();
const auto key_it = table.find(key);
if (key_it == table.end()) {
return kEmptyText;
}
const auto& localized_values = key_it->second;
const std::string& language_code =
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
const auto exact_it = localized_values.find(language_code);
if (exact_it != localized_values.end()) {
return exact_it->second;
}
const auto english_it = localized_values.find("en-US");
if (english_it != localized_values.end()) {
return english_it->second;
}
const auto chinese_it = localized_values.find("zh-CN");
if (chinese_it != localized_values.end()) {
return chinese_it->second;
}
return kEmptyText;
}
} // namespace detail
inline const std::string& GetLiteralText(const std::string& key, int language) {
static const std::string empty;
const auto it = LiteralTable().find(key);
if (it == LiteralTable().end()) return empty;
const auto& values = it->second;
return values[ClampLanguageIndex(language)];
}
inline const std::string& GetTranslatedText(const std::string& key, int language) {
static const std::unordered_map<std::string, std::string> names = [] {
std::unordered_map<std::string, std::string> result;
for (const auto& row : kNamedTranslations) result.emplace(row.name, row.literal);
return result;
}();
static const std::string empty;
const auto it = names.find(key);
return it == names.end() ? empty : GetLiteralText(it->second, language);
}
} // namespace detail
inline const std::string& Literal(const std::string& key, int language) {
return detail::GetLiteralText(key, language);
}
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
@@ -130,11 +105,11 @@ inline std::string FormatDisplayLabel(size_t display_index,
return label;
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetWindowsLocalizedLabel(
int language_index, const char* key, const wchar_t* fallback,
@@ -187,8 +162,8 @@ inline const wchar_t* GetExitProgramLabel(int language_index) {
cache);
}
#endif
} // namespace localization
} // namespace crossdesk
#endif
} // namespace localization
} // namespace crossdesk
#endif
File diff suppressed because one or more lines are too long
@@ -0,0 +1,111 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SYSTEM_LANGUAGE_H_
#define _SYSTEM_LANGUAGE_H_
#include <algorithm>
#include <cstdlib>
#include <string>
#include <vector>
#if defined(_WIN32)
#include <Windows.h>
#elif defined(__APPLE__)
#include <CoreFoundation/CoreFoundation.h>
#endif
namespace crossdesk {
namespace localization {
inline constexpr int kSystemLanguage = 10;
inline constexpr int kEnglishLanguage = 1;
// Keep 0/1/2 compatible with existing Chinese/English/Russian config files.
inline int MatchLanguage(std::string tag) {
for (char& c : tag) {
if (c == '_') c = '-';
else if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
}
tag = tag.substr(0, tag.find_first_of(".@"));
const auto base = tag.substr(0, tag.find('-'));
if (base == "zh") {
const auto parts = "-" + tag + "-";
if (parts.find("-hans-") != std::string::npos) return 0;
return parts.find("-hant-") != std::string::npos ||
parts.find("-tw-") != std::string::npos ||
parts.find("-hk-") != std::string::npos ||
parts.find("-mo-") != std::string::npos ? 3 : 0;
}
const char* codes[] = {"zh", "en", "ru", "zh", "ja", "ko", "de", "fr", "es", "pt"};
for (int i = 1; i < kSystemLanguage; ++i)
if (i != 3 && base == codes[i]) return i;
if (base == "c" || base == "posix") return kEnglishLanguage;
return -1;
}
inline int MatchPreferredLanguages(const std::vector<std::string>& tags) {
for (const auto& tag : tags) {
const int match = MatchLanguage(tag);
if (match >= 0) return match;
}
return kEnglishLanguage;
}
inline int DetectSystemLanguage() {
std::vector<std::string> tags;
#if defined(_WIN32)
ULONG count = 0, length = 0;
GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, nullptr, &length);
std::vector<wchar_t> names(length);
if (length && GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, names.data(), &length)) {
for (const wchar_t* name = names.data(); *name; name += wcslen(name) + 1) {
std::string tag;
for (const wchar_t* c = name; *c; ++c) tag += static_cast<char>(*c);
tags.push_back(tag);
}
}
#elif defined(__APPLE__)
if (CFArrayRef names = CFLocaleCopyPreferredLanguages()) {
for (CFIndex i = 0; i < CFArrayGetCount(names); ++i) {
char tag[128] = {};
auto name = static_cast<CFStringRef>(CFArrayGetValueAtIndex(names, i));
if (CFStringGetCString(name, tag, sizeof(tag), kCFStringEncodingUTF8)) tags.emplace_back(tag);
}
CFRelease(names);
}
#else
// The Harmony desktop host supplies LANGUAGE from the system i18n API.
const char* locale = std::getenv("LC_ALL");
if (!locale || !*locale) locale = std::getenv("LC_MESSAGES");
if (!locale || !*locale) locale = std::getenv("LANG");
if (locale && (std::string(locale) == "C" || std::string(locale) == "POSIX")) return kEnglishLanguage;
if (const char* preferred = std::getenv("LANGUAGE")) {
std::string list(preferred);
size_t start = 0;
do {
const auto end = list.find(':', start);
tags.push_back(list.substr(start, end - start));
if (end == std::string::npos) break;
start = end + 1;
} while (start < list.size());
}
if (locale) tags.emplace_back(locale);
#endif
return MatchPreferredLanguages(tags);
}
inline int SystemLanguage() {
// Language changes take effect on the next application launch; explicit
// application preferences always win over this process-wide default.
static const int language = DetectSystemLanguage();
return language;
}
} // namespace localization
} // namespace crossdesk
#endif
@@ -461,8 +461,7 @@ int SettingsManager::Load() {
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
owner_.aes128_key_, owner_.aes128_iv_);
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
static_cast<int>(owner_.config_center_->GetLanguage()));
owner_.language_button_value_ = static_cast<int>(owner_.config_center_->GetLanguage());
owner_.video_encode_format_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
+15 -15
View File
@@ -19,12 +19,12 @@ export struct AboutComponent {
}
export global AboutStrings {
out property <string> software-license: GalleryStrings.tr("软件许可", "Software license", "Лицензия программы");
out property <string> components: GalleryStrings.tr("开源组件", "Open-source components", "Компоненты с открытым кодом");
out property <string> source: GalleryStrings.tr("源码与构建说明", "Source code and build instructions", "Исходный код и сборка");
out property <string> notices: GalleryStrings.tr("许可与版权声明", "Licenses and copyright notices", "Лицензии и авторские права");
out property <string> view-source: GalleryStrings.tr("查看源码", "View source code", "Открыть исходный код");
out property <string> back: GalleryStrings.tr("返回", "Back", "Назад");
out property <string> software-license: GalleryStrings.texts[130];
out property <string> components: GalleryStrings.texts[131];
out property <string> source: GalleryStrings.texts[132];
out property <string> notices: GalleryStrings.texts[133];
out property <string> view-source: GalleryStrings.texts[134];
out property <string> back: GalleryStrings.texts[135];
}
export component AboutNavigationGroup inherits FlatSurface {
@@ -137,7 +137,7 @@ export component AboutPanel inherits SettingsDialog {
if root.page == AboutPage.components: VerticalLayout {
padding: 0px; spacing: 10px;
Text {
text: GalleryStrings.tr("以下列出本版本的主要开源组件。各组件遵循各自的开源许可。", "Main open-source components used in this build. Each component is covered by its own license.", "Основные открытые компоненты этой сборки. Каждый распространяется по своей лицензии.");
text: GalleryStrings.texts[136];
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
}
Rectangle {
@@ -146,9 +146,9 @@ export component AboutPanel inherits SettingsDialog {
notice := VerticalLayout {
x: 16px; y: 16px; width: parent.width - 32px;
height: self.preferred-height; spacing: 12px;
Text { text: GalleryStrings.tr("OpenFEC 告知", "OpenFEC notice", "Уведомление OpenFEC"); color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; }
Text { text: GalleryStrings.texts[137]; color: WorkbenchStyle.text; font-size: 14px; font-weight: 600; }
Text {
text: GalleryStrings.tr("本应用使用 OpenFEC 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。该许可规定了有限保证及责任限制,详情见许可全文。", "This application uses OpenFEC for forward error correction under CeCILL-C 1.0, which provides limited warranties and limits liability. See the full license for details.", "Приложение использует OpenFEC для прямого исправления ошибок по лицензии CeCILL-C 1.0 с ограниченной гарантией и ответственностью. Подробности — в тексте лицензии.");
text: GalleryStrings.texts[138];
color: WorkbenchStyle.muted; font-size: 13px; wrap: word-wrap;
font-family: "CrossDesk Message Spacing";
}
@@ -158,7 +158,7 @@ export component AboutPanel inherits SettingsDialog {
font-family: "CrossDesk Message Spacing";
}
if root.openfec-index >= 0: AboutNavigationRow {
title: GalleryStrings.tr("OpenFEC 许可与源码", "OpenFEC license and source", "Лицензия и исходный код OpenFEC");
title: GalleryStrings.texts[139];
clicked => { root.component-index = root.openfec-index; root.page = AboutPage.component; }
}
}
@@ -185,24 +185,24 @@ export component AboutPanel inherits SettingsDialog {
clicked => { root.document-index = index; root.page = AboutPage.document; }
}
if root.selected.documents.length == 0: Text {
text: GalleryStrings.tr("本组件未内置许可文本,请通过“查看源码”查阅许可与版权声明。", "License text is not bundled for this component. Use View source code to read its license and copyright notices.", "Текст лицензии не включён. Откройте исходный код для просмотра лицензии и авторских прав.");
text: GalleryStrings.texts[140];
color: WorkbenchStyle.muted; font-size: 12px; wrap: word-wrap;
}
}
if root.page == AboutPage.source: VerticalLayout {
padding: 0px; spacing: 12px;
Text {
text: root.revision != "" ? GalleryStrings.tr("源码版本:", "Source revision: ", "Ревизия исходного кода: ") + root.revision
: GalleryStrings.tr("本版本源码信息暂不可用。", "Source revision is unavailable for this build.", "Ревизия исходного кода этой сборки недоступна.");
text: root.revision != "" ? GalleryStrings.texts[141] + root.revision
: GalleryStrings.texts[142];
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
AboutNavigationRow { title: AboutStrings.view-source; external: true; clicked => { root.open-link(root.source-url); } }
AboutNavigationRow {
title: GalleryStrings.tr("构建说明", "Build instructions", "Инструкции по сборке"); external: true;
title: GalleryStrings.texts[143]; external: true;
clicked => { root.open-link(root.build-url); }
}
AboutNavigationRow {
title: GalleryStrings.tr("项目主页", "Project website", "Сайт проекта"); external: true;
title: GalleryStrings.texts[144]; external: true;
clicked => { root.open-link("https://crossdesk.cn"); }
}
}
@@ -35,12 +35,12 @@ export component DeviceCardActions inherits Rectangle {
spacing: 0px;
CardAction {
icon: FontAwesomeIcons.pen;
tooltip: GalleryStrings.tr("修改名称", "Rename", "Переименовать");
tooltip: GalleryStrings.texts[35];
clicked => { root.edit-alias(); }
}
CardAction {
icon: FontAwesomeIcons.tags;
tooltip: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы");
tooltip: GalleryStrings.texts[30];
clicked => { root.edit-groups(); }
}
CardAction {
@@ -51,7 +51,7 @@ export component DeviceCardActions inherits Rectangle {
CardAction {
icon: root.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long;
icon-size: 13px;
tooltip: root.connected ? GalleryStrings.tr("断开", "Disconnect", "Отключить") : GalleryStrings.tr("连接", "Connect", "Подключить");
tooltip: root.connected ? GalleryStrings.texts[36] : GalleryStrings.texts[37];
clicked => {
if root.connected { root.disconnect-device(); }
else { root.connect-device(); }
@@ -31,7 +31,7 @@ export component DeviceGroupDialog inherits Rectangle {
TouchArea {}
Text {
x: 18px; y: 16px; height: 24px;
text: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы");
text: GalleryStrings.texts[30];
color: #24272d; font-size: 17px; font-weight: 600;
}
Text {
@@ -64,7 +64,7 @@ export component DeviceGroupDialog inherits Rectangle {
}
Text {
x: 39px; width: parent.width - 48px; height: parent.height;
text: option.name == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : option.name;
text: option.name == "" ? GalleryStrings.texts[31] : option.name;
color: parent.selected ? ControlStyle.selected-text : #30343b;
font-weight: parent.selected ? 600 : 400;
font-size: 12px; overflow: elide; vertical-alignment: center;
@@ -91,7 +91,7 @@ export component DeviceGroupDialog inherits Rectangle {
x: 18px; y: group-list.y + group-list.height + 12px;
width: parent.width - 36px; height: 30px;
text <=> root.new-group;
placeholder-text: GalleryStrings.tr("新建分组(可选)", "New group (optional)", "Новая группа (необязательно)");
placeholder-text: GalleryStrings.texts[32];
}
if root.error != "": Text {
x: 18px; y: group-list.y + group-list.height + 48px;
@@ -102,8 +102,8 @@ export component DeviceGroupDialog inherits Rectangle {
x: 18px; y: parent.height - 46px; width: parent.width - 36px; height: 28px;
spacing: 10px;
Rectangle { horizontal-stretch: 1; }
CompactButton { width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.cancel(); } }
CompactButton { width: 88px; height: 28px; primary: true; text: GalleryStrings.tr("保存", "Save", "Сохранить"); clicked => { root.save(root.new-group); } }
CompactButton { width: 72px; height: 28px; text: GalleryStrings.texts[33]; clicked => { root.cancel(); } }
CompactButton { width: 88px; height: 28px; primary: true; text: GalleryStrings.texts[34]; clicked => { root.save(root.new-group); } }
}
}
}
@@ -1,10 +1,24 @@
export global GalleryStrings {
in-out property <int> language: 0;
in-out property <[string]> texts;
in-out property <string> retry-prefix;
in-out property <string> retry-suffix;
public pure function retry-after(seconds: int) -> string {
return self.retry-prefix + seconds + self.retry-suffix;
}
// Compatibility for embedding callers with their own three-language text.
public pure function tr(zh: string, en: string, ru: string) -> string {
return self.language == 0 ? zh : self.language == 2 ? ru : en;
}
public pure function device-count(count: int) -> string {
if self.language == 0 { return count + " 台设备"; }
if self.language == 3 { return count + " 臺裝置"; }
if self.language == 4 { return count + " 台"; }
if self.language == 5 { return count + "개 기기"; }
if self.language == 6 { return count + (count == 1 ? " Gerät" : " Geräte"); }
if self.language == 7 { return count + (count == 1 ? " appareil" : " appareils"); }
if self.language == 8 { return count + (count == 1 ? " dispositivo" : " dispositivos"); }
if self.language == 9 { return count + (count == 1 ? " dispositivo" : " dispositivos"); }
if self.language == 2 {
let last-two = mod(count, 100);
let last = mod(count, 10);
@@ -65,10 +65,10 @@ component NetworkRow inherits Rectangle {
encryption-text := Text {
x: 6px; width: parent.width - 12px; height: parent.height;
text: !root.device.encryption-known
? GalleryStrings.tr("待确认", "Unknown", "Неизвестно")
? GalleryStrings.texts[38]
: root.device.encryption-enabled
? GalleryStrings.tr("已加密", "Encrypted", "Зашифровано")
: GalleryStrings.tr("未加密", "Unencrypted", "Без шифр.");
? GalleryStrings.texts[39]
: GalleryStrings.texts[40];
color: !root.device.encryption-known ? #7a8490
: root.device.encryption-enabled ? #18794e : #8a6428;
font-size: 10px; horizontal-alignment: center;
@@ -83,7 +83,7 @@ component NetworkRow inherits Rectangle {
Text {
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width;
text: root.heading ? GalleryStrings.tr("画面", "Video", "Видео") : root.device.resolution;
text: root.heading ? GalleryStrings.texts[41] : root.device.resolution;
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide;
@@ -98,7 +98,7 @@ component NetworkRow inherits Rectangle {
width: max(100px, root.width * 0.18);
if root.heading: Text {
x: 0px; width: parent.width;
text: GalleryStrings.tr("收发速率", "Transfer rate", "Скорость");
text: GalleryStrings.texts[42];
font-size: 11px; font-weight: 600; color: #6b7785;
vertical-alignment: center; overflow: elide;
}
@@ -126,14 +126,14 @@ component NetworkRow inherits Rectangle {
Text {
y: root.heading ? 0px : 8px; height: root.heading ? parent.height : 20px;
width: parent.width;
text: root.heading ? GalleryStrings.tr("视频延时", "Video latency", "Задержка видео") : root.device.video-latency;
text: root.heading ? GalleryStrings.texts[43] : root.device.video-latency;
font-size: root.heading ? 11px : 12px; font-weight: root.heading ? 600 : 400;
color: root.heading ? #6b7785 : #293746;
vertical-alignment: center; overflow: elide;
}
if !root.heading: Text {
y: 30px; height: 17px; width: parent.width;
text: GalleryStrings.tr("丢包 ", "Loss ", "Потери ") + root.device.loss;
text: GalleryStrings.texts[44] + root.device.loss;
font-size: 11px; color: #7a8490;
vertical-alignment: center; overflow: elide;
}
@@ -155,7 +155,7 @@ component NetworkRow inherits Rectangle {
width: (parent.width - 12px) / 2;
encoder-label := Text {
height: parent.height; width: self.preferred-width;
text: GalleryStrings.tr("被控端编码器 ", "Remote encoder ", "Удалённый кодер ");
text: GalleryStrings.texts[45];
font-size: 10px; color: #586f87; vertical-alignment: center;
}
ScrollingText {
@@ -167,7 +167,7 @@ component NetworkRow inherits Rectangle {
width: (parent.width - 12px) / 2;
decoder-label := Text {
height: parent.height; width: self.preferred-width;
text: GalleryStrings.tr("控制端解码器 ", "Local decoder ", "Локальный декодер ");
text: GalleryStrings.texts[46];
font-size: 10px; color: #586f87; vertical-alignment: center;
}
ScrollingText {
@@ -182,11 +182,11 @@ component NetworkEmptyState inherits Rectangle {
VerticalLayout {
alignment: center; spacing: 8px;
Text {
text: GalleryStrings.tr("暂无已连接设备", "No connected devices", "Нет подключённых устройств");
text: GalleryStrings.texts[47];
font-size: 13px; color: #596878; horizontal-alignment: center;
}
Text {
text: GalleryStrings.tr("建立连接后,网络和画面信息将在此显示", "Network and video details appear here once connected", "После подключения здесь появятся данные сети и видео");
text: GalleryStrings.texts[48];
font-size: 11px; color: #8a95a1; horizontal-alignment: center;
}
}
@@ -216,25 +216,25 @@ export component GalleryNetworkDialog inherits Rectangle {
}
Text {
x: 20px; y: 17px; width: parent.width - 120px; height: 24px;
text: GalleryStrings.tr("设备网络状态", "Device network status", "Состояние сети устройств");
text: GalleryStrings.texts[49];
font-size: 17px; font-weight: 600; color: #293746;
}
CompactButton {
border-radius: UiRadius.control;
x: parent.width - 80px; y: 17px; width: 60px; height: 26px;
text: GalleryStrings.tr("关闭", "Close", "Закрыть"); clicked => { root.close(); }
text: GalleryStrings.texts[50]; clicked => { root.close(); }
}
Text {
x: 20px; y: 48px; width: parent.width - 40px; height: 18px;
text: GalleryStrings.tr("已连接设备 ", "Connected devices ", "Подключённые устройства ") + root.devices.length
+ GalleryStrings.tr(" 台", "", "");
text: GalleryStrings.texts[51] + root.devices.length
+ GalleryStrings.texts[52];
font-size: 11px; color: #7a8490; overflow: elide;
}
NetworkRow {
x: 12px; y: 78px; width: parent.width - 38px; height: 30px;
heading: true; background: #f3f5f8; border-radius: UiRadius.control;
device: {
name: GalleryStrings.tr("设备 / ID", "Device / ID", "Устройство / ID"),
name: GalleryStrings.texts[53],
};
}
list := ScrollView {
@@ -9,31 +9,31 @@ export component GallerySettings inherits Rectangle {
in property <string> error;
SettingsGroup {
y: 0px; width: parent.width;
title: GalleryStrings.tr("画面传输", "Video transmission", "Передача видео"); rows: 3;
title: GalleryStrings.texts[145]; rows: 3;
SettingsRow {
label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Макс. разрешение");
label: GalleryStrings.texts[146];
label-reserve: self.width - 112px;
SettingsSelect {
border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.resolution-index;
model: [GalleryStrings.tr("自动", "Auto", "Авто"),
model: [GalleryStrings.texts[147],
"320 × 180", "640 × 360", "1280 × 720", "1920 × 1080", "2560 × 1440", "3840 × 2160",
GalleryStrings.tr("原始分辨率", "Original resolution", "Исходное разрешение")];
GalleryStrings.texts[148]];
}
}
SettingsRow {
label: GalleryStrings.tr("帧率", "Frame rate", "Частота кадров");
label: GalleryStrings.texts[149];
label-reserve: self.width - 112px;
SettingsSelect {
border-radius: UiRadius.control;
x: 136px; y: 4px; width: parent.width - 150px;
current-index <=> root.frame-rate-index;
model: [GalleryStrings.tr("跟随设备设置", "Follow device settings", "Настройки устройства"), "15 FPS", "30 FPS", "60 FPS"];
model: [GalleryStrings.texts[150], "15 FPS", "30 FPS", "60 FPS"];
}
}
SettingsRow {
label: GalleryStrings.tr("视频总带宽", "Total bandwidth", "Общий битрейт");
label: GalleryStrings.texts[151];
label-reserve: self.width - 112px; divider: false;
SettingsSelect {
border-radius: UiRadius.control;
+69 -69
View File
@@ -156,7 +156,7 @@ export component GalleryView inherits Rectangle {
in property <int> count: 0;
in property <int> connected-count: 0;
private property <string> connected-summary: root.connected-count + " / " + root.count
+ GalleryStrings.tr(" 台已连接", " connected", " подключено");
+ GalleryStrings.texts[152];
callback network-open;
in property <int> selected-count: 0;
in property <bool> selected-audio-available: false;
@@ -297,10 +297,10 @@ export component GalleryView inherits Rectangle {
add-menu := GalleryMenuButton {
horizontal-padding: root.toolbar-button-padding;
width: self.preferred-button-width;
text: GalleryStrings.tr("添加", "Add", "Добавить"); menu-width: 232px;
text: GalleryStrings.texts[153]; menu-width: 232px;
items: [
{label: GalleryStrings.tr("通过设备 ID 添加", "Add by device ID", "Добавить по ID устройства"), enabled: true},
{label: GalleryStrings.tr("从近期连接添加", "Add from recent", "Добавить из недавних"), enabled: true},
{label: GalleryStrings.texts[154], enabled: true},
{label: GalleryStrings.texts[155], enabled: true},
];
opened => { root.device-tooltip-id = ""; root.action("release", ""); }
selected(index) => {
@@ -316,12 +316,12 @@ export component GalleryView inherits Rectangle {
horizontal-stretch: 1; min-width: 0px;
Text {
x: 0px; y: 0px; width: parent.width; height: root.hidden-selected > 0 ? 13px : parent.height;
text: GalleryStrings.tr("已选 ", "Selected ", "Выбрано ") + root.selected-count + GalleryStrings.tr(" 台", "", "");
text: GalleryStrings.texts[156] + root.selected-count + GalleryStrings.texts[52];
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
}
if root.hidden-selected > 0: Text {
y: 14px; width: parent.width; height: 10px;
text: GalleryStrings.tr("跨页 ", "Other pages ", "Вне страницы ") + root.hidden-selected;
text: GalleryStrings.texts[157] + root.hidden-selected;
color: #80858d; font-size: 10px; overflow: elide;
}
}
@@ -329,27 +329,27 @@ export component GalleryView inherits Rectangle {
horizontal-padding: root.toolbar-button-padding;
enabled: root.count > 0;
text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор")
: GalleryStrings.tr("全选", "Select all", "Выбрать все");
? GalleryStrings.texts[158]
: GalleryStrings.texts[159];
alternate-text: root.count > 0 && root.selected-count == root.count
? GalleryStrings.tr("全选", "Select all", "Выбрать все")
: GalleryStrings.tr("取消全选", "Deselect all", "Снять выбор");
? GalleryStrings.texts[159]
: GalleryStrings.texts[158];
clicked => { root.action("all-select", ""); }
}
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action("connect", ""); } }
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.tr("断开", "Disconnect", "Отключить"); clicked => { root.confirm-action("disconnect", self.text); } }
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; primary: true; enabled: root.selected-count > 0; text: GalleryStrings.texts[37]; clicked => { root.action("connect", ""); } }
GalleryToolbarButton { horizontal-padding: root.toolbar-button-padding; enabled: root.selected-count > 0; text: GalleryStrings.texts[36]; clicked => { root.confirm-action("disconnect", self.text); } }
batch-menu := GalleryMenuButton {
horizontal-padding: root.toolbar-button-padding;
width: self.preferred-button-width; enabled: root.selected-count > 0;
text: GalleryStrings.tr("更多", "More", "Ещё");
text: GalleryStrings.texts[68];
items: [
{label: root.selected-audio-muted ? GalleryStrings.tr("取消静音", "Unmute", "Со звуком") : GalleryStrings.tr("静音", "Mute", "Без звука"), enabled: root.selected-audio-available},
{label: GalleryStrings.tr("分发文件", "Send file", "Отправить файл"), enabled: root.selected-count > 0},
{label: root.selected-audio-muted ? GalleryStrings.texts[160] : GalleryStrings.texts[161], enabled: root.selected-audio-available},
{label: GalleryStrings.texts[162], enabled: root.selected-count > 0},
];
opened => { root.device-tooltip-id = ""; root.action("release", ""); }
selected(index) => {
if index == 0 { root.action(root.selected-audio-muted ? "unmute" : "mute", ""); }
else { root.confirm-action("file", GalleryStrings.tr("分发文件", "Send file", "Отправить файл")); }
else { root.confirm-action("file", GalleryStrings.texts[162]); }
}
}
}
@@ -357,8 +357,8 @@ export component GalleryView inherits Rectangle {
x: parent.width - self.width; y: 0px;
horizontal-padding: root.toolbar-button-padding;
primary: root.batch;
alternate-text: GalleryStrings.tr("选择", "Select", "Выбрать");
text: root.batch ? GalleryStrings.tr("完成", "Done", "Готово") : GalleryStrings.tr("选择", "Select", "Выбрать");
alternate-text: GalleryStrings.texts[163];
text: root.batch ? GalleryStrings.texts[164] : GalleryStrings.texts[163];
clicked => { root.batch = !root.batch; root.action("selection", ""); }
}
}
@@ -405,7 +405,7 @@ export component GalleryView inherits Rectangle {
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-text = GalleryStrings.texts[165];
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 = ""; }
@@ -461,8 +461,8 @@ export component GalleryView inherits Rectangle {
else if root.drag-source != "" { root.move-device-drag(self.mouse-x, self.mouse-y); }
}
for tile[index] in root.tiles: tile-card := Rectangle {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
private property <string> presence: tile.online ? GalleryStrings.texts[166]
: tile.presence-known ? GalleryStrings.texts[167] : GalleryStrings.texts[100];
private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence;
accessible-role: button; accessible-label: self.details;
accessible-action-default => { root.action(root.batch && !root.simplified ? "select" : "focus", tile.id); }
@@ -522,7 +522,7 @@ export component GalleryView inherits Rectangle {
changed over-actions => { if self.has-hover { self.show-details(); } }
if tile-touch.has-hover && !root.dragging && !root.resizing && !root.batch && !root.simplified: DeviceCardActions {
x: parent.width - self.width; y: parent.height - 23px;
remove-label: GalleryStrings.tr("从桌面墙移除", "Remove from DeskHub", "Убрать из DeskHub");
remove-label: GalleryStrings.texts[168];
connected: tile.connected;
edit-alias => { root.action("release", ""); root.edit-alias(tile.id, tile.name); }
edit-groups => { root.edit-groups(tile.id, tile.name); }
@@ -557,8 +557,8 @@ export component GalleryView inherits Rectangle {
}
if root.count == 0: Text {
text: root.simplified
? GalleryStrings.tr("暂无设备,退出精简模式后可添加", "No devices. Exit compact mode to add devices", "Нет устройств. Выйдите из компактного режима, чтобы добавить их")
: GalleryStrings.tr("添加设备,建立你的桌面墙", "Add devices to build your DeskHub", "Добавьте устройства в DeskHub");
? GalleryStrings.texts[169]
: GalleryStrings.texts[170];
color: #808080; horizontal-alignment: center; vertical-alignment: center;
}
}
@@ -568,7 +568,7 @@ export component GalleryView inherits Rectangle {
height: footer.y - 12px - self.y;
HorizontalLayout {
x: 0px; y: 0px; width: parent.width - root.view-controls-space; height: 24px; spacing: 8px;
GalleryButton { width: 52px; text: GalleryStrings.tr("返回", "Back", "Назад"); clicked => { root.action("focus", ""); } }
GalleryButton { width: 52px; text: GalleryStrings.texts[135]; clicked => { root.action("focus", ""); } }
Rectangle {
horizontal-stretch: 1; min-width: 0px;
Text { x: 0px; width: parent.width; height: parent.height; text: root.focused.name; color: root.focused.online ? #303943 : DeviceCardStyle.offline-text; font-size: 12px; font-weight: 600; overflow: elide; horizontal-alignment: left; vertical-alignment: center; }
@@ -579,10 +579,10 @@ export component GalleryView inherits Rectangle {
background: root.controlling ? #e7f1fd : #f0f2f5;
focus-state := Text {
x: 8px; width: parent.width - 16px; height: parent.height;
text: root.group-controlling ? GalleryStrings.tr("群控中", "Group control", "Групп. управление")
: root.controlling ? GalleryStrings.tr("正在操控", "Controlling", "Управление")
text: root.group-controlling ? GalleryStrings.texts[171]
: root.controlling ? GalleryStrings.texts[172]
: root.focused.connected && root.focused.has-frame && !root.focused.stale
? GalleryStrings.tr("只读预览", "View only", "Просмотр") : root.focused.status;
? GalleryStrings.texts[173] : root.focused.status;
font-size: 10px; color: root.controlling ? #3e77b3 : #6b7480; vertical-alignment: center; overflow: elide;
}
}
@@ -590,14 +590,14 @@ export component GalleryView inherits Rectangle {
horizontal-padding: root.toolbar-button-padding;
primary: root.group-controlling;
enabled: root.group-controlling || (root.focused.connected && root.focused.has-frame && !root.focused.stale);
text: root.group-controlling ? GalleryStrings.tr("关闭群控", "Stop group control", "Откл. групп. ввод") : GalleryStrings.tr("开启群控", "Group control", "Групп. ввод");
alternate-text: root.group-controlling ? GalleryStrings.tr("开启群控", "Group control", "Групп. ввод") : GalleryStrings.tr("关闭群控", "Stop group control", "Откл. групп. ввод");
text: root.group-controlling ? GalleryStrings.texts[174] : GalleryStrings.texts[175];
alternate-text: root.group-controlling ? GalleryStrings.texts[175] : GalleryStrings.texts[174];
accessible-role: button;
accessible-label: self.text + GalleryStrings.tr(":同步键鼠到所有已连接设备", ": mirror input to all connected devices", ": ввод на всех подключённых устройствах");
accessible-label: self.text + GalleryStrings.texts[176];
accessible-action-default => { self.clicked(); }
clicked => { root.action(root.group-controlling ? "group-release" : "group-control", root.focused.id); }
}
GalleryButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.tr("释放操控", "Release", "Отпустить") : GalleryStrings.tr("接管操控", "Control", "Управлять")) : GalleryStrings.tr("连接", "Connect", "Подключить"); clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } }
GalleryButton { width: 100px; primary: true; enabled: !root.focused.connected || root.controlling || (root.focused.has-frame && !root.focused.stale); text: root.focused.connected ? (root.controlling ? GalleryStrings.texts[177] : GalleryStrings.texts[178]) : GalleryStrings.texts[37]; clicked => { root.action(root.focused.connected ? (root.controlling ? "release" : "control") : "connect", root.focused.id); } }
}
FlatSurface {
x: 0px; y: rail.visible ? focus-divider.y + focus-divider.height : 36px;
@@ -671,8 +671,8 @@ export component GalleryView inherits Rectangle {
width: tile.id == root.focused.id ? 0px : root.rail-slot-width;
height: root.rail-card-height; visible: tile.id != root.focused.id;
rail-card := FlatSurface {
private property <string> presence: tile.online ? GalleryStrings.tr("在线", "Online", "В сети")
: tile.presence-known ? GalleryStrings.tr("离线", "Offline", "Не в сети") : GalleryStrings.tr("未知", "Unknown", "Неизвестно");
private property <string> presence: tile.online ? GalleryStrings.texts[166]
: tile.presence-known ? GalleryStrings.texts[167] : GalleryStrings.texts[100];
private property <string> details: tile.name + "\n" + tile.host-name + "\nID: " + tile.id + "\n" + tile.group-names + "\n" + self.presence;
width: root.rail-preview-width + 10px; height: root.rail-card-height; border-radius: UiRadius.surface;
background: tile.online ? #f5f6f8 : DeviceCardStyle.offline-background; outline-color: UiSurface.edge;
@@ -744,7 +744,7 @@ export component GalleryView inherits Rectangle {
function show-hint() {
if self.has-hover && !root.focus-resizing {
root.device-tooltip-id = "resize-focus";
root.device-tooltip-text = GalleryStrings.tr("上下拖动,调整缩略图与主画面大小", "Drag up or down to resize the thumbnails and main preview", "Потяните вверх или вниз, чтобы изменить размер миниатюр и основного экрана");
root.device-tooltip-text = GalleryStrings.texts[179];
root.device-tooltip-x = focus-view.x + self.mouse-x;
root.device-tooltip-y = focus-view.y + focus-divider.y + self.mouse-y + 16px;
} else if root.device-tooltip-id == "resize-focus" { root.device-tooltip-id = ""; }
@@ -788,7 +788,7 @@ export component GalleryView inherits Rectangle {
connection-count := Text {
x: 0px; width: min(self.preferred-width, parent.width - network-button.width - 12px); height: parent.height;
text: root.group-controlling
? GalleryStrings.tr("群控中 · 所有已连接设备", "Group control · All connected devices", "Групповое управление · Все подключения")
? GalleryStrings.texts[180]
: root.connected-summary;
color: #62666d; vertical-alignment: center; font-size: 11px; overflow: elide;
}
@@ -796,7 +796,7 @@ export component GalleryView inherits Rectangle {
x: connection-count.x + connection-count.width + 12px;
width: 64px; height: parent.height;
outline-color: UiSurface.edge; border-radius: UiRadius.control;
text: GalleryStrings.tr("网络状态", "Network", "Сеть");
text: GalleryStrings.texts[181];
accessible-role: button;
accessible-label: self.text;
accessible-action-default => { self.clicked(); }
@@ -817,9 +817,9 @@ export component GalleryView inherits Rectangle {
compact-toggle := GalleryToolbarButton {
horizontal-padding: root.toolbar-button-padding;
primary: root.simplified;
alternate-text: GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид");
text: root.simplified ? GalleryStrings.tr("退出精简", "Exit compact", "Обычный вид")
: GalleryStrings.tr("精简模式", "Compact", "Компактно");
alternate-text: GalleryStrings.texts[182];
text: root.simplified ? GalleryStrings.texts[182]
: GalleryStrings.texts[183];
clicked => { root.toggle-simplified(); }
}
IconButton {
@@ -827,7 +827,7 @@ export component GalleryView inherits Rectangle {
border-radius: UiRadius.control;
normal-background: ControlStyle.surface;
icon: root.fullscreen ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen ? GalleryStrings.tr("退出全屏", "Exit fullscreen", "Выйти из полноэкранного режима") : GalleryStrings.tr("全屏", "Fullscreen", "На весь экран");
tooltip: root.fullscreen ? GalleryStrings.texts[184] : GalleryStrings.texts[185];
clicked => { root.toggle-fullscreen(); }
}
}
@@ -859,9 +859,9 @@ export component GalleryView inherits Rectangle {
Text { x: 80px; y: 8px; width: parent.width - 88px; height: 18px; text: root.dragged-tile.name; font-size: 12px; color: #303943; overflow: elide; }
drag-hint := Text {
x: 80px; y: 29px; width: parent.width - 88px; height: max(16px, self.preferred-height);
text: root.drag-page-direction != 0 ? GalleryStrings.tr("停留翻页,松开取消", "Hold to turn page; release to cancel", "Удерживайте для перелистывания; отпустите для отмены")
: root.drag-target >= 0 ? GalleryStrings.tr("松开鼠标交换位置", "Release to swap", "Отпустите для обмена")
: GalleryStrings.tr("拖到另一台设备上", "Drag onto another device", "Перетащите на другое устройство");
text: root.drag-page-direction != 0 ? GalleryStrings.texts[186]
: root.drag-target >= 0 ? GalleryStrings.texts[187]
: GalleryStrings.texts[188];
font-size: 10px; color: #527ca8; wrap: word-wrap;
}
}
@@ -870,7 +870,7 @@ export component GalleryView inherits Rectangle {
width: min(420px, parent.width - 32px); height: 36px; z: 10;
background: #fff2f0; outline-color: UiSurface.edge; border-radius: UiRadius.surface;
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;
text: GalleryStrings.texts[189]; overflow: elide;
}
}
// Device details are rendered by MainWindow; resize hints stay local.
@@ -896,36 +896,36 @@ export component GalleryView inherits Rectangle {
root.adding = false;
}
}
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("添加设备", "Add device", "Добавить устройство"); font-size: 16px; }
Text { x: 18px; y: 56px; width: 72px; height: 30px; text: GalleryStrings.tr("设备 ID", "Device ID", "ID"); vertical-alignment: center; font-size: 12px; }
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.texts[190]; font-size: 16px; }
Text { x: 18px; y: 56px; width: 72px; height: 30px; text: GalleryStrings.texts[191]; vertical-alignment: center; font-size: 12px; }
device-id := RoundedLineEdit {
x: 96px; y: 56px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("9 位设备 ID", "9-digit device ID", "ID из 9 цифр");
placeholder-text: GalleryStrings.texts[192];
input-type: InputType.number;
accepted => { password.focus(); }
}
Text { x: 18px; y: 100px; width: 72px; height: 30px; text: GalleryStrings.tr("密码", "Password", "Пароль"); vertical-alignment: center; font-size: 12px; }
Text { x: 18px; y: 100px; width: 72px; height: 30px; text: GalleryStrings.texts[193]; vertical-alignment: center; font-size: 12px; }
password := RoundedLineEdit {
x: 96px; y: 100px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("设备连接密码", "Device password", "Пароль устройства");
placeholder-text: GalleryStrings.texts[194];
input-type: InputType.password;
accepted => { group.focus(); }
}
Text { x: 18px; y: 144px; width: 72px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); vertical-alignment: center; font-size: 12px; }
Text { x: 18px; y: 144px; width: 72px; height: 30px; text: GalleryStrings.texts[101]; vertical-alignment: center; font-size: 12px; }
group := RoundedLineEdit {
x: 96px; y: 144px; width: 286px; height: 30px;
placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)");
placeholder-text: GalleryStrings.texts[195];
accepted => { add-dialog.submit(); }
}
remember := ImGuiCheckBox { x: 96px; y: 188px; }
Text { x: 123px; y: 188px; height: 21px; text: GalleryStrings.tr("记住密码", "Remember password", "Запомнить пароль"); font-size: 12px; vertical-alignment: center;
Text { x: 123px; y: 188px; height: 21px; text: GalleryStrings.texts[196]; font-size: 12px; vertical-alignment: center;
TouchArea { clicked => { remember.checked = !remember.checked; } }
}
Text { x: 18px; y: 216px; width: 364px; height: 28px; text: root.notice; color: #8e4740; font-size: 11px; wrap: word-wrap; overflow: elide; }
GalleryButton { x: 136px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { password.text = ""; root.adding = false; } }
GalleryButton { x: 136px; y: 260px; width: 72px; height: 28px; text: GalleryStrings.texts[33]; clicked => { password.text = ""; root.adding = false; } }
GalleryButton {
x: 218px; y: 260px; width: 164px; height: 28px; primary: true;
text: GalleryStrings.tr("添加并连接", "Add and connect", "Добавить и подключить");
text: GalleryStrings.texts[197];
enabled: device-id.text != "" && password.text != "";
clicked => { add-dialog.submit(); }
}
@@ -937,12 +937,12 @@ export component GalleryView inherits Rectangle {
FlatSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2; width: 440px; height: 378px;
background: white; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.tr("从近期连接添加", "Add from recent connections", "Добавить из недавних"); font-size: 16px; }
Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.tr("加入桌面墙后,可通过批量选择连接", "Add to DeskHub, then select devices to connect.", "Добавьте в DeskHub, затем выберите устройства для подключения."); color: #62666d; font-size: 11px; }
Text { x: 18px; y: 16px; height: 24px; text: GalleryStrings.texts[155]; font-size: 16px; }
Text { x: 18px; y: 44px; width: 404px; height: 18px; text: GalleryStrings.texts[198]; color: #62666d; font-size: 11px; }
ImGuiCheckBox { x: 18px; y: 74px; enabled: root.recent-available > 0; checked: root.recent-devices.length > 0 && root.recent-selected == root.recent-available; }
Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.tr("全选", "Select all", "Выбрать все"); color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; }
Text { x: 45px; y: 74px; height: 21px; text: GalleryStrings.texts[159]; color: root.recent-available > 0 ? #24272d : #92969e; font-size: 12px; vertical-alignment: center; }
TouchArea { x: 18px; y: 74px; width: 144px; height: 21px; enabled: root.recent-available > 0; clicked => { root.action("recent-all", ""); } }
Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.tr("待添加 ", "To add ", "К добавлению ") + root.recent-selected + " / " + root.recent-available; horizontal-alignment: right; vertical-alignment: center; font-size: 12px; color: #62666d; }
Text { x: 224px; y: 74px; width: 198px; height: 21px; text: GalleryStrings.texts[199] + root.recent-selected + " / " + root.recent-available; horizontal-alignment: right; vertical-alignment: center; font-size: 12px; color: #62666d; }
FlatSurface {
x: 18px; y: 102px; width: 404px; height: 160px;
outline-color: UiSurface.edge; border-radius: UiRadius.surface; clip: true;
@@ -961,13 +961,13 @@ export component GalleryView inherits Rectangle {
Rectangle { x: parent.width - 74px; y: 19px; width: 6px; height: 6px; border-radius: 3px; background: !device.added && device.online ? #21b567 : #92969e; }
Text {
x: parent.width - 61px; y: 12px; width: 57px; height: 20px;
text: device.added ? GalleryStrings.tr("已添加", "Added", "Добавлено") : device.online ? GalleryStrings.tr("在线", "Online", "В сети") : GalleryStrings.tr("离线", "Offline", "Не в сети");
text: device.added ? GalleryStrings.texts[200] : device.online ? GalleryStrings.texts[166] : GalleryStrings.texts[167];
color: device.added ? #92969e : device.online ? #17854c : #808080; font-size: 11px; vertical-alignment: center;
}
row-touch := TouchArea { enabled: !device.added; clicked => { root.action("recent-select", device.id); } }
}
}
if root.recent-devices.length == 0: Text { text: GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений"); color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
if root.recent-devices.length == 0: Text { text: GalleryStrings.texts[89]; color: #808080; font-size: 12px; horizontal-alignment: center; vertical-alignment: center; }
Rectangle {
x: -UiRadius.surface; y: -UiRadius.surface;
width: parent.width + 2 * UiRadius.surface;
@@ -981,11 +981,11 @@ export component GalleryView inherits Rectangle {
outline-color: UiSurface.edge;
}
}
Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.tr("分组", "Group", "Группа"); font-size: 12px; vertical-alignment: center; }
import-group := RoundedLineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.tr("分组名称(可选)", "Group name (optional)", "Группа (необязательно)"); }
Text { x: 18px; y: 274px; width: 60px; height: 30px; text: GalleryStrings.texts[101]; font-size: 12px; vertical-alignment: center; }
import-group := RoundedLineEdit { x: 84px; y: 274px; width: 338px; height: 30px; text: root.import-group; placeholder-text: GalleryStrings.texts[195]; }
Text { x: 18px; y: 309px; width: 404px; height: 20px; text: root.notice; font-size: 11px; color: #8e4740; overflow: elide; }
GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.importing = false; } }
GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.tr("加入桌面墙", "Add to DeskHub", "В DeskHub"); enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } }
GalleryButton { x: 192px; y: 336px; width: 72px; text: GalleryStrings.texts[33]; clicked => { root.importing = false; } }
GalleryButton { x: 274px; y: 336px; width: 148px; primary: true; text: GalleryStrings.texts[201]; enabled: root.recent-selected > 0; clicked => { if root.import-recent(import-group.text) { root.importing = false; } } }
}
}
if root.confirming: Rectangle {
@@ -996,7 +996,7 @@ export component GalleryView inherits Rectangle {
background: white; border-radius: UiRadius.surface; outline-color: UiSurface.edge;
Text {
x: 20px; y: 18px; width: parent.width - 132px; height: 24px;
text: root.action-code == "disconnect" ? GalleryStrings.tr("断开连接", "Disconnect devices", "Отключение") : root.action-name;
text: root.action-code == "disconnect" ? GalleryStrings.texts[96] : root.action-name;
color: #24272d; font-size: 16px; font-weight: 600; vertical-alignment: center;
}
Rectangle {
@@ -1021,16 +1021,16 @@ export component GalleryView inherits Rectangle {
width: GalleryStrings.language == 0 ? 68px : 104px; height: 20px; border-radius: 10px;
background: device.connected ? #eaf6ee : #f0f2f5;
Rectangle { x: 9px; y: 7px; width: 6px; height: 6px; border-radius: 3px; background: device.connected ? #27a762 : #a0a6b0; }
Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.tr("已连接", "Connected", "Подключено") : GalleryStrings.tr("未连接", "Not connected", "Нет соединения"); color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; }
Text { x: 21px; width: parent.width - 27px; height: parent.height; text: device.connected ? GalleryStrings.texts[202] : GalleryStrings.texts[203]; color: device.connected ? #348259 : #858d98; font-size: 10px; vertical-alignment: center; }
}
if index + 1 < root.selected-devices.length: SurfaceSeparator { x: 12px; y: parent.height - 1px; width: parent.width - 24px; height: 1px; outline-color: UiSurface.separator; }
}
}
}
GalleryButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.tr("取消", "Cancel", "Отмена"); clicked => { root.confirming = false; } }
GalleryButton { x: parent.width - 208px; y: parent.height - 46px; width: 72px; height: 28px; text: GalleryStrings.texts[33]; clicked => { root.confirming = false; } }
GalleryButton {
x: parent.width - 126px; y: parent.height - 46px; width: 108px; height: 28px; primary: true;
text: root.action-code == "disconnect" ? GalleryStrings.tr("确认断开", "Disconnect", "Отключить") : GalleryStrings.tr("确认执行", "Continue", "Продолжить");
text: root.action-code == "disconnect" ? GalleryStrings.texts[204] : GalleryStrings.texts[205];
clicked => { root.confirming = false; root.action(root.action-code, ""); }
}
}
+32 -32
View File
@@ -5,24 +5,24 @@ export global LanStrings {
// Slint Text does not expose a line-height property.
public pure function access-error-lines(code: string) -> [string] {
if code == "LanPortInUse" { return [
GalleryStrings.tr("此端口已被占用,", "This port is in use.", "Порт занят."),
GalleryStrings.tr("请更换端口。", "Choose another port.", "Выберите другой порт."),
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
GalleryStrings.texts[0],
GalleryStrings.texts[1],
GalleryStrings.texts[2]
]; }
if code == "LanPortCheckFailed" { return [
GalleryStrings.tr("无法确认此端口是否可用,", "Could not check this port.", "Не удалось проверить порт."),
GalleryStrings.tr("请重试或检查本机防火墙。", "Retry or check the local firewall.", "Повторите попытку или проверьте брандмауэр."),
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
GalleryStrings.texts[3],
GalleryStrings.texts[4],
GalleryStrings.texts[2]
]; }
if code == "LanPortPermissionDenied" { return [
GalleryStrings.tr("无法使用此端口,", "Cannot use this port.", "Порт недоступен."),
GalleryStrings.tr("请选择其他端口或检查系统权限。", "Choose another port or check system permissions.", "Смените порт или проверьте разрешения."),
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
GalleryStrings.texts[5],
GalleryStrings.texts[6],
GalleryStrings.texts[2]
]; }
if code == "Selected LAN interface is unavailable" { return [
GalleryStrings.tr("未找到可用的局域网连接。", "No local network connection was found.", "Нет доступного подключения к LAN."),
GalleryStrings.tr("请连接 Wi-Fi 或网线,", "Connect to Wi-Fi or Ethernet,", "Подключитесь к Wi-Fi или Ethernet"),
GalleryStrings.tr("或在主界面选择本机 IP。", "or select a local IP on the main screen.", "или выберите локальный IP в главном окне.")
GalleryStrings.texts[7],
GalleryStrings.texts[8],
GalleryStrings.texts[9]
]; }
// Keep all branches the same size for Slint's C++ model generator.
return ["", "", ""];
@@ -30,29 +30,29 @@ export global LanStrings {
public pure function error-message(code: string) -> string {
let lines = LanStrings.access-error-lines(code);
if lines[0] != "" {
let separator = GalleryStrings.tr("", " ", " ");
let separator = GalleryStrings.texts[10];
return lines[0] + separator + lines[1] + separator + lines[2];
}
if code == "" { return ""; }
if code == "InvalidCredentials" { return GalleryStrings.tr("密码不正确,或对方密码已更新。请查看对方的局域网访问密码后重试。", "The password is incorrect or has changed. Check the other desktop's current LAN password and try again.", "Пароль неверен или изменён. Проверьте текущий пароль LAN другого компьютера и повторите попытку."); }
if code == "AuthenticationRateLimited" { return GalleryStrings.tr("尝试次数过多,请等待倒计时结束后重试。", "Too many attempts. Try again when the countdown ends.", "Слишком много попыток. Дождитесь окончания обратного отсчёта."); }
if code == "CredentialsChanged" { return GalleryStrings.tr("对方密码已更新,请使用新密码重试。", "The password changed. Try again with the new password.", "Пароль изменился. Повторите с новым паролем."); }
if code == "Busy" { return GalleryStrings.tr("设备正忙,请稍后重试。", "The desktop is busy. Try again shortly.", "Компьютер занят. Повторите позже."); }
if code == "LAN password must contain exactly 6 ASCII letters or digits" { return GalleryStrings.tr("访问密码必须为 6 位英文字母或数字,区分大小写。", "Use exactly 6 letters (A–Z, a–z) or digits (0–9). Case-sensitive.", "Пароль: ровно 6 латинских букв или цифр. Регистр учитывается."); }
if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "IceConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" || code == "HeartbeatTimeout" || code == "SignalSendFailed" { return GalleryStrings.tr("无法连接,请检查对方 IP、程序是否运行,以及防火墙是否放行。", "Could not connect. Check the IP, whether the other desktop is running CrossDesk, and its firewall settings.", "Не удалось подключиться. Проверьте IP, работу CrossDesk на другом компьютере и брандмауэр."); }
if code == "Enter a numeric IPv4 address or IPv4:port" || code == "Invalid LAN endpoint" { return GalleryStrings.tr("请输入有效的 IPv4,例如 192.168.1.20;指定端口可用 IPv4:端口,端口范围 1–65535。", "Enter a valid IPv4 address such as 192.168.1.20, or IPv4:port (1–65535).", "Введите IPv4, например 192.168.1.20, или IPv4:порт (1–65535)."); }
if code == "Invalid LAN port" { return GalleryStrings.tr("请输入 1–65535 之间的整数端口。", "Enter a port number from 1 to 65535.", "Введите целый номер порта от 1 до 65535."); }
if code == "LAN password is not ready" { return GalleryStrings.tr("请点击主界面密码旁的修改按钮设置密码。", "Set a password using Edit next to the password on the main screen.", "Задайте пароль кнопкой редактирования рядом с паролем в главном окне."); }
if code == "LAN connection limit reached" { return GalleryStrings.tr("IP 连接数量已达上限,请先断开部分连接后重试。", "The IP connection limit has been reached. Disconnect some sessions and try again.", "Достигнут лимит IP-подключений. Завершите часть сеансов и повторите попытку."); }
if code == "UnsupportedNetworkPath" || code == "NetworkPolicyFailed" { return GalleryStrings.tr("当前网络路径不支持 IP 连接,请检查双方的局域网连接。", "This network path does not support IP connections. Check both devices' LAN connections.", "Этот сетевой маршрут не поддерживает IP-подключения. Проверьте подключения обоих устройств к LAN."); }
if code == "IncompatibleProtocol" || code == "RequiredCryptoUnsupported" || code == "TLS 1.3 is required for LAN connections" { return GalleryStrings.tr("双方的协议或加密能力不兼容,请将双方 CrossDesk 升级至最新版本后重试。", "The protocol or encryption capabilities are incompatible. Update CrossDesk on both devices and try again.", "Протоколы или возможности шифрования несовместимы. Обновите CrossDesk на обоих устройствах и повторите попытку."); }
if code == "InvalidCertificate" || code == "FingerprintMissing" || code == "FingerprintMismatch" { return GalleryStrings.tr("无法验证对方设备的身份,请确认连接地址和对方设备后重试。", "Could not verify the remote device's identity. Check the address and the remote device before retrying.", "Не удалось проверить подлинность удалённого устройства. Проверьте адрес и устройство перед повторной попыткой."); }
if code == "DtlsHandshakeFailed" || code == "SrtpInstallFailed" || code == "SrtpKeyExportFailed" || code == "SecureTransportFailed" { return GalleryStrings.tr("无法建立加密连接,请检查双方网络并重试。", "Could not establish an encrypted connection. Check both devices' networks and try again.", "Не удалось установить зашифрованное соединение. Проверьте сеть на обоих устройствах и повторите попытку."); }
if code == "InvalidSignal" || code == "InvalidLocalSignal" || code == "Invalid LAN protocol message" { return GalleryStrings.tr("连接协商数据无效,请重试并确认双方 CrossDesk 版本兼容。", "Invalid connection negotiation data. Try again and check that both CrossDesk versions are compatible.", "Неверные данные согласования соединения. Повторите попытку и проверьте совместимость версий CrossDesk."); }
if code == "ListenerChanged" { return GalleryStrings.tr("连接地址或监听端口已更改,请确认最新地址后重试。", "The connection address or listening port has changed. Check the current address and try again.", "Адрес подключения или порт прослушивания изменился. Проверьте текущий адрес и повторите попытку."); }
if code == "PeerPreparationFailed" { return GalleryStrings.tr("无法准备远程会话,请稍后重试。", "Could not prepare the remote session. Try again shortly.", "Не удалось подготовить удалённый сеанс. Повторите позже."); }
if code == "Cannot copy text" { return GalleryStrings.tr("复制失败,请重试。", "Could not copy. Try again.", "Не удалось скопировать. Повторите попытку."); }
if code == "InvalidCredentials" { return GalleryStrings.texts[11]; }
if code == "AuthenticationRateLimited" { return GalleryStrings.texts[12]; }
if code == "CredentialsChanged" { return GalleryStrings.texts[13]; }
if code == "Busy" { return GalleryStrings.texts[14]; }
if code == "LAN password must contain exactly 6 ASCII letters or digits" { return GalleryStrings.texts[15]; }
if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "IceConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" || code == "HeartbeatTimeout" || code == "SignalSendFailed" { return GalleryStrings.texts[16]; }
if code == "Enter a numeric IPv4 address or IPv4:port" || code == "Invalid LAN endpoint" { return GalleryStrings.texts[17]; }
if code == "Invalid LAN port" { return GalleryStrings.texts[18]; }
if code == "LAN password is not ready" { return GalleryStrings.texts[19]; }
if code == "LAN connection limit reached" { return GalleryStrings.texts[20]; }
if code == "UnsupportedNetworkPath" || code == "NetworkPolicyFailed" { return GalleryStrings.texts[21]; }
if code == "IncompatibleProtocol" || code == "RequiredCryptoUnsupported" || code == "TLS 1.3 is required for LAN connections" { return GalleryStrings.texts[22]; }
if code == "InvalidCertificate" || code == "FingerprintMissing" || code == "FingerprintMismatch" { return GalleryStrings.texts[23]; }
if code == "DtlsHandshakeFailed" || code == "SrtpInstallFailed" || code == "SrtpKeyExportFailed" || code == "SecureTransportFailed" { return GalleryStrings.texts[24]; }
if code == "InvalidSignal" || code == "InvalidLocalSignal" || code == "Invalid LAN protocol message" { return GalleryStrings.texts[25]; }
if code == "ListenerChanged" { return GalleryStrings.texts[26]; }
if code == "PeerPreparationFailed" { return GalleryStrings.texts[27]; }
if code == "Cannot copy text" { return GalleryStrings.texts[28]; }
// Keep diagnostic details for unexpected system errors, with a localized explanation.
return GalleryStrings.tr("IP 连接操作失败,详情:", "IP connection operation failed. Details: ", "Ошибка операции IP-подключения. Подробности: ") + code;
return GalleryStrings.texts[29] + code;
}
}
+84 -90
View File
@@ -265,7 +265,7 @@ component PasswordInput inherits FlatSurface {
}
export component MainWindow inherits Window {
title: root.gallery-mode ? "CrossDesk " + GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub") : "CrossDesk";
title: root.gallery-mode ? "CrossDesk " + GalleryStrings.texts[54] : "CrossDesk";
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
preferred-width: root.minimum-window-width;
preferred-height: root.minimum-window-height;
@@ -455,7 +455,7 @@ export component MainWindow inherits Window {
root.lan-remember-password = false;
let error = root.lan-prepare-connect(address);
root.connection-input-error = error == "Enter a numeric IPv4 address or IPv4:port"
? GalleryStrings.tr("请输入有效的 IPv4 或 IPv4:端口", "Enter a valid IPv4 or IPv4:port", "Укажите IPv4 или IPv4:порт")
? GalleryStrings.texts[55]
: LanStrings.error-message(error);
if error == "" {
root.lan-open = true;
@@ -527,8 +527,8 @@ export component MainWindow inherits Window {
private property <bool> prompt-validating: root.lan-open ? root.lan-connecting : root.connection-validating;
private property <bool> prompt-pending: root.lan-open ? root.lan-connecting : root.connection-pending;
private property <string> prompt-status-text: !root.lan-open ? root.connection-status-text
: root.lan-connecting ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…")
: root.lan-retry-after > 0 ? GalleryStrings.tr("请在 " + root.lan-retry-after + " 秒后重试", "Try again in " + root.lan-retry-after + "s", "Повторите через " + root.lan-retry-after + " с")
: root.lan-connecting ? GalleryStrings.texts[56]
: root.lan-retry-after > 0 ? GalleryStrings.retry-after(root.lan-retry-after)
: root.lan-message == "InvalidCredentials" ? UiStrings.reinput-password
: root.lan-message == "" || root.lan-message == "AuthenticationRateLimited" ? UiStrings.input-password
: LanStrings.error-message(root.lan-message);
@@ -556,7 +556,7 @@ export component MainWindow inherits Window {
in property <[RecentConnection]> recent-connections;
out property <length> recent-preview-width: max(1px, (recent-scroll.width - 24px) / 3);
out property <length> recent-preview-height: 80px;
in property <[string]> recent-groups: [GalleryStrings.tr("全部设备", "All devices", "Все устройства"), GalleryStrings.tr("未分组", "Ungrouped", "Без группы")];
in property <[string]> recent-groups: [GalleryStrings.texts[57], GalleryStrings.texts[31]];
in-out property <int> recent-group-index: 0;
callback recent-group-selected(int);
changed recent-group-index => {
@@ -628,7 +628,8 @@ export component MainWindow inherits Window {
in property <string> portable-service-status: "";
in-out property <bool> portable-service-do-not-remind: false;
in-out property <int> language-index: 0;
in-out property <int> language-index: 10;
in property <[string]> language-options;
in property <bool> capture-method-visible: false;
in-out property <int> capture-method-index: 0;
in-out property <int> codec-index: 0;
@@ -640,11 +641,11 @@ export component MainWindow inherits Window {
in property <bool> autostart-system-managed: false;
in property <AutostartStatus> autostart-status: AutostartStatus.checking;
out property <string> autostart-status-text:
root.autostart-status == AutostartStatus.checking ? GalleryStrings.tr("正在读取…", "Checking…", "Проверка…") :
root.autostart-status == AutostartStatus.enabled ? GalleryStrings.tr("已开启", "Enabled", "Включён") :
root.autostart-status == AutostartStatus.disabled ? GalleryStrings.tr("未开启", "Disabled", "Выключен") :
root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr("设备不支持", "Unsupported", "Не поддерживается") :
GalleryStrings.tr("读取失败", "Read failed", "Ошибка чтения");
root.autostart-status == AutostartStatus.checking ? GalleryStrings.texts[58] :
root.autostart-status == AutostartStatus.enabled ? GalleryStrings.texts[59] :
root.autostart-status == AutostartStatus.disabled ? GalleryStrings.texts[60] :
root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.texts[61] :
GalleryStrings.texts[62];
in-out property <bool> autostart-guide-open: false;
changed autostart-guide-open => { if root.autostart-guide-open { root.dialog-focus-pending = true; } }
in-out property <bool> daemon-enabled: false;
@@ -770,7 +771,7 @@ export component MainWindow inherits Window {
focus-sink.focus();
}
private property <CopyFeedback> copy-feedback: idle;
private property <bool> compact-language: root.language-index == 0;
private property <bool> compact-language: GalleryStrings.language == 0 || GalleryStrings.language == 3 || GalleryStrings.language == 4 || GalleryStrings.language == 5;
private property <bool> recent-tooltip-visible: false;
private property <string> recent-tooltip-name: "";
private property <string> recent-tooltip-host: "";
@@ -1114,9 +1115,9 @@ export component MainWindow inherits Window {
width: 136px; height: 28px;
background: WorkbenchStyle.field-surface;
enabled: !root.connection-pending && !root.lan-connecting;
tooltip: root.lan-mode ? GalleryStrings.tr("切换到 ID 连接模式", "Switch to ID connection mode", "Переключить в режим ID") : GalleryStrings.tr("切换到 IP 连接模式", "Switch to IP connection mode", "Переключить в режим IP");
tooltip: root.lan-mode ? GalleryStrings.texts[63] : GalleryStrings.texts[64];
accessible-role: switch;
accessible-label: GalleryStrings.tr("IP 连接模式", "IP connection mode", "Режим IP");
accessible-label: GalleryStrings.texts[65];
accessible-checked: root.lan-mode;
z: 8;
clicked => {
@@ -1138,7 +1139,7 @@ export component MainWindow inherits Window {
y: 2px;
width: (parent.width - 6px) / 2;
height: parent.height - 4px;
text: lan ? GalleryStrings.tr("IP 连接", "IP mode", "Режим IP") : GalleryStrings.tr("ID 连接", "ID mode", "Режим ID");
text: lan ? GalleryStrings.texts[66] : GalleryStrings.texts[67];
font-size: 12px;
font-weight: 500;
color: root.lan-mode == lan ? white : WorkbenchStyle.muted;
@@ -1154,7 +1155,7 @@ export component MainWindow inherits Window {
y: connection-mode-switch.y;
width: 80px; height: 28px;
normal-background: white;
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
text: GalleryStrings.texts[54];
z: 8;
clicked => { root.open-gallery(); }
}
@@ -1164,7 +1165,7 @@ export component MainWindow inherits Window {
width: 36px; height: 28px;
normal-background: white;
icon: FontAwesomeIcons.bars;
tooltip: GalleryStrings.tr("更多", "More", "Ещё");
tooltip: GalleryStrings.texts[68];
z: 8;
clicked => { root.gallery-action("release", ""); root.menu-open = !root.menu-open; }
}
@@ -1247,16 +1248,16 @@ export component MainWindow inherits Window {
x: 0px; y: 0px;
width: (parent.width - 16px) / 2; height: parent.height;
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.local-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("将 IP 和密码分享给连接方", "Share your IP and password to get connected", "Передайте IP и пароль для подключения") : GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
local-address-label := Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("本机 IP / 端口", "Local IP / Port", "IP / Порт") : UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; vertical-alignment: top; }
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.texts[69] : GalleryStrings.texts[70]; font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
local-address-label := Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.texts[71] : UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; vertical-alignment: top; }
lan-switch-ip-metrics := Text {
visible: false; accessible-role: none;
text: GalleryStrings.tr("切换 IP", "Switch IP", "Сменить IP");
text: GalleryStrings.texts[72];
font-size: 12px; font-weight: 500;
}
lan-edit-port-metrics := Text {
visible: false; accessible-role: none;
text: GalleryStrings.tr("修改端口", "Edit port", "Изменить порт");
text: GalleryStrings.texts[73];
font-size: 12px; font-weight: 500;
}
lan-switch-ip-action := WorkbenchButton {
@@ -1328,7 +1329,7 @@ export component MainWindow inherits Window {
normal-outline-color: UiSurface.edge;
hover-outline-color: UiSurface.hover;
focus-outline-color: WorkbenchStyle.accent;
accessible-label: GalleryStrings.tr("切换本机 IP", "Switch local IP", "Сменить локальный IP");
accessible-label: GalleryStrings.texts[74];
selected(index) => {
root.copy-feedback = CopyFeedback.idle;
// Save the draft for its original IP before changing
@@ -1384,7 +1385,7 @@ export component MainWindow inherits Window {
font-size: 18px * local-address-field.lan-font-scale; font-weight: 600;
input-type: InputType.number; color: WorkbenchStyle.text;
enabled: root.lan-mode && !root.lan-password-pending;
accessible-label: GalleryStrings.tr("局域网端口", "LAN port", "Порт LAN");
accessible-label: GalleryStrings.texts[75];
vertical-alignment: center; horizontal-alignment: left;
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
changed has-focus => {
@@ -1403,7 +1404,7 @@ export component MainWindow inherits Window {
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
icon: root.copy-feedback == CopyFeedback.success ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
icon-color: root.copy-feedback == CopyFeedback.success ? WorkbenchStyle.success : WorkbenchStyle.muted;
tooltip: root.lan-mode ? GalleryStrings.tr("复制 IP 和端口", "Copy IP and port", "Копировать IP и порт") : GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
tooltip: root.lan-mode ? GalleryStrings.texts[76] : GalleryStrings.texts[77];
enabled: !root.lan-mode || root.lan-listener-address != "";
tooltip-suppressed: root.copy-feedback != CopyFeedback.idle;
// Let other controls receive the original click; moving
@@ -1420,8 +1421,8 @@ export component MainWindow inherits Window {
}
private property <string> edit-hint: !root.lan-mode || !root.sponsorship-unobstructed
|| root.lan-password-pending ? ""
: lan-ip-picker.has-hover ? GalleryStrings.tr("点击切换本机 IP", "Click to switch the local IP", "Нажмите, чтобы сменить IP")
: lan-port-hover.has-hover && !lan-port-editor.has-focus ? GalleryStrings.tr("点击修改端口", "Click to edit the port", "Нажмите, чтобы изменить порт") : "";
: lan-ip-picker.has-hover ? GalleryStrings.texts[78]
: lan-port-hover.has-hover && !lan-port-editor.has-focus ? GalleryStrings.texts[79] : "";
private property <bool> edit-hint-visible;
changed edit-hint => { local-address-field.edit-hint-visible = false; }
Timer {
@@ -1453,7 +1454,7 @@ export component MainWindow inherits Window {
WorkbenchButton {
x: parent.width - 112px; y: 0px; width: 36px; quiet: true;
icon: root.active-password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
tooltip: root.active-password-visible ? GalleryStrings.tr("隐藏密码", "Hide password", "Скрыть пароль") : GalleryStrings.tr("显示密码", "Show password", "Показать пароль");
tooltip: root.active-password-visible ? GalleryStrings.texts[80] : GalleryStrings.texts[81];
clicked => { if root.lan-mode { root.lan-password-visible = !root.lan-password-visible; } else { root.toggle-password-visibility(); } }
}
WorkbenchButton {
@@ -1465,7 +1466,7 @@ export component MainWindow inherits Window {
WorkbenchButton {
x: parent.width - 36px; y: 0px; width: 36px; quiet: true;
icon: FontAwesomeIcons.pen;
tooltip: GalleryStrings.tr("修改密码", "Change password", "Изменить пароль");
tooltip: GalleryStrings.texts[82];
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
clicked => { root.reset-password-open = true; }
}
@@ -1474,8 +1475,8 @@ export component MainWindow inherits Window {
WorkbenchCard {
x: local-card.width + 16px; y: 0px; width: local-card.width; height: parent.height;
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.remote-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("输入 IP:端口,通过局域网连接", "Enter IP:port to connect over LAN", "Введите IP:порт для подключения по LAN") : GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.texts[83] : GalleryStrings.texts[84]; font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.texts[85] : UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
FlatSurface {
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
background: remote-id-editor.has-focus ? ControlStyle.input-focused : ControlStyle.input-surface;
@@ -1485,14 +1486,14 @@ export component MainWindow inherits Window {
animate outline-color { duration: 120ms; }
if root.remote-id-input == "": Text {
x: 12px; width: parent.width - 24px; height: parent.height;
text: root.lan-mode ? "192.168.1.20:52888" : GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр");
text: root.lan-mode ? "192.168.1.20:52888" : GalleryStrings.texts[86];
font-size: 13px; color: ControlStyle.input-placeholder; vertical-alignment: center;
}
remote-id-editor := TextInput {
x: 12px; width: parent.width - 24px; height: parent.height;
text <=> root.remote-id-input; font-size: root.lan-mode ? 18px : 24px; font-weight: 600; letter-spacing: root.lan-mode ? 0px : 1px;
input-type: root.lan-mode ? InputType.text : InputType.number; color: WorkbenchStyle.text;
accessible-label: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id;
accessible-label: root.lan-mode ? GalleryStrings.texts[85] : UiStrings.remote-id;
enabled: !root.connection-pending && !root.lan-connecting;
vertical-alignment: center;
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
@@ -1512,13 +1513,13 @@ export component MainWindow inherits Window {
Text {
x: 16px; y: 133px; width: parent.width - 32px; height: 24px;
text: root.connection-input-error != "" ? root.connection-input-error
: root.lan-mode ? GalleryStrings.tr("不填端口时,默认使用 52888", "Port defaults to 52888 if omitted", "Без порта используется 52888") : "";
: root.lan-mode ? GalleryStrings.texts[87] : "";
font-size: 11px; color: root.connection-input-error != "" ? #be3d42 : WorkbenchStyle.muted; wrap: word-wrap;
}
WorkbenchButton {
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
primary: true; icon: FontAwesomeIcons.arrow-right-long;
text: root.connection-pending || root.lan-connecting ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…") : UiStrings.connect;
text: root.connection-pending || root.lan-connecting ? GalleryStrings.texts[56] : UiStrings.connect;
enabled: !root.connection-pending && !root.lan-connecting;
clicked => { root.submit-connection(); }
}
@@ -1566,19 +1567,19 @@ export component MainWindow inherits Window {
Text {
x: 84px; y: 36px; width: parent.width - 104px; height: 24px;
text: root.lan-mode
? GalleryStrings.tr("暂无近期 IP 连接", "No recent IP connections", "Нет недавних IP-подключений")
? GalleryStrings.texts[88]
: root.recent-group-index == 0
? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений")
: GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений");
? GalleryStrings.texts[89]
: GalleryStrings.texts[90];
color: WorkbenchStyle.text; font-size: 14px; font-weight: 600;
}
Text {
x: 84px; y: 68px; width: parent.width - 104px; height: 20px;
text: root.lan-mode
? GalleryStrings.tr("通过 IP 连接过的设备会显示在这里。", "Devices you connect to by IP will appear here.", "Здесь появятся устройства, подключённые по IP.")
? GalleryStrings.texts[91]
: root.recent-group-index == 0
? GalleryStrings.tr("连接过的设备会显示在这里,方便下次快速访问。", "Devices you connect to will appear here for quick access.", "Устройства появятся здесь после первого подключения.")
: GalleryStrings.tr("切换设备分组,查看其他连接记录。", "Choose another group to see your other connections.", "Выберите другую группу для просмотра подключений.");
? GalleryStrings.texts[92]
: GalleryStrings.texts[93];
color: WorkbenchStyle.muted; font-size: 12px;
}
}
@@ -1762,12 +1763,12 @@ export component MainWindow inherits Window {
compact-trigger: true;
compact-menu: true;
enabled: !root.gallery-mode;
text: GalleryStrings.tr("更多操作", "More actions", "Другие действия");
menu-width: root.compact-language ? 116px : root.language-index == 2 ? 156px : 128px;
text: GalleryStrings.texts[94];
menu-width: root.compact-language ? 116px : 156px;
items: [
{label: GalleryStrings.tr("修改名称", "Rename", "Переименовать"), enabled: true},
{label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: !connection.lan},
{label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"), enabled: true},
{label: GalleryStrings.texts[35], enabled: true},
{label: GalleryStrings.texts[30], enabled: !connection.lan},
{label: GalleryStrings.texts[95], enabled: true},
];
item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can];
destructive-index: 2;
@@ -1794,7 +1795,7 @@ export component MainWindow inherits Window {
primary: !connection.connected;
destructive: connection.connected;
icon: connection.connected ? FontAwesomeIcons.xmark : FontAwesomeIcons.arrow-right-long;
tooltip: connection.connected ? GalleryStrings.tr("断开连接", "Disconnect", "Отключить") : UiStrings.connect;
tooltip: connection.connected ? GalleryStrings.texts[96] : UiStrings.connect;
tooltip-above: true;
clicked => {
root.copy-feedback = CopyFeedback.idle;
@@ -1884,7 +1885,7 @@ export component MainWindow inherits Window {
height: parent.height;
width: parent.width - 160px;
overflow: elide;
text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.tr("IP 连接已启用", "IP connections enabled", "Подключения по IP включены") : root.lan-password-pending ? GalleryStrings.tr("IP 连接准备中…", "Preparing IP connections…", "Подготовка подключений по IP…") : GalleryStrings.tr("IP 连接未就绪", "IP connections not ready", "Подключения по IP не готовы"))
text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.texts[97] : root.lan-password-pending ? GalleryStrings.texts[98] : GalleryStrings.texts[99])
: root.signal-tls-error ? UiStrings.tls-error
: root.signal-connected ? UiStrings.signal-connected
: root.signal-reconnecting ? UiStrings.signal-reconnecting
@@ -1969,7 +1970,7 @@ export component MainWindow inherits Window {
tooltip-status-text := Text {
x: 7px; width: parent.width - 14px; height: parent.height;
text: device-tip.online ? UiStrings.online : device-tip.presence-known ? UiStrings.offline
: GalleryStrings.tr("未知", "Unknown", "Неизвестно");
: GalleryStrings.texts[100];
color: device-tip.status-color; font-size: 10px; font-weight: 600;
vertical-alignment: center;
}
@@ -2013,14 +2014,14 @@ export component MainWindow inherits Window {
tooltip-group-label := Text {
x: 12px; y: tooltip-groups.y;
width: device-tip.label-width; height: 16px;
text: GalleryStrings.tr("分组", "Groups", "Группы");
text: GalleryStrings.texts[101];
color: WorkbenchStyle.muted; font-size: 10px;
vertical-alignment: center;
}
tooltip-groups := Text {
x: 20px + device-tip.label-width; y: tooltip-id-label.y + tooltip-id-label.height + 4px;
width: parent.width - self.x - 12px; height: max(16px, self.preferred-height);
text: device-tip.group-names == "" ? GalleryStrings.tr("未分组", "Ungrouped", "Без группы") : device-tip.group-names;
text: device-tip.group-names == "" ? GalleryStrings.texts[31] : device-tip.group-names;
color: device-tip.group-names == "" ? WorkbenchStyle.muted : WorkbenchStyle.accent;
font-size: 10px;
wrap: word-wrap; horizontal-alignment: right; vertical-alignment: center;
@@ -2220,8 +2221,8 @@ export component MainWindow inherits Window {
error: root.copy-feedback == CopyFeedback.failed;
icon: self.error ? FontAwesomeIcons.xmark : FontAwesomeIcons.check;
text: self.error
? GalleryStrings.tr("复制失败,请重试", "Couldn’t copy. Try again", "Не удалось скопировать. Повторите попытку")
: root.lan-mode ? GalleryStrings.tr("IP 和端口已复制", "IP and port copied", "IP и порт скопированы") : UiStrings.copied;
? GalleryStrings.texts[102]
: root.lan-mode ? GalleryStrings.texts[103] : UiStrings.copied;
z: 30;
}
@@ -2332,7 +2333,7 @@ export component MainWindow inherits Window {
height: 140px;
VerticalLayout {
padding: 20px; spacing: 18px;
DialogPromptText { prompt: root.delete-from-gallery ? (root.gallery-group-index > 0 ? GalleryStrings.tr("从当前分组移除此设备?", "Remove from this group?", "Убрать из этой группы?") : GalleryStrings.tr("从桌面墙移除此设备?", "Remove from DeskHub?", "Убрать из DeskHub?")) : UiStrings.confirm-delete; }
DialogPromptText { prompt: root.delete-from-gallery ? (root.gallery-group-index > 0 ? GalleryStrings.texts[104] : GalleryStrings.texts[105]) : UiStrings.confirm-delete; }
HorizontalLayout {
alignment: center; spacing: 8px;
PrimaryButton { text: UiStrings.ok; clicked => { if root.delete-from-gallery { root.gallery-action("remove", root.delete-remote-id); } else { root.recent-delete(root.delete-remote-id); } root.delete-open = false; } }
@@ -2385,7 +2386,7 @@ export component MainWindow inherits Window {
padding: 24px;
spacing: 16px;
Text {
text: GalleryStrings.tr("确认断开连接?", "Disconnect this session?", "Отключить этот сеанс?");
text: GalleryStrings.texts[106];
color: WorkbenchStyle.text;
font-size: 18px;
font-weight: 600;
@@ -2407,7 +2408,7 @@ export component MainWindow inherits Window {
}
}
Text {
text: GalleryStrings.tr("当前远程控制会话将结束。", "The current remote session will end.", "Текущий сеанс удалённого управления будет завершён.");
text: GalleryStrings.texts[107];
color: WorkbenchStyle.muted;
font-size: 13px;
wrap: word-wrap;
@@ -2420,7 +2421,7 @@ export component MainWindow inherits Window {
clicked => { root.dismiss-disconnect(); }
}
disconnect-confirm := WorkbenchButton {
text: GalleryStrings.tr("断开连接", "Disconnect", "Отключить");
text: GalleryStrings.texts[96];
destructive: true;
clicked => {
let remote-id = root.disconnect-remote-id;
@@ -2548,9 +2549,9 @@ export component MainWindow inherits Window {
spacing: 12px;
for permission[index] in [
{label: UiStrings.accessibility-permission, granted: root.accessibility-granted,
description: GalleryStrings.tr("允许远程键盘和鼠标控制", "Allow remote keyboard and mouse input", "Разрешить удалённый ввод с клавиатуры и мыши")},
description: GalleryStrings.texts[108]},
{label: UiStrings.screen-recording-permission, granted: root.screen-recording-granted,
description: GalleryStrings.tr("允许共享此 Mac 的屏幕画面", "Allow sharing this Mac’s screen", "Разрешить показ экрана этого Mac")},
description: GalleryStrings.texts[109]},
]: WorkbenchCard {
height: max(80px, permission-labels.preferred-height + 32px);
permission-labels := VerticalLayout {
@@ -2571,7 +2572,7 @@ export component MainWindow inherits Window {
height: 36px;
permission-action-metrics := Text {
visible: false; font-size: 13px; font-weight: 600;
text: GalleryStrings.tr("前往设置", "Open settings", "Настройки");
text: GalleryStrings.texts[110];
}
if !permission.granted: WorkbenchButton {
width: parent.width; height: parent.height; primary: true;
@@ -2590,7 +2591,7 @@ export component MainWindow inherits Window {
vertical-alignment: center; accessible-role: none;
}
Text {
text: GalleryStrings.tr("已授权", "Granted", "Разрешено");
text: GalleryStrings.texts[111];
font-size: 12px; color: WorkbenchStyle.success; vertical-alignment: center;
}
}
@@ -2601,9 +2602,7 @@ export component MainWindow inherits Window {
spacing: 12px;
SurfaceSeparator { height: 1px; outline-color: UiSurface.separator; }
Text {
text: GalleryStrings.tr("请在系统设置中允许 CrossDesk 访问。如系统提示重新打开应用,请按提示操作。",
"Allow CrossDesk access in System Settings. Reopen the app if macOS asks you to.",
"Разрешите доступ для CrossDesk в Системных настройках. Перезапустите приложение, если macOS попросит об этом.");
text: GalleryStrings.texts[112];
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
}
@@ -2645,7 +2644,7 @@ export component MainWindow inherits Window {
background: WorkbenchStyle.accent; border-radius: UiRadius.control;
animate x { duration: 180ms; easing: ease-in-out; }
}
for label[index] in [GalleryStrings.tr("常规", "General", "Общие"), GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub")]: Rectangle {
for label[index] in [GalleryStrings.texts[113], GalleryStrings.texts[54]]: Rectangle {
x: index == 1 ? parent.width / 2 + 1px : 2px;
y: 2px; width: (parent.width - 6px) / 2; height: parent.height - 4px;
accessible-role: button; accessible-label: label;
@@ -2683,14 +2682,14 @@ export component MainWindow inherits Window {
padding-right: settings-dialog.scrollbar-gutter; padding-bottom: 4px;
spacing: 14px;
SettingsGroup {
title: GalleryStrings.tr("界面", "Interface", "Интерфейс"); rows: 1;
title: GalleryStrings.texts[114]; rows: 1;
SettingsRow {
label: UiStrings.language; divider: false;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
SettingsSelect { x: parent.width - 192px; y: 4px; width: 180px; model: root.language-options; current-index <=> root.language-index; }
}
}
SettingsGroup {
title: GalleryStrings.tr("视频", "Video", "Видео");
title: GalleryStrings.texts[115];
rows: root.capture-method-visible ? 2 : 1;
if root.capture-method-visible: SettingsRow {
label: UiStrings.capture-method; enabled: !root.settings-session-active;
@@ -2702,9 +2701,9 @@ export component MainWindow inherits Window {
}
}
SettingsGroup {
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3;
title: GalleryStrings.texts[116]; rows: 3;
SettingsToggleRow {
label: GalleryStrings.tr("启用 IP 连接", "Enable IP connections", "Подключение по IP");
label: GalleryStrings.texts[117];
checked <=> root.ip-direct-enabled-draft;
}
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: !root.settings-session-active; }
@@ -2715,7 +2714,7 @@ export component MainWindow inherits Window {
self-host-edit := SettingsButton {
x: parent.width - self.width - 60px; y: 4px;
width: self-host-edit-metrics.preferred-width + 24px; height: 28px;
text: GalleryStrings.tr("编辑", "Edit", "Изменить");
text: GalleryStrings.texts[118];
enabled: !root.settings-session-active;
clicked => { root.self-host-settings-open = true; }
self-host-edit-metrics := Text {
@@ -2726,21 +2725,21 @@ export component MainWindow inherits Window {
}
}
Text {
text: GalleryStrings.tr("关闭 IP 连接将隐藏模式按钮,并断开 IP 连接会话。", "Disabling IP connections hides the mode button and ends IP sessions.", "Отключение подключений по IP скроет кнопку режима и завершит IP-сеансы.");
text: GalleryStrings.texts[119];
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
SettingsGroup {
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 3;
title: GalleryStrings.texts[120]; rows: 3;
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
SettingsToggleRow { label: UiStrings.privacy-local-unlock; checked <=> root.privacy-local-unlock-allowed; enabled: root.privacy-setting-available; }
SettingsToggleRow { label: UiStrings.lock-on-disconnect; checked <=> root.lock-on-disconnect-enabled; enabled: root.lock-on-disconnect-available; divider: false; }
}
Text {
text: GalleryStrings.tr("最后一个远程控制端断开后,自动锁定本机。", "Lock this device when the last remote controller disconnects.", "Блокировать это устройство после отключения последнего управляющего клиента.");
text: GalleryStrings.texts[121];
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
}
SettingsGroup {
title: GalleryStrings.tr("本地存储", "Local storage", "Локальное хранение"); rows: 2;
title: GalleryStrings.texts[122]; rows: 2;
SettingsToggleRow { label: UiStrings.save-remote-previews; checked <=> root.save-remote-previews; }
SettingsRow {
label: UiStrings.file-save-path; enabled: !root.settings-session-active; divider: false;
@@ -2748,7 +2747,7 @@ export component MainWindow inherits Window {
}
}
SettingsGroup {
title: GalleryStrings.tr("启动与后台", "Startup & background", "Запуск и фоновая работа"); rows: 2;
title: GalleryStrings.texts[123]; rows: 2;
if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
if root.autostart-system-managed: SettingsToggleRow {
label: UiStrings.autostart;
@@ -2784,12 +2783,12 @@ export component MainWindow inherits Window {
width: parent.width - 2 * settings-dialog.content-inset; height: 32px; spacing: 10px;
Rectangle { horizontal-stretch: 1; }
SettingsButton {
width: 80px; text: UiStrings.cancel;
width: max(80px, self.label-width); text: UiStrings.cancel;
clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; }
}
SettingsButton {
width: 92px; primary: true;
text: GalleryStrings.tr("保存", "Save", "Сохранить");
width: max(92px, self.label-width); primary: true;
text: GalleryStrings.texts[34];
clicked => { if root.save-settings() { root.settings-open = false; root.self-host-settings-open = false; } }
}
}
@@ -2807,7 +2806,7 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 24px; y: 18px; width: parent.width - 48px; height: 26px;
text: GalleryStrings.tr("设置开机自启动", "Set up auto start", "Настройка автозапуска");
text: GalleryStrings.texts[124];
font-size: 18px; font-weight: 600; color: WorkbenchStyle.text;
}
ScrollView {
@@ -2819,25 +2818,20 @@ export component MainWindow inherits Window {
width: parent.width; padding: 0px; spacing: 12px;
Text { text: root.autostart-status-text; color: WorkbenchStyle.accent; font-size: 13px; wrap: word-wrap; }
Text {
text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr(
"当前设备不支持开机自启动。", "This device does not support auto start.", "Это устройство не поддерживает автозапуск.") :
root.autostart-status == AutostartStatus.failed ? GalleryStrings.tr(
"无法读取系统状态。请刷新重试,或在系统设置中核对。", "Could not read the system status. Refresh to retry, or check System Settings.", "Не удалось прочитать состояние. Повторите попытку или проверьте настройки системы.") :
GalleryStrings.tr("请在系统设置中添加或移除本应用,返回后会自动刷新状态。", "Add or remove this app in system Settings. The status refreshes when you return.", "Добавьте или удалите приложение в настройках системы. После возвращения статус обновится.");
text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.texts[125] :
root.autostart-status == AutostartStatus.failed ? GalleryStrings.texts[126] :
GalleryStrings.texts[127];
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
if root.autostart-status != AutostartStatus.unsupported: Text {
text: GalleryStrings.tr(
"设置 → 应用和元服务 → 应用启动管理\n\n开启:点击“添加应用”,选择 CrossDesk,然后点击“完成”。\n\n关闭:在自启动列表中找到本应用,点击“移除”。",
"Settings → Apps & services → App startup management\n\nEnable: choose Add app, select CrossDesk, then choose Done.\n\nDisable: find this app in the startup list and choose Remove.",
"Настройки → Приложения и сервисы → Управление запуском\n\nВключить: добавьте CrossDesk в список и подтвердите выбор.\n\nВыключить: удалите приложение из списка автозапуска.");
text: GalleryStrings.texts[128];
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
}
}
SettingsButton {
x: 24px; y: parent.height - 48px; width: 112px;
text: GalleryStrings.tr("刷新状态", "Refresh status", "Обновить статус");
text: GalleryStrings.texts[129];
enabled: root.autostart-status != AutostartStatus.checking;
clicked => { root.refresh-autostart-status(); }
}
@@ -2882,7 +2876,7 @@ export component MainWindow inherits Window {
}
SurfaceSeparator { x: 18px; y: parent.height - 66px; width: parent.width - 36px; height: 1px; outline-color: UiSurface.separator; }
SettingsButton { x: parent.width - 114px; y: parent.height - 50px; width: 92px; primary: true; text: UiStrings.ok; clicked => { if root.save-self-hosted-settings() { root.self-host-settings-open = false; } } }
SettingsButton { x: parent.width - 204px; y: parent.height - 50px; width: 80px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
SettingsButton { x: parent.width - 124px - self.width; y: parent.height - 50px; width: max(80px, self.label-width); text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
}
}
@@ -11,6 +11,7 @@ export component SettingsButton inherits FlatSurface {
in property <bool> quiet: false;
in property <bool> align-left: false;
in property <bool> enabled: true;
out property <length> label-width: button-label.preferred-width + 2 * root.content-padding + (root.has-icon ? 24px : 0px);
private property <bool> has-icon: root.icon.width > 0;
private property <length> content-padding: root.has-icon ? 8px : 12px;
callback clicked();
+15 -1
View File
@@ -40,7 +40,21 @@ inline int ApplyMainWindowStrings(
int language_index) {
const int language =
localization::detail::ClampLanguageIndex(language_index);
window->global<ui::GalleryStrings>().set_language(language);
auto& gallery = window->global<ui::GalleryStrings>();
gallery.set_language(language);
const auto& retry = localization::Literal("请在 {0} 秒后重试", language);
const auto split = retry.find("{0}");
gallery.set_retry_prefix(Text(retry.substr(0, split)));
gallery.set_retry_suffix(Text(split == std::string::npos ? "" : retry.substr(split + 3)));
std::vector<slint::SharedString> literals;
for (const auto* key : localization::detail::kSlintLiterals)
literals.push_back(Text(localization::Literal(key, language)));
gallery.set_texts(std::make_shared<slint::VectorModel<slint::SharedString>>(literals));
std::vector<slint::SharedString> languages;
for (const auto& option : localization::GetSupportedLanguages())
languages.push_back(Text(option.display_name));
languages.push_back(Text(localization::Literal("跟随系统", language)));
window->set_language_options(std::make_shared<slint::VectorModel<slint::SharedString>>(languages));
auto& strings = window->global<ui::UiStrings>();
strings.set_window_minimize(Text(localization::window_minimize[language]));
strings.set_window_close(Text(localization::window_close[language]));
@@ -25,15 +25,15 @@ std::optional<bool> Boolean(const std::string& value) {
return std::nullopt;
}
const char* OnOff(bool value) { return value ? "on" : "off"; }
const char* languages[] = {"zh-CN", "en-US", "ru-RU"};
const char* languages[] = {"zh-CN", "en-US", "ru-RU", "zh-Hant", "ja", "ko", "de", "fr", "es", "pt-BR", "system"};
const char* turn_modes[] = {"off", "auto", "udp", "tcp"};
} // namespace
std::vector<ConsoleSetting> GetConsoleSettings(const ConfigCenter& config) {
const auto language = std::clamp(static_cast<int>(config.GetLanguage()), 0, 2);
const auto language = std::clamp(static_cast<int>(config.GetLanguage()), 0, 10);
const auto turn = std::clamp(static_cast<int>(config.GetTurnMode()), 0, 3);
return {
{"language", "console_setting_language", languages[language], "zh-CN / en-US / ru-RU"},
{"language", "console_setting_language", languages[language], "system / zh-CN / en-US / ru-RU / zh-Hant / ja / ko / de / fr / es / pt-BR"},
{"codec", "console_setting_codec", config.GetVideoEncodeFormat() == ConfigCenter::VIDEO_ENCODE_FORMAT::AV1 ? "av1" : "h264", "h264 / av1"},
{"turn", "console_setting_turn", turn_modes[turn], "off / auto / udp / tcp"},
{"self_hosted", "console_setting_self_hosted", OnOff(config.IsSelfHosted()), "on / off"},
@@ -70,6 +70,10 @@ SettingResult ApplyConsoleSetting(ConfigCenter& config, const std::string& key,
int language = value == "zh-cn" || value == "zh" || value == "0" ? 0
: value == "en-us" || value == "en" || value == "1" ? 1
: value == "ru-ru" || value == "ru" || value == "2" ? 2 : -1;
if (language < 0) {
for (int i = 0; i <= 10; ++i)
if (value == Lower(languages[i]) || value == std::to_string(i)) language = i;
}
if (language < 0) return result;
canonical = languages[language];
save = [&config, language] { return config.SetLanguage(static_cast<ConfigCenter::LANGUAGE>(language)); };