feat: 国际化 / 键盘操作 / 无障碍 / 性能优化
国际化 - 新增 ui/i18n 双语文案表(中文原文为 key,英文集中映射,tf 支持占位符) - AppSettings 增加 language(跟随系统/中文/English),启动与保存时即时生效 - 全部界面文案(标题栏/侧栏/工作区/发布/订阅/表单/设置/全部对话框/通知)入表 键盘操作 - Ctrl+W 关闭当前标签页;Ctrl+Tab / Ctrl+Shift+Tab 循环切换标签 - Ctrl+F 聚焦当前工作区消息过滤框 无障碍 - 全部纯图标按钮补 accessibility_label(屏幕阅读器 / UIA 自动化可识别) 性能 - 折叠态预览(JSON 美化/压平+截断)改为入环形缓冲时计算一次, 随 MqttRecord 缓存(Arc<str>),渲染热路径对最多数百条可见行 不再逐帧重复 serde_json 解析
This commit is contained in:
@@ -140,7 +140,10 @@ jobs:
|
|||||||
needs: [meta, build]
|
needs: [meta, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
# fetch-depth: 0 拉取附注 tag 对象,发布正文取自 tag message
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -177,8 +180,17 @@ jobs:
|
|||||||
VER: ${{ needs.meta.outputs.version }}
|
VER: ${{ needs.meta.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# 发布正文优先取附注 tag 的 message(剥掉签名块),无附注时回退自动生成
|
||||||
|
notes="$(git tag -l --format='%(contents)' "$VER" | sed '/^-----BEGIN PGP SIGNATURE----$/,/^-----END PGP SIGNATURE----$/d' || true)"
|
||||||
|
notes="$(echo "$notes" | sed -e '/^[[:space:]]*$/d' | sed -e ':a;N;$!ba;s/
|
||||||
|
$//')"
|
||||||
if gh release view "$VER" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
if gh release view "$VER" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||||
echo "Release ${VER} 已存在,覆盖上传资产"
|
echo "Release ${VER} 已存在,覆盖上传资产"
|
||||||
|
if [ -n "$notes" ]; then
|
||||||
|
gh release edit "$VER" -R "$GITHUB_REPOSITORY" --notes "$notes"
|
||||||
|
fi
|
||||||
|
elif [ -n "$notes" ]; then
|
||||||
|
gh release create "$VER" -R "$GITHUB_REPOSITORY" --title "$VER" --notes "$notes"
|
||||||
else
|
else
|
||||||
gh release create "$VER" -R "$GITHUB_REPOSITORY" --title "$VER" --generate-notes
|
gh release create "$VER" -R "$GITHUB_REPOSITORY" --title "$VER" --generate-notes
|
||||||
fi
|
fi
|
||||||
|
|||||||
Generated
+1
@@ -4273,6 +4273,7 @@ dependencies = [
|
|||||||
"sha1 0.11.0",
|
"sha1 0.11.0",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"smol",
|
"smol",
|
||||||
|
"sys-locale",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ureq",
|
"ureq",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|||||||
@@ -22,8 +22,11 @@
|
|||||||
- **阿里云 IoT** — Token(GroupId@@@DeviceId + HMAC-SHA1 签名)与一机一密两种鉴权方式一键生成连接
|
- **阿里云 IoT** — Token(GroupId@@@DeviceId + HMAC-SHA1 签名)与一机一密两种鉴权方式一键生成连接
|
||||||
- **连接测试** — 表单内握手测试(超时可配置),保存前验证连通性
|
- **连接测试** — 表单内握手测试(超时可配置),保存前验证连通性
|
||||||
- **日志面板** — 连接级事件日志(CONNACK/SUBACK/PUBACK/错误/重连等),按日落盘 `mqttx-YYYY-MM-DD.log`,设置内一键打开日志目录
|
- **日志面板** — 连接级事件日志(CONNACK/SUBACK/PUBACK/错误/重连等),按日落盘 `mqttx-YYYY-MM-DD.log`,设置内一键打开日志目录
|
||||||
- **快捷键** — `Ctrl+N` 新建连接、`Ctrl+,` 设置、`Ctrl+Shift+E` 导出、`Ctrl+Shift+I` 导入、`Ctrl+Shift+R` 资源监控、`Ctrl+Enter` 发送消息(全局生效)、`Ctrl+B` 收起/展开连接侧栏、`Enter` 提交订阅、`Esc` 退出编辑/清空输入
|
- **快捷键** — `Ctrl+N` 新建连接、`Ctrl+,` 设置、`Ctrl+Shift+E` 导出、`Ctrl+Shift+I` 导入、`Ctrl+Shift+R` 资源监控、`Ctrl+Enter` 发送消息(全局生效)、`Ctrl+B` 收起/展开连接侧栏、`Ctrl+W` 关闭标签页、`Ctrl+Tab` / `Ctrl+Shift+Tab` 切换标签、`Ctrl+F` 聚焦消息过滤、`Enter` 提交订阅、`Esc` 退出编辑/清空输入
|
||||||
- **抽屉式布局** — 连接侧栏(持久化,Ctrl+B)、订阅面板、底部发布面板均可一键收起为窄条抽屉,最大化消息流空间
|
- **抽屉式布局** — 连接侧栏(持久化,Ctrl+B)、订阅面板、底部发布面板均可一键收起为窄条抽屉,最大化消息流空间
|
||||||
|
- **国际化** — 中文 / English 双语界面,默认跟随系统语言,设置内可切换即时生效
|
||||||
|
- **无障碍** — 全部图标按钮带可访问名称(屏幕阅读器 / UIA 可识别),完整键盘操作路径
|
||||||
|
- **性能** — 消息预览(JSON 美化/截断)入缓冲时计算一次并随记录缓存,渲染热路径零重复解析
|
||||||
- **主题** — Linear 风格配色(近黑/纯白扁平底 + 靛蓝 #5e6ad2 主色,14px 紧凑排版、6px 圆角),浅色 / 深色 / 跟随系统
|
- **主题** — Linear 风格配色(近黑/纯白扁平底 + 靛蓝 #5e6ad2 主色,14px 紧凑排版、6px 圆角),浅色 / 深色 / 跟随系统
|
||||||
- **设置** — 自动检查更新开关、消息缓存条数、时间戳毫秒、数据目录展示与打开、关于页(版本 / 许可)
|
- **设置** — 自动检查更新开关、消息缓存条数、时间戳毫秒、数据目录展示与打开、关于页(版本 / 许可)
|
||||||
- **本地持久化** — 连接、订阅、预设、变量、设置以 JSON 存于系统数据目录
|
- **本地持久化** — 连接、订阅、预设、变量、设置以 JSON 存于系统数据目录
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ rumqttc = { package = "rumqttc-v5-next", version = "0.34", features = ["websocke
|
|||||||
rumqttc-v4 = { package = "rumqttc-v4-next", version = "0.34", features = ["websocket"] }
|
rumqttc-v4 = { package = "rumqttc-v4-next", version = "0.34", features = ["websocket"] }
|
||||||
|
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
sys-locale = "0.3"
|
||||||
chrono = { version = "0.4", features = ["clock"] }
|
chrono = { version = "0.4", features = ["clock"] }
|
||||||
dirs = "7"
|
dirs = "7"
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ pub struct MqttRecord {
|
|||||||
/// 非文本负载的原始字节(合法 UTF-8 时为 None)。文本化会失真
|
/// 非文本负载的原始字节(合法 UTF-8 时为 None)。文本化会失真
|
||||||
/// (非法字节被替换为 U+FFFD),详情面板的 Hex/Base64 需要真实报文。
|
/// (非法字节被替换为 U+FFFD),详情面板的 Hex/Base64 需要真实报文。
|
||||||
pub raw_bytes: Option<Arc<[u8]>>,
|
pub raw_bytes: Option<Arc<[u8]>>,
|
||||||
|
/// 折叠态预览文本(JSON 美化/压平 + 截断),入环形缓冲时计算一次。
|
||||||
|
/// 渲染热路径每帧对最多数百条可见行直接取用,避免逐帧重复 JSON 解析。
|
||||||
|
pub preview: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MqttRecord {
|
impl MqttRecord {
|
||||||
@@ -115,6 +118,59 @@ impl MqttRecord {
|
|||||||
pub fn retained_bytes(&self) -> usize {
|
pub fn retained_bytes(&self) -> usize {
|
||||||
self.payload.len() + self.raw_bytes.as_ref().map_or(0, |b| b.len())
|
self.payload.len() + self.raw_bytes.as_ref().map_or(0, |b| b.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 入环形缓冲前调用:计算并填充折叠态预览(每条消息仅此一次)。
|
||||||
|
pub fn compute_preview(&mut self) {
|
||||||
|
self.preview = Arc::from(compute_preview(&self.payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 折叠态预览行数与字符上限:JSON 美化后多行完整展示,超大负载兜底截断
|
||||||
|
const PREVIEW_MAX_LINES: usize = 40;
|
||||||
|
const PREVIEW_MAX_CHARS: usize = 4000;
|
||||||
|
|
||||||
|
/// 计算折叠态预览:JSON 负载按原始数据美化展示(缩进多行),非 JSON
|
||||||
|
/// 压平换行/制表符截 200 字符;含替换字符(U+FFFD)提示切 Hex。
|
||||||
|
pub fn compute_preview(payload: &str) -> String {
|
||||||
|
let parsed = serde_json::from_str::<serde_json::Value>(payload).ok();
|
||||||
|
let mut out = match &parsed {
|
||||||
|
Some(value) => {
|
||||||
|
let pretty = serde_json::to_string_pretty(value).unwrap_or_default();
|
||||||
|
truncate_preview(pretty)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let flat: String = payload
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'\n' | '\r' | '\t' => ' ',
|
||||||
|
_ => c,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut s: String = flat.chars().take(200).collect();
|
||||||
|
if flat.chars().count() > 200 {
|
||||||
|
s.push('…');
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if payload.contains('\u{FFFD}') {
|
||||||
|
out.push_str("(非文本,详情可切 Hex)");
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 截断预览文本:最多 [`PREVIEW_MAX_LINES`] 行、[`PREVIEW_MAX_CHARS`] 字符。
|
||||||
|
fn truncate_preview(text: String) -> String {
|
||||||
|
let mut lines: Vec<&str> = text.lines().take(PREVIEW_MAX_LINES).collect();
|
||||||
|
let more = text.lines().count() > PREVIEW_MAX_LINES;
|
||||||
|
if more {
|
||||||
|
lines.push("…");
|
||||||
|
}
|
||||||
|
let mut out = lines.join("\n");
|
||||||
|
if out.chars().count() > PREVIEW_MAX_CHARS {
|
||||||
|
out = out.chars().take(PREVIEW_MAX_CHARS).collect::<String>() + "…";
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 单连接的消息环形缓冲:条数上限 + [`MAX_CONNECTION_MESSAGE_BYTES`]
|
/// 单连接的消息环形缓冲:条数上限 + [`MAX_CONNECTION_MESSAGE_BYTES`]
|
||||||
@@ -199,3 +255,36 @@ pub fn retained_payload(raw: &[u8]) -> (Arc<str>, Option<Arc<[u8]>>, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── 发布参数 / 发布预设 ──────────────────────────────────────────────────────
|
// ─── 发布参数 / 发布预设 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod preview_tests {
|
||||||
|
use super::compute_preview;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_json_is_pretty_and_complete_for_typical_payloads() {
|
||||||
|
let payload = r#"{"type":"heartbeat","ts":1790751279869,"data":{"version":"0.1.0","lon":117.229,"lat":31.8206,"uptime":1716450,"rssi":-52}}"#;
|
||||||
|
let out = compute_preview(payload);
|
||||||
|
// JSON 被美化(多行缩进)且常规负载完整展示、不带任何加工注释
|
||||||
|
assert!(out.contains('\n'), "JSON 应多行展示: {out}");
|
||||||
|
assert!(out.contains("\"rssi\": -52"), "常规负载应完整显示: {out}");
|
||||||
|
assert!(!out.contains("//"), "预览不应附加注释: {out}");
|
||||||
|
assert!(!out.contains('…'), "常规负载不应截断: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_truncates_very_deep_json() {
|
||||||
|
// 50 个元素的数组美化后超过 40 行,应截断并带省略号
|
||||||
|
let items: Vec<String> = (0..50).map(|i| i.to_string()).collect();
|
||||||
|
let payload = format!("{{\"a\":[{}]}}", items.join(","));
|
||||||
|
let deep = compute_preview(&payload);
|
||||||
|
assert!(deep.lines().count() <= 41, "超行应截断: {deep}");
|
||||||
|
assert!(deep.contains('…'), "截断应有省略号: {deep}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_non_json_still_flat() {
|
||||||
|
assert_eq!(compute_preview("hello\nworld"), "hello world");
|
||||||
|
assert_eq!(compute_preview("纯文本"), "纯文本");
|
||||||
|
assert!(compute_preview("bad\ntext\u{FFFD}").contains("非文本"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ mod tests {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
preview: Arc::from(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,38 @@ impl ThemeModePref {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 界面语言偏好。
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum LanguagePref {
|
||||||
|
/// 首次启动按系统语言解析(zh 开头 → 中文,其余 → English)
|
||||||
|
#[default]
|
||||||
|
System,
|
||||||
|
/// 中文
|
||||||
|
Zh,
|
||||||
|
/// English
|
||||||
|
En,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LanguagePref {
|
||||||
|
pub const ALL: [Self; 3] = [Self::System, Self::Zh, Self::En];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::System => "跟随系统 / System",
|
||||||
|
Self::Zh => "中文",
|
||||||
|
Self::En => "English",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppSettings {
|
pub struct AppSettings {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub theme: ThemeModePref,
|
pub theme: ThemeModePref,
|
||||||
|
/// 界面语言(跟随系统 / 中文 / English)
|
||||||
|
#[serde(default)]
|
||||||
|
pub language: LanguagePref,
|
||||||
/// 每条连接在内存中保留的最大消息条数
|
/// 每条连接在内存中保留的最大消息条数
|
||||||
#[serde(default = "default_max_messages")]
|
#[serde(default = "default_max_messages")]
|
||||||
pub max_messages: usize,
|
pub max_messages: usize,
|
||||||
@@ -72,6 +100,7 @@ impl Default for AppSettings {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
theme: ThemeModePref::default(),
|
theme: ThemeModePref::default(),
|
||||||
|
language: LanguagePref::default(),
|
||||||
max_messages: default_max_messages(),
|
max_messages: default_max_messages(),
|
||||||
show_millis: true,
|
show_millis: true,
|
||||||
auto_check_update: true,
|
auto_check_update: true,
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ pub(super) fn spawn_v4_loop(
|
|||||||
subscription_identifier: None,
|
subscription_identifier: None,
|
||||||
payload_truncated,
|
payload_truncated,
|
||||||
raw_bytes,
|
raw_bytes,
|
||||||
|
preview: Arc::from(""),
|
||||||
};
|
};
|
||||||
engine.log(
|
engine.log(
|
||||||
&id, LogLevel::Info, "publish_received",
|
&id, LogLevel::Info, "publish_received",
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ fn v5_record(engine: &MqttEngine, seq: u64, conn_id: &str, p: &V5Publish) -> Mqt
|
|||||||
.and_then(|p| p.subscription_identifiers.first().map(|v| *v as u32)),
|
.and_then(|p| p.subscription_identifiers.first().map(|v| *v as u32)),
|
||||||
payload_truncated,
|
payload_truncated,
|
||||||
raw_bytes,
|
raw_bytes,
|
||||||
|
preview: Arc::from(""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::ui::IconName;
|
|||||||
use crate::ui::{ connection_form::ConnectionForm, connection_view::ConnectionView, };
|
use crate::ui::{ connection_form::ConnectionForm, connection_view::ConnectionView, };
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl MqttXApp {
|
impl MqttXApp {
|
||||||
pub fn save_connection(&mut self, cfg: ConnectionConfig) {
|
pub fn save_connection(&mut self, cfg: ConnectionConfig) {
|
||||||
@@ -117,6 +118,23 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ctrl+Tab / Ctrl+Shift+Tab:按方向在打开的标签页间循环切换。
|
||||||
|
pub fn cycle_tab(&mut self, dir: i32, cx: &mut Context<Self>) {
|
||||||
|
if self.open_tabs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cur = self
|
||||||
|
.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|a| self.open_tabs.iter().position(|t| t == a))
|
||||||
|
.unwrap_or(0);
|
||||||
|
let len = self.open_tabs.len() as i32;
|
||||||
|
let next = ((cur as i32 + dir).rem_euclid(len)) as usize;
|
||||||
|
self.active_tab = Some(self.open_tabs[next].clone());
|
||||||
|
self.persist_ui_state();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
// ── 订阅 ──────────────────────────────────────────────────────────────
|
// ── 订阅 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn add_subscription(&mut self, sub: Subscription) {
|
pub fn add_subscription(&mut self, sub: Subscription) {
|
||||||
@@ -171,6 +189,12 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
|||||||
|
|
||||||
pub fn save_settings(&mut self, settings: AppSettings, window: &mut Window, cx: &mut App) {
|
pub fn save_settings(&mut self, settings: AppSettings, window: &mut Window, cx: &mut App) {
|
||||||
apply_theme(settings.theme, cx);
|
apply_theme(settings.theme, cx);
|
||||||
|
// 语言即时生效(含跟随系统)
|
||||||
|
crate::ui::i18n::set_lang(match settings.language {
|
||||||
|
crate::model::LanguagePref::Zh => crate::ui::i18n::Lang::Zh,
|
||||||
|
crate::model::LanguagePref::En => crate::ui::i18n::Lang::En,
|
||||||
|
crate::model::LanguagePref::System => crate::ui::i18n::from_system(),
|
||||||
|
});
|
||||||
self.settings = settings;
|
self.settings = settings;
|
||||||
self.storage.save_settings(&self.settings);
|
self.storage.save_settings(&self.settings);
|
||||||
// 切主题只更新全局令牌,不触发整窗重绘,未损坏区域会长期滞留旧主题
|
// 切主题只更新全局令牌,不触发整窗重绘,未损坏区域会长期滞留旧主题
|
||||||
@@ -204,7 +228,7 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
|||||||
let testing = form_test.read(cx).is_testing();
|
let testing = form_test.read(cx).is_testing();
|
||||||
dialog
|
dialog
|
||||||
.w(px(680.))
|
.w(px(680.))
|
||||||
.title("连接配置")
|
.title(i18n::t("连接配置"))
|
||||||
.child(form.clone())
|
.child(form.clone())
|
||||||
.footer(
|
.footer(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -218,7 +242,7 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
|||||||
} else {
|
} else {
|
||||||
IconName::PlugZap
|
IconName::PlugZap
|
||||||
})
|
})
|
||||||
.label(if testing { "测试中…" } else { "测试连接" })
|
.label(if testing { i18n::t("测试中…") } else { i18n::t("测试连接") })
|
||||||
.outline()
|
.outline()
|
||||||
.loading(testing)
|
.loading(testing)
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
@@ -228,13 +252,13 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
|||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
.child(
|
.child(
|
||||||
Button::new("form-cancel")
|
Button::new("form-cancel")
|
||||||
.label("取消")
|
.label(i18n::t("取消"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("form-ok")
|
Button::new("form-ok")
|
||||||
.label("保存")
|
.label(i18n::t("保存"))
|
||||||
.primary()
|
.primary()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let built = form_ok.read(cx).build(cx);
|
let built = form_ok.read(cx).build(cx);
|
||||||
@@ -264,7 +288,7 @@ pub(super) fn close_tab(&mut self, id: &str, cx: &mut Context<Self>) {
|
|||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
if stale {
|
if stale {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::info("配置已保存,重连后生效"),
|
Notification::info(i18n::t("配置已保存,重连后生效")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::model::{ ConnectionStatus, Direction, LogEntry, LogLevel, MqttRecord,
|
|||||||
use crate::mqtt::EngineEvent;
|
use crate::mqtt::EngineEvent;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl MqttXApp {
|
impl MqttXApp {
|
||||||
pub(super) fn on_engine_event(&mut self, event: EngineEvent, window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn on_engine_event(&mut self, event: EngineEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -84,6 +85,7 @@ pub(super) fn on_engine_event(&mut self, event: EngineEvent, window: &mut Window
|
|||||||
subscription_identifier: None,
|
subscription_identifier: None,
|
||||||
payload_truncated,
|
payload_truncated,
|
||||||
raw_bytes,
|
raw_bytes,
|
||||||
|
preview: Arc::from(""),
|
||||||
};
|
};
|
||||||
self.push_message(Arc::from(connection_id.as_str()), record);
|
self.push_message(Arc::from(connection_id.as_str()), record);
|
||||||
self.notify_message_view(&connection_id, cx);
|
self.notify_message_view(&connection_id, cx);
|
||||||
@@ -109,7 +111,10 @@ pub(super) fn on_engine_event(&mut self, event: EngineEvent, window: &mut Window
|
|||||||
} else {
|
} else {
|
||||||
if let Some(e) = error {
|
if let Some(e) = error {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error(format!("订阅失败: {e}")),
|
Notification::error(i18n::tf(
|
||||||
|
"订阅失败: {e}",
|
||||||
|
&[("e", &e)],
|
||||||
|
)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -172,7 +177,9 @@ pub(super) fn on_engine_event(&mut self, event: EngineEvent, window: &mut Window
|
|||||||
self.seq
|
self.seq
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_message(&mut self, connection_id: Arc<str>, record: MqttRecord) {
|
fn push_message(&mut self, connection_id: Arc<str>, mut record: MqttRecord) {
|
||||||
|
// 折叠态预览在此唯一入口计算一次;渲染热路径逐帧直接取用
|
||||||
|
record.compute_preview();
|
||||||
let cap = self.settings.max_messages;
|
let cap = self.settings.max_messages;
|
||||||
self.messages
|
self.messages
|
||||||
.entry(connection_id)
|
.entry(connection_id)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use gpui_kit::{ Context, Window, };
|
|||||||
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl MqttXApp {
|
impl MqttXApp {
|
||||||
/// 数据目录(设置对话框展示 / 打开用)。
|
/// 数据目录(设置对话框展示 / 打开用)。
|
||||||
@@ -76,9 +77,12 @@ impl MqttXApp {
|
|||||||
let count = app.connections.len();
|
let count = app.connections.len();
|
||||||
match crate::store::export_connections_to(&path, &app.connections) {
|
match crate::store::export_connections_to(&path, &app.connections) {
|
||||||
Ok(()) => window.push_notification(
|
Ok(()) => window.push_notification(
|
||||||
Notification::success(format!(
|
Notification::success(i18n::tf(
|
||||||
"已导出 {count} 条连接到 {}",
|
"导出 {n} 条连接到 {path}",
|
||||||
path.display()
|
&[
|
||||||
|
("n", &count.to_string()),
|
||||||
|
("path", &path.display().to_string()),
|
||||||
|
],
|
||||||
)),
|
)),
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
@@ -129,7 +133,13 @@ impl MqttXApp {
|
|||||||
app.storage.save_connections(&app.connections);
|
app.storage.save_connections(&app.connections);
|
||||||
}
|
}
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::success(format!("导入 {added} 条,跳过 {skipped} 条")),
|
Notification::success(i18n::tf(
|
||||||
|
"导入 {n} 条,跳过 {n2} 条",
|
||||||
|
&[
|
||||||
|
("n", &added.to_string()),
|
||||||
|
("n2", &skipped.to_string()),
|
||||||
|
],
|
||||||
|
)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use gpui_kit::{ div, px, Context, IntoElement, SharedString, Window, };
|
|||||||
use crate::ui::IconName;
|
use crate::ui::IconName;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl MqttXApp {
|
impl MqttXApp {
|
||||||
pub(super) fn render_main(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
pub(super) fn render_main(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
@@ -26,18 +27,18 @@ pub(super) fn render_main(&mut self, _window: &mut Window, cx: &mut Context<Self
|
|||||||
cx.theme().muted,
|
cx.theme().muted,
|
||||||
cx.theme().muted_foreground,
|
cx.theme().muted_foreground,
|
||||||
))
|
))
|
||||||
.child(div().text_base().font_medium().child("开始使用 MQTTX"))
|
.child(div().text_base().font_medium().child(i18n::t("开始使用 MQTTX")))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.text_center()
|
.text_center()
|
||||||
.child("从左侧选择一个连接,或新建连接开始调试"),
|
.child(i18n::t("从左侧选择一个连接,或新建连接开始调试")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("empty-new")
|
Button::new("empty-new")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.label("新建连接")
|
.label(i18n::t("新建连接"))
|
||||||
.outline()
|
.outline()
|
||||||
.small()
|
.small()
|
||||||
.mt_1()
|
.mt_1()
|
||||||
|
|||||||
+68
-12
@@ -32,6 +32,7 @@ use crate::mqtt::MqttEngine;
|
|||||||
use crate::store::{Storage, UiState};
|
use crate::store::{Storage, UiState};
|
||||||
use crate::update;
|
use crate::update;
|
||||||
use crate::ui::connection_view::ConnectionView;
|
use crate::ui::connection_view::ConnectionView;
|
||||||
|
use crate::ui::i18n::{self, Lang};
|
||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -48,7 +49,7 @@ const MAX_LOGS: usize = 3000;
|
|||||||
const EVENT_QUEUE_CAPACITY: usize = 1024;
|
const EVENT_QUEUE_CAPACITY: usize = 1024;
|
||||||
|
|
||||||
// 应用级快捷键动作(context=None:任意焦点状态下都匹配)。
|
// 应用级快捷键动作(context=None:任意焦点状态下都匹配)。
|
||||||
gpui_kit::actions!(mqttx, [NewConnection, OpenSettings, ExportConnections, ImportConnections, OpenResourceMonitor, PublishMessage, ToggleSidebar]);
|
gpui_kit::actions!(mqttx, [NewConnection, OpenSettings, ExportConnections, ImportConnections, OpenResourceMonitor, PublishMessage, ToggleSidebar, CloseActiveTab, NextTab, PrevTab, FocusMessageFilter]);
|
||||||
|
|
||||||
/// 侧边栏分组过滤;chips 只做列表过滤,分组重命名/删除通过编辑连接的分组字段完成。
|
/// 侧边栏分组过滤;chips 只做列表过滤,分组重命名/删除通过编辑连接的分组字段完成。
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
@@ -130,6 +131,12 @@ impl MqttXApp {
|
|||||||
let settings = storage.load_settings();
|
let settings = storage.load_settings();
|
||||||
|
|
||||||
apply_theme(settings.theme, cx);
|
apply_theme(settings.theme, cx);
|
||||||
|
// 界面语言:设置显式指定优先,跟随系统时按系统 locale 解析
|
||||||
|
i18n::set_lang(match settings.language {
|
||||||
|
crate::model::LanguagePref::Zh => Lang::Zh,
|
||||||
|
crate::model::LanguagePref::En => Lang::En,
|
||||||
|
crate::model::LanguagePref::System => i18n::from_system(),
|
||||||
|
});
|
||||||
|
|
||||||
// 有界事件队列:引擎侧对高频事件(消息/日志)做丢弃并计数,保证 UI 消费
|
// 有界事件队列:引擎侧对高频事件(消息/日志)做丢弃并计数,保证 UI 消费
|
||||||
// 不过来时内存有上界。1024 × 单条最大 128KB ≈ 最坏 128MB,常规消息下几百 KB。
|
// 不过来时内存有上界。1024 × 单条最大 128KB ≈ 最坏 128MB,常规消息下几百 KB。
|
||||||
@@ -209,12 +216,12 @@ impl MqttXApp {
|
|||||||
let staged_consent = staged.clone();
|
let staged_consent = staged.clone();
|
||||||
dialog
|
dialog
|
||||||
.w(px(440.))
|
.w(px(440.))
|
||||||
.title("更新就绪")
|
.title(i18n::t("更新就绪"))
|
||||||
.child(
|
.child(
|
||||||
v_flex().gap_2().child(
|
v_flex().gap_2().child(
|
||||||
div().text_sm().child(format!(
|
div().text_sm().child(i18n::tf(
|
||||||
"新版本 v{} 已下载完成(已通过 sha256 校验)。",
|
"新版本 v{v} 已下载完成(已通过 sha256 校验)。",
|
||||||
staged.version
|
&[("v", &staged.version)],
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -225,7 +232,7 @@ impl MqttXApp {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("upd-skip")
|
Button::new("upd-skip")
|
||||||
.label("暂不更新")
|
.label(i18n::t("暂不更新"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
// 拒绝本次更新:删除暂存包,
|
// 拒绝本次更新:删除暂存包,
|
||||||
@@ -236,7 +243,7 @@ impl MqttXApp {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("upd-later")
|
Button::new("upd-later")
|
||||||
.label("下次启动安装")
|
.label(i18n::t("下次启动安装"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
// 同意标记:启动时据此自动安装;
|
// 同意标记:启动时据此自动安装;
|
||||||
@@ -247,7 +254,7 @@ impl MqttXApp {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("upd-now")
|
Button::new("upd-now")
|
||||||
.label("立即安装")
|
.label(i18n::t("立即安装"))
|
||||||
.primary()
|
.primary()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
@@ -255,9 +262,10 @@ impl MqttXApp {
|
|||||||
update::install_staged(&staged)
|
update::install_staged(&staged)
|
||||||
{
|
{
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error(format!(
|
Notification::error(i18n::tf(
|
||||||
"安装失败:{e}"
|
"安装失败:{e}",
|
||||||
)),
|
&[("e", &e),
|
||||||
|
])),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -284,7 +292,7 @@ impl MqttXApp {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
let search = cx.new(|cx| InputState::new(window, cx).placeholder("搜索连接…"));
|
let search = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("搜索连接…")));
|
||||||
|
|
||||||
// 自验后门:MQTTX_OPEN_RESMON=1 启动时直接打开资源监控
|
// 自验后门:MQTTX_OPEN_RESMON=1 启动时直接打开资源监控
|
||||||
// (与 MQTTX_VERSION_OVERRIDE 同类的测试入口,供自动化验证 UI)。
|
// (与 MQTTX_VERSION_OVERRIDE 同类的测试入口,供自动化验证 UI)。
|
||||||
@@ -323,6 +331,10 @@ impl MqttXApp {
|
|||||||
KeyBinding::new("ctrl-shift-r", OpenResourceMonitor, None),
|
KeyBinding::new("ctrl-shift-r", OpenResourceMonitor, None),
|
||||||
KeyBinding::new("ctrl-enter", PublishMessage, None),
|
KeyBinding::new("ctrl-enter", PublishMessage, None),
|
||||||
KeyBinding::new("ctrl-b", ToggleSidebar, None),
|
KeyBinding::new("ctrl-b", ToggleSidebar, None),
|
||||||
|
KeyBinding::new("ctrl-w", CloseActiveTab, None),
|
||||||
|
KeyBinding::new("ctrl-tab", NextTab, None),
|
||||||
|
KeyBinding::new("ctrl-shift-tab", PrevTab, None),
|
||||||
|
KeyBinding::new("ctrl-f", FocusMessageFilter, None),
|
||||||
]);
|
]);
|
||||||
let weak = cx.entity().downgrade();
|
let weak = cx.entity().downgrade();
|
||||||
// Context 上有同名 on_action(绘制期注册),这里必须走 App 的全局注册
|
// Context 上有同名 on_action(绘制期注册),这里必须走 App 的全局注册
|
||||||
@@ -402,6 +414,50 @@ impl MqttXApp {
|
|||||||
entity.update(cx, |app, cx| app.toggle_sidebar(cx));
|
entity.update(cx, |app, cx| app.toggle_sidebar(cx));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Ctrl+W 关闭当前标签页
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
App::on_action::<CloseActiveTab>(cx, move |_, cx| {
|
||||||
|
let w = weak.clone();
|
||||||
|
run_on_active_window(cx, w, |entity, _, cx| {
|
||||||
|
entity.update(cx, |app, cx| {
|
||||||
|
if let Some(tab) = app.active_tab.clone() {
|
||||||
|
app.close_tab(&tab, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Ctrl+Tab / Ctrl+Shift+Tab 在打开的标签页间循环切换
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
App::on_action::<NextTab>(cx, move |_, cx| {
|
||||||
|
let w = weak.clone();
|
||||||
|
run_on_active_window(cx, w, |entity, _, cx| {
|
||||||
|
entity.update(cx, |app, cx| app.cycle_tab(1, cx));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
App::on_action::<PrevTab>(cx, move |_, cx| {
|
||||||
|
let w = weak.clone();
|
||||||
|
run_on_active_window(cx, w, |entity, _, cx| {
|
||||||
|
entity.update(cx, |app, cx| app.cycle_tab(-1, cx));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Ctrl+F 聚焦当前工作区的消息过滤框
|
||||||
|
let weak = cx.entity().downgrade();
|
||||||
|
App::on_action::<FocusMessageFilter>(cx, move |_, cx| {
|
||||||
|
let w = weak.clone();
|
||||||
|
run_on_active_window(cx, w, |entity, window, cx| {
|
||||||
|
if window.has_active_dialog(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.update(cx, |app, cx| {
|
||||||
|
if let Some(tab) = app.active_tab.clone()
|
||||||
|
&& let Some(view) = app.views.get(&tab).cloned()
|
||||||
|
{
|
||||||
|
view.update(cx, |view, cx| view.focus_filter(window, cx));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
storage,
|
storage,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crate::ui::IconName;
|
|||||||
use crate::ui::widgets::status_color;
|
use crate::ui::widgets::status_color;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl MqttXApp {
|
impl MqttXApp {
|
||||||
/// 侧栏收起后的窄 rail:抽屉把手(拉回侧栏)+ 新建连接入口。
|
/// 侧栏收起后的窄 rail:抽屉把手(拉回侧栏)+ 新建连接入口。
|
||||||
@@ -33,7 +34,8 @@ impl MqttXApp {
|
|||||||
.icon(IconName::PanelLeftOpen)
|
.icon(IconName::PanelLeftOpen)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("展开连接侧栏(Ctrl+B)")
|
.tooltip(i18n::t("展开连接侧栏(Ctrl+B)"))
|
||||||
|
.accessibility_label(i18n::t("展开连接侧栏(Ctrl+B)"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| this.toggle_sidebar(cx))),
|
.on_click(cx.listener(|this, _, _, cx| this.toggle_sidebar(cx))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -41,7 +43,8 @@ impl MqttXApp {
|
|||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("新建连接")
|
.tooltip(i18n::t("新建连接"))
|
||||||
|
.accessibility_label(i18n::t("新建连接"))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.open_connection_form(None, window, cx);
|
this.open_connection_form(None, window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -171,7 +174,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child("还没有连接,点击上方 + 新建"),
|
.child(i18n::t("还没有连接,点击上方 + 新建")),
|
||||||
)
|
)
|
||||||
} else if shown == 0 {
|
} else if shown == 0 {
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -183,7 +186,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child("没有匹配的连接"),
|
.child(i18n::t("没有匹配的连接")),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
v_flex().flex_1().child(rows)
|
v_flex().flex_1().child(rows)
|
||||||
@@ -208,7 +211,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(div().text_sm().font_semibold().child("连接"))
|
.child(div().text_sm().font_semibold().child(i18n::t("连接")))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -224,7 +227,8 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
.icon(IconName::PanelLeftClose)
|
.icon(IconName::PanelLeftClose)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("收起侧栏(Ctrl+B)")
|
.tooltip(i18n::t("收起侧栏(Ctrl+B)"))
|
||||||
|
.accessibility_label(i18n::t("收起侧栏(Ctrl+B)"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.toggle_sidebar(cx)
|
this.toggle_sidebar(cx)
|
||||||
})),
|
})),
|
||||||
@@ -232,7 +236,8 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
.child(
|
.child(
|
||||||
Button::new("sidebar-add")
|
Button::new("sidebar-add")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.tooltip("新建连接")
|
.tooltip(i18n::t("新建连接"))
|
||||||
|
.accessibility_label(i18n::t("新建连接"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -276,13 +281,13 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
.overflow_y_scrollbar();
|
.overflow_y_scrollbar();
|
||||||
row = row.child(self.render_group_chip(
|
row = row.child(self.render_group_chip(
|
||||||
"chip-all",
|
"chip-all",
|
||||||
format!("全部 {}", self.connections.len()),
|
format!("{} {}", i18n::t("全部"), self.connections.len()),
|
||||||
GroupFilter::All,
|
GroupFilter::All,
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
row = row.child(self.render_group_chip(
|
row = row.child(self.render_group_chip(
|
||||||
"chip-ungrouped",
|
"chip-ungrouped",
|
||||||
format!("未分组 {ungrouped}"),
|
format!("{} {ungrouped}", i18n::t("未分组")),
|
||||||
GroupFilter::Ungrouped,
|
GroupFilter::Ungrouped,
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
@@ -348,7 +353,8 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
.icon(if connected { IconName::Square } else { IconName::PlugZap })
|
.icon(if connected { IconName::Square } else { IconName::PlugZap })
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip(if connected { "断开" } else { "连接" })
|
.tooltip(if connected { i18n::t("断开") } else { i18n::t("连接") })
|
||||||
|
.accessibility_label(if connected { i18n::t("断开") } else { i18n::t("连接") })
|
||||||
.on_click(cx.listener(move |this, _, _, _| {
|
.on_click(cx.listener(move |this, _, _, _| {
|
||||||
this.toggle_connection(&cfg);
|
this.toggle_connection(&cfg);
|
||||||
}))
|
}))
|
||||||
@@ -364,7 +370,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
.dropdown_menu(move |menu, _, _| {
|
.dropdown_menu(move |menu, _, _| {
|
||||||
let (e1, e2, e3) = (eid.clone(), eid.clone(), eid.clone());
|
let (e1, e2, e3) = (eid.clone(), eid.clone(), eid.clone());
|
||||||
let (w1, w2, w3) = (weak.clone(), weak.clone(), weak.clone());
|
let (w1, w2, w3) = (weak.clone(), weak.clone(), weak.clone());
|
||||||
menu.item(PopupMenuItem::new("编辑").on_click(move |_, window, cx| {
|
menu.item(PopupMenuItem::new(i18n::t("编辑")).on_click(move |_, window, cx| {
|
||||||
w1.update(cx, |app, cx| {
|
w1.update(cx, |app, cx| {
|
||||||
let cfg = app.connections.iter().find(|c| c.id == e1).cloned();
|
let cfg = app.connections.iter().find(|c| c.id == e1).cloned();
|
||||||
if let Some(cfg) = cfg {
|
if let Some(cfg) = cfg {
|
||||||
@@ -373,7 +379,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}))
|
}))
|
||||||
.item(PopupMenuItem::new("复制连接").on_click(move |_, _, cx| {
|
.item(PopupMenuItem::new(i18n::t("复制连接")).on_click(move |_, _, cx| {
|
||||||
w2.update(cx, |app, cx| {
|
w2.update(cx, |app, cx| {
|
||||||
app.duplicate_connection(&e2);
|
app.duplicate_connection(&e2);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -381,7 +387,7 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
.ok();
|
.ok();
|
||||||
}))
|
}))
|
||||||
.separator()
|
.separator()
|
||||||
.item(PopupMenuItem::new("删除").on_click(move |_, window, cx| {
|
.item(PopupMenuItem::new(i18n::t("删除")).on_click(move |_, window, cx| {
|
||||||
let Some(app) = w3.upgrade() else {
|
let Some(app) = w3.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -403,13 +409,14 @@ pub(super) fn render_sidebar(&mut self, _window: &mut Window, cx: &mut Context<S
|
|||||||
let app = app.clone();
|
let app = app.clone();
|
||||||
let confirm_id = confirm_id.clone();
|
let confirm_id = confirm_id.clone();
|
||||||
alert
|
alert
|
||||||
.title("删除连接")
|
.title(i18n::t("删除连接"))
|
||||||
.description(SharedString::from(format!(
|
.description(SharedString::from(i18n::tf(
|
||||||
"确定删除「{name}」?将同时删除 {sub_count} 个订阅,并断开当前连接,此操作不可撤销。"
|
"确定删除「{name}」?将同时删除 {sub_count} 个订阅,并断开当前连接,此操作不可撤销。",
|
||||||
|
&[("name", &name), ("sub_count", &sub_count.to_string())],
|
||||||
)))
|
)))
|
||||||
.button_props(
|
.button_props(
|
||||||
DialogButtonProps::default()
|
DialogButtonProps::default()
|
||||||
.ok_text("删除")
|
.ok_text(i18n::t("删除"))
|
||||||
.ok_variant(ButtonVariant::Danger)
|
.ok_variant(ButtonVariant::Danger)
|
||||||
.show_cancel(true),
|
.show_cancel(true),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::model::ThemeModePref;
|
|||||||
use crate::ui::IconName;
|
use crate::ui::IconName;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl MqttXApp {
|
impl MqttXApp {
|
||||||
pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
@@ -26,9 +27,14 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip(if self.pinned {
|
.tooltip(if self.pinned {
|
||||||
"取消窗口置顶"
|
i18n::t("取消窗口置顶")
|
||||||
} else {
|
} else {
|
||||||
"窗口置顶"
|
i18n::t("置顶窗口")
|
||||||
|
})
|
||||||
|
.accessibility_label(if self.pinned {
|
||||||
|
i18n::t("取消窗口置顶")
|
||||||
|
} else {
|
||||||
|
i18n::t("置顶窗口")
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
let next = !this.pinned;
|
let next = !this.pinned;
|
||||||
@@ -37,7 +43,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error("置顶操作失败"),
|
Notification::error(i18n::t("置顶操作失败")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -68,7 +74,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child("MQTT 调试客户端"),
|
.child(i18n::t("MQTT 调试客户端")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -84,7 +90,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
.child(
|
.child(
|
||||||
Button::new("new-connection")
|
Button::new("new-connection")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.label("新建连接")
|
.label(i18n::t("新建连接"))
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -106,21 +112,21 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
let w5 = app_weak.clone();
|
let w5 = app_weak.clone();
|
||||||
let w6 = app_weak.clone();
|
let w6 = app_weak.clone();
|
||||||
menu.item(
|
menu.item(
|
||||||
PopupMenuItem::new("阿里云设备")
|
PopupMenuItem::new(i18n::t("阿里云设备"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
if let Some(entity) = w1.upgrade() {
|
if let Some(entity) = w1.upgrade() {
|
||||||
crate::ui::dialogs::aliyun_dialog::open(entity, window, cx);
|
crate::ui::dialogs::aliyun_dialog::open(entity, window, cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.item(PopupMenuItem::new("全局变量").on_click(
|
.item(PopupMenuItem::new(i18n::t("全局变量")).on_click(
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(entity) = w2.upgrade() {
|
if let Some(entity) = w2.upgrade() {
|
||||||
crate::ui::dialogs::variables_dialog::open(entity, window, cx);
|
crate::ui::dialogs::variables_dialog::open(entity, window, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.item(PopupMenuItem::new("资源监控").on_click(
|
.item(PopupMenuItem::new(i18n::t("资源监控")).on_click(
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(entity) = w6.upgrade() {
|
if let Some(entity) = w6.upgrade() {
|
||||||
crate::ui::dialogs::resource_dialog::open(entity, window, cx);
|
crate::ui::dialogs::resource_dialog::open(entity, window, cx);
|
||||||
@@ -128,7 +134,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
.separator()
|
.separator()
|
||||||
.item(PopupMenuItem::new("导出连接").on_click(
|
.item(PopupMenuItem::new(i18n::t("导出连接")).on_click(
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(entity) = w4.upgrade() {
|
if let Some(entity) = w4.upgrade() {
|
||||||
entity.update(cx, |app, cx| {
|
entity.update(cx, |app, cx| {
|
||||||
@@ -137,7 +143,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.item(PopupMenuItem::new("导入连接").on_click(
|
.item(PopupMenuItem::new(i18n::t("导入连接")).on_click(
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(entity) = w5.upgrade() {
|
if let Some(entity) = w5.upgrade() {
|
||||||
entity.update(cx, |app, cx| {
|
entity.update(cx, |app, cx| {
|
||||||
@@ -147,7 +153,7 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
.separator()
|
.separator()
|
||||||
.item(PopupMenuItem::new("设置").on_click(
|
.item(PopupMenuItem::new(i18n::t("设置")).on_click(
|
||||||
move |_, window, cx| {
|
move |_, window, cx| {
|
||||||
if let Some(entity) = w3.upgrade() {
|
if let Some(entity) = w3.upgrade() {
|
||||||
crate::ui::dialogs::settings_dialog::open(entity, window, cx);
|
crate::ui::dialogs::settings_dialog::open(entity, window, cx);
|
||||||
@@ -166,7 +172,16 @@ pub(super) fn render_titlebar(&mut self, _window: &mut Window, cx: &mut Context<
|
|||||||
.icon(if dark { IconName::Sun } else { IconName::Moon })
|
.icon(if dark { IconName::Sun } else { IconName::Moon })
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip(if dark { "切换为浅色" } else { "切换为深色" })
|
.tooltip(if dark {
|
||||||
|
i18n::t("切换为浅色")
|
||||||
|
} else {
|
||||||
|
i18n::t("切换为深色")
|
||||||
|
})
|
||||||
|
.accessibility_label(if dark {
|
||||||
|
i18n::t("切换为浅色")
|
||||||
|
} else {
|
||||||
|
i18n::t("切换为深色")
|
||||||
|
})
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
let next = if cx.theme().mode == ThemeMode::Dark {
|
let next = if cx.theme().mode == ThemeMode::Dark {
|
||||||
ThemeModePref::Light
|
ThemeModePref::Light
|
||||||
|
|||||||
+102
-100
@@ -15,6 +15,7 @@ use gpui_kit::component::{
|
|||||||
};
|
};
|
||||||
use gpui_kit::prelude::FluentBuilder as _;
|
use gpui_kit::prelude::FluentBuilder as _;
|
||||||
use crate::ui::IconName;
|
use crate::ui::IconName;
|
||||||
|
use crate::ui::i18n;
|
||||||
use gpui_kit::{
|
use gpui_kit::{
|
||||||
div, percentage, px, App, AppContext as _, Context, Entity, InteractiveElement as _,
|
div, percentage, px, App, AppContext as _, Context, Entity, InteractiveElement as _,
|
||||||
IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _,
|
IntoElement, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _,
|
||||||
@@ -25,11 +26,11 @@ use crate::model::{ConnectionConfig, LastWill, ProtocolVersion, SslConfig, Trans
|
|||||||
use crate::mqtt::MqttEngine;
|
use crate::mqtt::MqttEngine;
|
||||||
use crate::ui::widgets::{field, field_ex, make_select, OptionDelegate};
|
use crate::ui::widgets::{field, field_ex, make_select, OptionDelegate};
|
||||||
|
|
||||||
const PROTOCOLS: [&str; 2] = ["MQTT 5.0", "MQTT 3.1.1"];
|
fn protocol_labels() -> [&'static str; 2] { [i18n::t("MQTT 5.0"), "MQTT 3.1.1"] }
|
||||||
const TRANSPORTS: [&str; 4] = ["TCP", "TLS", "WebSocket", "WSS"];
|
const TRANSPORTS: [&str; 4] = ["TCP", "TLS", "WebSocket", "WSS"]; // 语言无关
|
||||||
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"];
|
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"]; // 语言无关
|
||||||
/// 分组下拉的首项:选择它表示不归属任何分组。
|
/// 分组下拉的首项:选择它表示不归属任何分组。
|
||||||
const NO_GROUP: &str = "无分组";
|
fn no_group_label() -> &'static str { i18n::t("无分组") }
|
||||||
|
|
||||||
pub struct ConnectionForm {
|
pub struct ConnectionForm {
|
||||||
engine: Arc<MqttEngine>,
|
engine: Arc<MqttEngine>,
|
||||||
@@ -142,28 +143,28 @@ impl ConnectionForm {
|
|||||||
chrono::Local::now().timestamp()
|
chrono::Local::now().timestamp()
|
||||||
});
|
});
|
||||||
|
|
||||||
let name = input(&c.name, "连接名称", window, cx);
|
let name = input(&c.name, i18n::t("连接名称"), window, cx);
|
||||||
let host = input(&c.host, "broker.example.com", window, cx);
|
let host = input(&c.host, i18n::t("broker.example.com"), window, cx);
|
||||||
let port = input(&c.port.to_string(), "端口", window, cx);
|
let port = input(&c.port.to_string(), i18n::t(i18n::t("端口")), window, cx);
|
||||||
let path = input(&c.path, "/mqtt", window, cx);
|
let path = input(&c.path, i18n::t("/mqtt"), window, cx);
|
||||||
let client_id = input(&c.client_id, "Client ID", window, cx);
|
let client_id = input(&c.client_id, i18n::t("Client ID"), window, cx);
|
||||||
let username = input(&c.username, "用户名(可选)", window, cx);
|
let username = input(&c.username, i18n::t("用户名(可选)"), window, cx);
|
||||||
let password = cx.new(|cx| {
|
let password = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("密码(可选)")
|
.placeholder(i18n::t("密码(可选)"))
|
||||||
.default_value(&c.password)
|
.default_value(&c.password)
|
||||||
.masked(true)
|
.masked(true)
|
||||||
});
|
});
|
||||||
let keep_alive = input(&c.keep_alive.to_string(), "60", window, cx);
|
let keep_alive = input(&c.keep_alive.to_string(), i18n::t("60"), window, cx);
|
||||||
let conn_timeout = input(
|
let conn_timeout = input(
|
||||||
&c.connection_timeout_secs.to_string(),
|
&c.connection_timeout_secs.to_string(),
|
||||||
"秒(默认 10)",
|
i18n::t("秒(默认 10)"),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let max_reconnect = input(
|
let max_reconnect = input(
|
||||||
&c.max_reconnect_times.to_string(),
|
&c.max_reconnect_times.to_string(),
|
||||||
"0=无限重连",
|
i18n::t("0=无限重连"),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -173,44 +174,44 @@ impl ConnectionForm {
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
},
|
},
|
||||||
"秒(0=会话随连接结束)",
|
i18n::t("秒(0=会话随连接结束)"),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let receive_max = optional_input(c.receive_maximum.map(u32::from), "服务端接收上限", window, cx);
|
let receive_max = optional_input(c.receive_maximum.map(u32::from), i18n::t("服务端接收上限"), window, cx);
|
||||||
let max_packet = optional_input(c.maximum_packet_size, "最大报文长度", window, cx);
|
let max_packet = optional_input(c.maximum_packet_size, i18n::t("最大报文长度"), window, cx);
|
||||||
let topic_alias = optional_input(c.topic_alias_maximum.map(u32::from), "主题别名上限", window, cx);
|
let topic_alias = optional_input(c.topic_alias_maximum.map(u32::from), i18n::t("主题别名上限"), window, cx);
|
||||||
|
|
||||||
let ssl_ca = input(&c.ssl.ca_file, "CA 证书 PEM 路径(可选)", window, cx);
|
let ssl_ca = input(&c.ssl.ca_file, i18n::t("CA 证书 PEM 路径(可选)"), window, cx);
|
||||||
let ssl_client_cert = input(&c.ssl.client_cert_file, "客户端证书 PEM 路径(可选)", window, cx);
|
let ssl_client_cert = input(&c.ssl.client_cert_file, i18n::t("客户端证书 PEM 路径(可选)"), window, cx);
|
||||||
let ssl_client_key = input(&c.ssl.client_key_file, "客户端私钥 PEM 路径(可选)", window, cx);
|
let ssl_client_key = input(&c.ssl.client_key_file, i18n::t("客户端私钥 PEM 路径(可选)"), window, cx);
|
||||||
|
|
||||||
let group_new = input("", "填写后优先于左侧所选分组", window, cx);
|
let group_new = input("", i18n::t("填写后优先于左侧所选分组"), window, cx);
|
||||||
|
|
||||||
let (will_topic, will_payload, will_enabled, will_retain, will_qos_idx, will_content_type, will_response_topic) =
|
let (will_topic, will_payload, will_enabled, will_retain, will_qos_idx, will_content_type, will_response_topic) =
|
||||||
match &c.last_will {
|
match &c.last_will {
|
||||||
Some(w) => (
|
Some(w) => (
|
||||||
input(&w.topic, "遗嘱主题", window, cx),
|
input(&w.topic, i18n::t("遗嘱主题"), window, cx),
|
||||||
input(&w.payload, "遗嘱消息", window, cx),
|
input(&w.payload, i18n::t("遗嘱消息"), window, cx),
|
||||||
true,
|
true,
|
||||||
w.retain,
|
w.retain,
|
||||||
w.qos as usize,
|
w.qos as usize,
|
||||||
input(w.content_type.as_deref().unwrap_or(""), "内容类型(可选)", window, cx),
|
input(w.content_type.as_deref().unwrap_or(""), i18n::t("内容类型(可选)"), window, cx),
|
||||||
input(w.response_topic.as_deref().unwrap_or(""), "响应主题(可选)", window, cx),
|
input(w.response_topic.as_deref().unwrap_or(""), i18n::t("响应主题(可选)"), window, cx),
|
||||||
),
|
),
|
||||||
None => (
|
None => (
|
||||||
input("", "遗嘱主题", window, cx),
|
input("", i18n::t("遗嘱主题"), window, cx),
|
||||||
input("", "遗嘱消息", window, cx),
|
input("", i18n::t("遗嘱消息"), window, cx),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
input("", "内容类型(可选)", window, cx),
|
input("", i18n::t("内容类型(可选)"), window, cx),
|
||||||
input("", "响应主题(可选)", window, cx),
|
input("", i18n::t("响应主题(可选)"), window, cx),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let protocol = make_select(
|
let protocol = make_select(
|
||||||
&PROTOCOLS,
|
&protocol_labels(),
|
||||||
if c.protocol.is_v5() { 0 } else { 1 },
|
if c.protocol.is_v5() { 0 } else { 1 },
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@@ -222,7 +223,7 @@ impl ConnectionForm {
|
|||||||
let transport = make_select(&TRANSPORTS, transport_idx, window, cx);
|
let transport = make_select(&TRANSPORTS, transport_idx, window, cx);
|
||||||
let will_qos = make_select(&QOS, will_qos_idx, window, cx);
|
let will_qos = make_select(&QOS, will_qos_idx, window, cx);
|
||||||
// 分组下拉:首项固定为「无分组」,其后是已有分组
|
// 分组下拉:首项固定为「无分组」,其后是已有分组
|
||||||
let mut group_labels = vec![NO_GROUP];
|
let mut group_labels = vec![no_group_label()];
|
||||||
group_labels.extend(groups.iter().map(|s| s.as_str()));
|
group_labels.extend(groups.iter().map(|s| s.as_str()));
|
||||||
let group_idx = c
|
let group_idx = c
|
||||||
.group
|
.group
|
||||||
@@ -398,39 +399,39 @@ impl ConnectionForm {
|
|||||||
r.map_err(|e| self.fail(key, e))
|
r.map_err(|e| self.fail(key, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_u16(s: &str, label: &str) -> Result<u16, String> {
|
fn parse_u16(s: &str, label: &'static str) -> Result<u16, String> {
|
||||||
s.trim()
|
s.trim()
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.map_err(|_| format!("{label} 需要是 0~65535 的整数"))
|
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("需要是 0~65535 的整数")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 端口:1~65535。0 不是可连接端口,放行只会在 CONNECT 阶段报出难以理解的错误。
|
/// 端口:1~65535。0 不是可连接端口,放行只会在 CONNECT 阶段报出难以理解的错误。
|
||||||
fn parse_port(s: &str) -> Result<u16, String> {
|
fn parse_port(s: &str) -> Result<u16, String> {
|
||||||
match s.trim().parse::<u16>() {
|
match s.trim().parse::<u16>() {
|
||||||
Ok(0) => Err("端口需要是 1~65535 的整数".into()),
|
Ok(0) => Err(i18n::t("端口需要是 1~65535 的整数").to_string()),
|
||||||
Ok(p) => Ok(p),
|
Ok(p) => Ok(p),
|
||||||
Err(_) => Err("端口需要是 1~65535 的整数".into()),
|
Err(_) => Err(i18n::t("端口需要是 1~65535 的整数").to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同 `parse_opt_u32`,但 0 视为非法:MQTT 5 规范把 Receive Maximum /
|
/// 同 `parse_opt_u32`,但 0 视为非法:MQTT 5 规范把 Receive Maximum /
|
||||||
/// Maximum Packet Size 为 0 定义为 Protocol Error,broker 会直接断开连接,
|
/// Maximum Packet Size 为 0 定义为 Protocol Error,broker 会直接断开连接,
|
||||||
/// 且该值会被持久化,表现为"每次连接都失败"。
|
/// 且该值会被持久化,表现为"每次连接都失败"。
|
||||||
fn parse_opt_positive_u32(s: &str, label: &str) -> Result<Option<u32>, String> {
|
fn parse_opt_positive_u32(s: &str, label: &'static str) -> Result<Option<u32>, String> {
|
||||||
match Self::parse_opt_u32(s, label)? {
|
match Self::parse_opt_u32(s, label)? {
|
||||||
Some(0) => Err(format!("{label} 必须大于 0(0 是 MQTT 5 协议错误)")),
|
Some(0) => Err(format!("{} {}", i18n::t(label), i18n::t("必须大于 0(0 是 MQTT 5 协议错误)"))),
|
||||||
other => Ok(other),
|
other => Ok(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_opt_u32(s: &str, label: &str) -> Result<Option<u32>, String> {
|
fn parse_opt_u32(s: &str, label: &'static str) -> Result<Option<u32>, String> {
|
||||||
let s = s.trim();
|
let s = s.trim();
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
s.parse::<u32>()
|
s.parse::<u32>()
|
||||||
.map(Some)
|
.map(Some)
|
||||||
.map_err(|_| format!("{label} 需要是非负整数"))
|
.map_err(|_| format!("{} {}", i18n::t(label), i18n::t("需要是非负整数")))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(&self, cx: &App) -> Result<ConnectionConfig, String> {
|
pub fn build(&self, cx: &App) -> Result<ConnectionConfig, String> {
|
||||||
@@ -450,28 +451,28 @@ impl ConnectionForm {
|
|||||||
let mut name = self.val(&self.name, cx);
|
let mut name = self.val(&self.name, cx);
|
||||||
if name.trim().is_empty() {
|
if name.trim().is_empty() {
|
||||||
if !for_test {
|
if !for_test {
|
||||||
return Err(self.fail("name", "连接名称不能为空"));
|
return Err(self.fail("name", i18n::t("连接名称不能为空")));
|
||||||
}
|
}
|
||||||
name = "测试连接".into();
|
name = i18n::t("测试连接").into();
|
||||||
}
|
}
|
||||||
// 判空已按 trim,保存也存 trim 后的值,避免首尾空白入库
|
// 判空已按 trim,保存也存 trim 后的值,避免首尾空白入库
|
||||||
let name = name.trim().to_string();
|
let name = name.trim().to_string();
|
||||||
let host = self.val(&self.host, cx);
|
let host = self.val(&self.host, cx);
|
||||||
if host.trim().is_empty() {
|
if host.trim().is_empty() {
|
||||||
return Err(self.fail("host", "主机地址不能为空"));
|
return Err(self.fail("host", i18n::t("主机地址不能为空")));
|
||||||
}
|
}
|
||||||
let host = host.trim().to_string();
|
let host = host.trim().to_string();
|
||||||
let port = self.fail_as("port", Self::parse_port(&self.val(&self.port, cx)))?;
|
let port = self.fail_as("port", Self::parse_port(&self.val(&self.port, cx)))?;
|
||||||
let mut client_id = self.val(&self.client_id, cx);
|
let mut client_id = self.val(&self.client_id, cx);
|
||||||
if client_id.trim().is_empty() {
|
if client_id.trim().is_empty() {
|
||||||
if !for_test {
|
if !for_test {
|
||||||
return Err(self.fail("client_id", "Client ID 不能为空"));
|
return Err(self.fail("client_id", i18n::t("Client ID 不能为空")));
|
||||||
}
|
}
|
||||||
client_id = format!("mqttx-test-{}", &uuid::Uuid::new_v4().simple().to_string()[..8]);
|
client_id = format!("mqttx-test-{}", &uuid::Uuid::new_v4().simple().to_string()[..8]);
|
||||||
}
|
}
|
||||||
let client_id = client_id.trim().to_string();
|
let client_id = client_id.trim().to_string();
|
||||||
let keep_alive =
|
let keep_alive =
|
||||||
self.fail_as("keep_alive", Self::parse_u16(&self.val(&self.keep_alive, cx), "Keep Alive"))?;
|
self.fail_as("keep_alive", Self::parse_u16(&self.val(&self.keep_alive, cx), i18n::t("Keep Alive")))?;
|
||||||
// 空串视为 0(会话随连接结束);非空必须是合法非负整数,不再静默吞掉 -1/abc
|
// 空串视为 0(会话随连接结束);非空必须是合法非负整数,不再静默吞掉 -1/abc
|
||||||
let session_expiry = {
|
let session_expiry = {
|
||||||
let raw = self.val(&self.session_expiry, cx);
|
let raw = self.val(&self.session_expiry, cx);
|
||||||
@@ -482,23 +483,23 @@ impl ConnectionForm {
|
|||||||
t.parse::<u32>().map_err(|_| {
|
t.parse::<u32>().map_err(|_| {
|
||||||
self.fail(
|
self.fail(
|
||||||
"session_expiry",
|
"session_expiry",
|
||||||
"会话过期间隔需要是非负整数(0=会话随连接结束)",
|
i18n::t("会话过期间隔需要是非负整数(0=会话随连接结束)"),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let receive_maximum = self.fail_as("receive_max", (|| -> Result<_, String> {
|
let receive_maximum = self.fail_as("receive_max", (|| -> Result<_, String> {
|
||||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.receive_max, cx), "接收上限")?
|
Ok(Self::parse_opt_positive_u32(&self.val(&self.receive_max, cx), i18n::t("接收上限"))?
|
||||||
.map(|v| u16::try_from(v).map_err(|_| "接收上限不能超过 65535".to_string()))
|
.map(|v| u16::try_from(v).map_err(|_| i18n::t("接收上限不能超过 65535").to_string()))
|
||||||
.transpose())
|
.transpose())
|
||||||
})())??;
|
})())??;
|
||||||
let maximum_packet_size = self.fail_as(
|
let maximum_packet_size = self.fail_as(
|
||||||
"max_packet",
|
"max_packet",
|
||||||
Self::parse_opt_positive_u32(&self.val(&self.max_packet, cx), "最大报文长度"),
|
Self::parse_opt_positive_u32(&self.val(&self.max_packet, cx), i18n::t("最大报文长度")),
|
||||||
)?;
|
)?;
|
||||||
let topic_alias_maximum = self.fail_as("topic_alias", (|| -> Result<_, String> {
|
let topic_alias_maximum = self.fail_as("topic_alias", (|| -> Result<_, String> {
|
||||||
Ok(Self::parse_opt_positive_u32(&self.val(&self.topic_alias, cx), "主题别名")?
|
Ok(Self::parse_opt_positive_u32(&self.val(&self.topic_alias, cx), i18n::t("主题别名"))?
|
||||||
.map(|v| u16::try_from(v).map_err(|_| "主题别名上限不能超过 65535".to_string()))
|
.map(|v| u16::try_from(v).map_err(|_| i18n::t("主题别名上限不能超过 65535").to_string()))
|
||||||
.transpose())
|
.transpose())
|
||||||
})())??;
|
})())??;
|
||||||
|
|
||||||
@@ -511,7 +512,7 @@ impl ConnectionForm {
|
|||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|v| *v >= 1)
|
.filter(|v| *v >= 1)
|
||||||
.ok_or_else(|| self.fail("conn_timeout", "连接超时需要是 1~65535 的整数(秒)"))?
|
.ok_or_else(|| self.fail("conn_timeout", i18n::t("连接超时需要是 1~65535 的整数(秒)")))?
|
||||||
};
|
};
|
||||||
let max_reconnect_raw = self.val(&self.max_reconnect, cx);
|
let max_reconnect_raw = self.val(&self.max_reconnect, cx);
|
||||||
let max_reconnect_times = if max_reconnect_raw.trim().is_empty() {
|
let max_reconnect_times = if max_reconnect_raw.trim().is_empty() {
|
||||||
@@ -520,7 +521,7 @@ impl ConnectionForm {
|
|||||||
max_reconnect_raw
|
max_reconnect_raw
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.map_err(|_| self.fail("max_reconnect", "最大重连次数需要是非负整数(0=无限)"))?
|
.map_err(|_| self.fail("max_reconnect", i18n::t("最大重连次数需要是非负整数(0=无限)")))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分组:新分组名非空时优先,否则取下拉所选(首项=无分组)
|
// 分组:新分组名非空时优先,否则取下拉所选(首项=无分组)
|
||||||
@@ -558,7 +559,7 @@ impl ConnectionForm {
|
|||||||
// WS/WSS 依赖 path 建立升级请求,缺失时连接必然失败,保存前拦截
|
// WS/WSS 依赖 path 建立升级请求,缺失时连接必然失败,保存前拦截
|
||||||
let path = self.val(&self.path, cx).trim().to_string();
|
let path = self.val(&self.path, cx).trim().to_string();
|
||||||
if transport.is_websocket() && path.is_empty() {
|
if transport.is_websocket() && path.is_empty() {
|
||||||
return Err(self.fail("path", "WS/WSS 传输下 WS 路径不能为空"));
|
return Err(self.fail("path", i18n::t("WS/WSS 传输下 WS 路径不能为空")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ssl = SslConfig {
|
let ssl = SslConfig {
|
||||||
@@ -571,7 +572,7 @@ impl ConnectionForm {
|
|||||||
if transport.is_tls()
|
if transport.is_tls()
|
||||||
&& ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty()
|
&& ssl.client_cert_file.is_empty() != ssl.client_key_file.is_empty()
|
||||||
{
|
{
|
||||||
return Err(self.fail("ssl_client_cert", "客户端证书与客户端密钥必须同时填写"));
|
return Err(self.fail("ssl_client_cert", i18n::t("客户端证书与客户端密钥必须同时填写")));
|
||||||
}
|
}
|
||||||
// 证书文件不存在时握手才报错太晚,保存前给出字段级提示
|
// 证书文件不存在时握手才报错太晚,保存前给出字段级提示
|
||||||
if transport.is_tls() {
|
if transport.is_tls() {
|
||||||
@@ -581,7 +582,7 @@ impl ConnectionForm {
|
|||||||
("ssl_client_key", "客户端私钥", &ssl.client_key_file),
|
("ssl_client_key", "客户端私钥", &ssl.client_key_file),
|
||||||
] {
|
] {
|
||||||
if !p.is_empty() && !std::path::Path::new(p).exists() {
|
if !p.is_empty() && !std::path::Path::new(p).exists() {
|
||||||
return Err(self.fail(key, format!("{label}路径不存在:{p}")));
|
return Err(self.fail(key, i18n::tf("{label}路径不存在:{p}", &[("label", &i18n::t(label)), ("p", &p)])));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -590,13 +591,13 @@ impl ConnectionForm {
|
|||||||
// 与其它字段一致:trim 后入库
|
// 与其它字段一致:trim 后入库
|
||||||
let topic = self.val(&self.will_topic, cx).trim().to_string();
|
let topic = self.val(&self.will_topic, cx).trim().to_string();
|
||||||
if topic.is_empty() {
|
if topic.is_empty() {
|
||||||
return Err(self.fail("will_topic", "遗嘱主题不能为空"));
|
return Err(self.fail("will_topic", i18n::t("遗嘱主题不能为空")));
|
||||||
}
|
}
|
||||||
// 通配符只用于订阅过滤器;出现在 PUBLISH 主题上会被 broker 直接拒绝
|
// 通配符只用于订阅过滤器;出现在 PUBLISH 主题上会被 broker 直接拒绝
|
||||||
if topic.contains('+') || topic.contains('#') {
|
if topic.contains('+') || topic.contains('#') {
|
||||||
return Err(self.fail(
|
return Err(self.fail(
|
||||||
"will_topic",
|
"will_topic",
|
||||||
"遗嘱主题不能包含通配符 + 或 #",
|
i18n::t("遗嘱主题不能包含通配符 + 或 #"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let opt_str = |v: String| {
|
let opt_str = |v: String| {
|
||||||
@@ -702,7 +703,7 @@ impl ConnectionForm {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(id)
|
Button::new(id)
|
||||||
.label("浏览…")
|
.label(i18n::t("浏览…"))
|
||||||
.outline()
|
.outline()
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
@@ -722,7 +723,7 @@ impl ConnectionForm {
|
|||||||
let (tx, rx) = smol::channel::bounded::<Option<std::path::PathBuf>>(1);
|
let (tx, rx) = smol::channel::bounded::<Option<std::path::PathBuf>>(1);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let picked = rfd::FileDialog::new()
|
let picked = rfd::FileDialog::new()
|
||||||
.set_title("选择证书 / 密钥文件")
|
.set_title(i18n::t("选择证书 / 密钥文件"))
|
||||||
.add_filter("证书 / 密钥 (PEM)", &["pem", "crt", "cer", "key", "der"])
|
.add_filter("证书 / 密钥 (PEM)", &["pem", "crt", "cer", "key", "der"])
|
||||||
.pick_file();
|
.pick_file();
|
||||||
let _ = tx.send_blocking(picked);
|
let _ = tx.send_blocking(picked);
|
||||||
@@ -877,39 +878,40 @@ impl Render for ConnectionForm {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|on| **on)
|
.filter(|on| **on)
|
||||||
.count();
|
.count();
|
||||||
let ssl_hint = (ssl_count > 0).then(|| SharedString::from(format!("已配置 {ssl_count} 项")));
|
let ssl_hint = (ssl_count > 0).then(|| SharedString::from(i18n::tf("已配置 {n} 项", &[("n", &ssl_count.to_string())])));
|
||||||
let mqtt5_hint =
|
let mqtt5_hint =
|
||||||
(mqtt5_count > 0).then(|| SharedString::from(format!("已配置 {mqtt5_count} 项")));
|
(mqtt5_count > 0).then(|| SharedString::from(i18n::tf("已配置 {n} 项", &[("n", &mqtt5_count.to_string())])));
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.max_h(px(560.))
|
.max_h(px(560.))
|
||||||
.overflow_y_scrollbar()
|
.overflow_y_scrollbar()
|
||||||
.pr_1()
|
.pr_1()
|
||||||
// ── 基本信息(始终展开) ──
|
// ── 基本信息(始终展开) ──
|
||||||
.child(section("基本信息", border, card_bg,
|
.child(section(i18n::t("基本信息"), border, card_bg,
|
||||||
v_flex().gap_2().child(
|
v_flex().gap_2().child(
|
||||||
// 主机与端口是一对,放同一行;名称与协议/传输同行
|
// 主机与端口是一对,放同一行;名称与协议/传输同行
|
||||||
h_flex().gap_2().w_full()
|
h_flex().gap_2().w_full()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("主机", Input::new(&self.host), true, err("host"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("主机"), Input::new(&self.host), true, err("host"), danger)))
|
||||||
.child(div().w(px(120.)).child(field_ex("端口", Input::new(&self.port), true, err("port"), danger)))
|
.child(div().w(px(120.)).child(field_ex(i18n::t(i18n::t("端口")), Input::new(&self.port), true, err("port"), danger)))
|
||||||
)
|
)
|
||||||
.child(h_flex().gap_2().w_full()
|
.child(h_flex().gap_2().w_full()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("名称", Input::new(&self.name), true, err("name"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("名称"), Input::new(&self.name), true, err("name"), danger)))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("协议", Select::new(&self.protocol))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("协议"), Select::new(&self.protocol))))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("传输", Select::new(&self.transport))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("传输"), Select::new(&self.transport))))
|
||||||
)
|
)
|
||||||
.child(h_flex().gap_2().w_full()
|
.child(h_flex().gap_2().w_full()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("分组", Select::new(&self.group_select))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("分组"), Select::new(&self.group_select))))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("新建分组", Input::new(&self.group_new))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("新建分组"), Input::new(&self.group_new))))
|
||||||
)
|
)
|
||||||
.child(h_flex().gap_2().w_full().items_end()
|
.child(h_flex().gap_2().w_full().items_end()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("Client ID", Input::new(&self.client_id), true, err("client_id"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("Client ID"), Input::new(&self.client_id), true, err("client_id"), danger)))
|
||||||
// 一键换一个随机 Client ID,与 ConnectionConfig::new 的生成逻辑一致
|
// 一键换一个随机 Client ID,与 ConnectionConfig::new 的生成逻辑一致
|
||||||
.child(
|
.child(
|
||||||
Button::new("cid-regen")
|
Button::new("cid-regen")
|
||||||
.icon(IconName::RefreshCw)
|
.icon(IconName::RefreshCw)
|
||||||
.ghost()
|
.ghost()
|
||||||
.tooltip("重新生成 Client ID")
|
.tooltip(i18n::t("重新生成 Client ID"))
|
||||||
|
.accessibility_label(i18n::t("重新生成 Client ID"))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
let cid = crate::model::generate_client_id();
|
let cid = crate::model::generate_client_id();
|
||||||
this.client_id
|
this.client_id
|
||||||
@@ -919,48 +921,48 @@ impl Render for ConnectionForm {
|
|||||||
)
|
)
|
||||||
// WS 路径仅对 WebSocket/WSS 有意义,按需显示;该传输下必填
|
// WS 路径仅对 WebSocket/WSS 有意义,按需显示;该传输下必填
|
||||||
.when(is_ws, |w| w.child(
|
.when(is_ws, |w| w.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field_ex("WS 路径", Input::new(&self.path), true, err("path"), danger))
|
div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("WS 路径"), Input::new(&self.path), true, err("path"), danger))
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
// ── 认证与心跳 ──
|
// ── 认证与心跳 ──
|
||||||
.child(self.collapsible_section(
|
.child(self.collapsible_section(
|
||||||
"sec-auth", "认证与心跳", None, false, self.show_auth,
|
"sec-auth", i18n::t("认证与心跳"), None, false, self.show_auth,
|
||||||
|f| f.show_auth = !f.show_auth, border, card_bg,
|
|f| f.show_auth = !f.show_auth, border, card_bg,
|
||||||
v_flex().gap_2()
|
v_flex().gap_2()
|
||||||
.child(h_flex().gap_2().w_full()
|
.child(h_flex().gap_2().w_full()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("用户名", Input::new(&self.username))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("用户名"), Input::new(&self.username))))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("密码", Input::new(&self.password).mask_toggle())))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("密码"), Input::new(&self.password).mask_toggle())))
|
||||||
.child(div().w(px(110.)).child(field_ex("Keep Alive (秒)", Input::new(&self.keep_alive), true, err("keep_alive"), danger)))
|
.child(div().w(px(110.)).child(field_ex(i18n::t("Keep Alive (秒)"), Input::new(&self.keep_alive), true, err("keep_alive"), danger)))
|
||||||
)
|
)
|
||||||
.child(h_flex().gap_2().w_full()
|
.child(h_flex().gap_2().w_full()
|
||||||
.child(div().w(px(150.)).child(field_ex("连接超时 (秒)", Input::new(&self.conn_timeout), false, err("conn_timeout"), danger)))
|
.child(div().w(px(150.)).child(field_ex(i18n::t("连接超时 (秒)"), Input::new(&self.conn_timeout), false, err("conn_timeout"), danger)))
|
||||||
.child(div().w(px(170.)).child(field_ex("最大重连次数 (0=无限)", Input::new(&self.max_reconnect), false, err("max_reconnect"), danger)))
|
.child(div().w(px(170.)).child(field_ex(i18n::t("最大重连次数 (0=无限)"), Input::new(&self.max_reconnect), false, err("max_reconnect"), danger)))
|
||||||
)
|
)
|
||||||
.child(self.switch_row("clean_start", "Clean Start / Clean Session", self.clean_start, cx))
|
.child(self.switch_row("clean_start", i18n::t("Clean Start / Clean Session"), self.clean_start, cx))
|
||||||
.child(self.switch_row("auto_reconnect", "断线自动重连", self.auto_reconnect, cx))
|
.child(self.switch_row("auto_reconnect", i18n::t("断线自动重连"), self.auto_reconnect, cx))
|
||||||
.child(self.switch_row("auto_resubscribe", "连接后自动恢复订阅", self.auto_resubscribe, cx))
|
.child(self.switch_row("auto_resubscribe", i18n::t("连接后自动恢复订阅"), self.auto_resubscribe, cx))
|
||||||
.child(self.switch_row("auto_connect", "启动时自动连接", self.auto_connect, cx)),
|
.child(self.switch_row("auto_connect", i18n::t("启动时自动连接"), self.auto_connect, cx)),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
// ── SSL/TLS(仅 TLS/WSS 显示) ──
|
// ── SSL/TLS(仅 TLS/WSS 显示) ──
|
||||||
.when(is_tls, |w| w.child(self.collapsible_section(
|
.when(is_tls, |w| w.child(self.collapsible_section(
|
||||||
"sec-ssl", "SSL/TLS", ssl_hint, true, self.show_ssl,
|
"sec-ssl", i18n::t("SSL/TLS"), ssl_hint, true, self.show_ssl,
|
||||||
|f| f.show_ssl = !f.show_ssl, border, card_bg,
|
|f| f.show_ssl = !f.show_ssl, border, card_bg,
|
||||||
v_flex().gap_2()
|
v_flex().gap_2()
|
||||||
.child(self.cert_path_row(
|
.child(self.cert_path_row(
|
||||||
"ssl-ca-browse", "CA 证书 (PEM)", self.ssl_ca.clone(),
|
"ssl-ca-browse", i18n::t("CA 证书 (PEM)"), self.ssl_ca.clone(),
|
||||||
err("ssl_ca"), danger, cx,
|
err("ssl_ca"), danger, cx,
|
||||||
))
|
))
|
||||||
.child(self.cert_path_row(
|
.child(self.cert_path_row(
|
||||||
"ssl-cert-browse", "客户端证书 (PEM)", self.ssl_client_cert.clone(),
|
"ssl-cert-browse", i18n::t("客户端证书 (PEM)"), self.ssl_client_cert.clone(),
|
||||||
err("ssl_client_cert"), danger, cx,
|
err("ssl_client_cert"), danger, cx,
|
||||||
))
|
))
|
||||||
.child(self.cert_path_row(
|
.child(self.cert_path_row(
|
||||||
"ssl-key-browse", "客户端密钥 (PEM)", self.ssl_client_key.clone(),
|
"ssl-key-browse", i18n::t("客户端密钥 (PEM)"), self.ssl_client_key.clone(),
|
||||||
err("ssl_client_key"), danger, cx,
|
err("ssl_client_key"), danger, cx,
|
||||||
))
|
))
|
||||||
.child(self.switch_row("ssl_ignore_ca", "忽略 CA 校验(不验证服务器证书)", self.ssl_ignore_ca, cx)),
|
.child(self.switch_row("ssl_ignore_ca", i18n::t("忽略 CA 校验(不验证服务器证书)"), self.ssl_ignore_ca, cx)),
|
||||||
cx,
|
cx,
|
||||||
)))
|
)))
|
||||||
// ── MQTT 5 高级属性(仅 v5 协议显示) ──
|
// ── MQTT 5 高级属性(仅 v5 协议显示) ──
|
||||||
@@ -968,29 +970,29 @@ impl Render for ConnectionForm {
|
|||||||
"sec-mqtt5", "MQTT 5 属性", mqtt5_hint, true, self.show_mqtt5,
|
"sec-mqtt5", "MQTT 5 属性", mqtt5_hint, true, self.show_mqtt5,
|
||||||
|f| f.show_mqtt5 = !f.show_mqtt5, border, card_bg,
|
|f| f.show_mqtt5 = !f.show_mqtt5, border, card_bg,
|
||||||
h_flex().gap_2().w_full()
|
h_flex().gap_2().w_full()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("会话过期间隔(秒)", Input::new(&self.session_expiry), false, err("session_expiry"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("会话过期间隔(秒)"), Input::new(&self.session_expiry), false, err("session_expiry"), danger)))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("接收最大值", Input::new(&self.receive_max), false, err("receive_max"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("接收最大值"), Input::new(&self.receive_max), false, err("receive_max"), danger)))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("最大报文长度", Input::new(&self.max_packet), false, err("max_packet"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("最大报文长度"), Input::new(&self.max_packet), false, err("max_packet"), danger)))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("主题别名上限", Input::new(&self.topic_alias), false, err("topic_alias"), danger))),
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("主题别名上限"), Input::new(&self.topic_alias), false, err("topic_alias"), danger))),
|
||||||
cx,
|
cx,
|
||||||
)))
|
)))
|
||||||
// ── 遗嘱 ──
|
// ── 遗嘱 ──
|
||||||
.child(self.collapsible_section(
|
.child(self.collapsible_section(
|
||||||
"sec-will", "遗嘱消息 (Last Will)",
|
"sec-will", i18n::t("遗嘱消息 (Last Will)"),
|
||||||
self.will_enabled.then(|| "已启用".into()), true, self.show_will,
|
self.will_enabled.then(|| i18n::t("已启用").into()), true, self.show_will,
|
||||||
|f| f.show_will = !f.show_will, border, card_bg,
|
|f| f.show_will = !f.show_will, border, card_bg,
|
||||||
v_flex().gap_2()
|
v_flex().gap_2()
|
||||||
.child(self.switch_row("will_enabled", "启用遗嘱消息", self.will_enabled, cx))
|
.child(self.switch_row("will_enabled", i18n::t("启用遗嘱消息"), self.will_enabled, cx))
|
||||||
.when(self.will_enabled, |w| w
|
.when(self.will_enabled, |w| w
|
||||||
.child(h_flex().gap_2()
|
.child(h_flex().gap_2()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field_ex("遗嘱主题", Input::new(&self.will_topic), true, err("will_topic"), danger)))
|
.child(div().flex_1().min_w(px(0.)).child(field_ex(i18n::t("遗嘱主题"), Input::new(&self.will_topic), true, err("will_topic"), danger)))
|
||||||
.child(div().w(px(130.)).child(field("QoS", Select::new(&self.will_qos))))
|
.child(div().w(px(130.)).child(field(i18n::t("QoS"), Select::new(&self.will_qos))))
|
||||||
)
|
)
|
||||||
.child(field("遗嘱内容", Input::new(&self.will_payload)))
|
.child(field(i18n::t("遗嘱内容"), Input::new(&self.will_payload)))
|
||||||
// v5 遗嘱属性;3.1.1 连接会忽略这两项
|
// v5 遗嘱属性;3.1.1 连接会忽略这两项
|
||||||
.child(h_flex().gap_2().w_full()
|
.child(h_flex().gap_2().w_full()
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("内容类型 (Content Type)", Input::new(&self.will_content_type))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("内容类型 (Content Type)"), Input::new(&self.will_content_type))))
|
||||||
.child(div().flex_1().min_w(px(0.)).child(field("响应主题 (Response Topic)", Input::new(&self.will_response_topic))))
|
.child(div().flex_1().min_w(px(0.)).child(field(i18n::t("响应主题 (Response Topic)"), Input::new(&self.will_response_topic))))
|
||||||
)
|
)
|
||||||
.child(self.switch_row("will_retain", "Retain", self.will_retain, cx))
|
.child(self.switch_row("will_retain", "Retain", self.will_retain, cx))
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::model::{ render_will_templates, ConnectionStatus, Direction, LogLevel
|
|||||||
use crate::ui::widgets::format_time;
|
use crate::ui::widgets::format_time;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl ConnectionView {
|
impl ConnectionView {
|
||||||
pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
@@ -31,7 +32,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
let connecting = matches!(status, ConnectionStatus::Connecting);
|
let connecting = matches!(status, ConnectionStatus::Connecting);
|
||||||
let _ = is_v5;
|
let _ = is_v5;
|
||||||
|
|
||||||
let status_text = status.label();
|
let status_text = i18n::t(status.label());
|
||||||
let status_tone = match status {
|
let status_tone = match status {
|
||||||
ConnectionStatus::Connected => cx.theme().success,
|
ConnectionStatus::Connected => cx.theme().success,
|
||||||
ConnectionStatus::Connecting => cx.theme().warning,
|
ConnectionStatus::Connecting => cx.theme().warning,
|
||||||
@@ -91,11 +92,11 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
IconName::PlugZap
|
IconName::PlugZap
|
||||||
})
|
})
|
||||||
.label(if connected {
|
.label(if connected {
|
||||||
"断开"
|
i18n::t("断开")
|
||||||
} else if connecting {
|
} else if connecting {
|
||||||
"连接中…"
|
i18n::t("连接中")
|
||||||
} else {
|
} else {
|
||||||
"连接"
|
i18n::t("连接")
|
||||||
})
|
})
|
||||||
.when(connected, |b| b.danger().ghost())
|
.when(connected, |b| b.danger().ghost())
|
||||||
.when(!connected, |b| b.primary().ghost())
|
.when(!connected, |b| b.primary().ghost())
|
||||||
@@ -150,7 +151,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
let border = cx.theme().border;
|
let border = cx.theme().border;
|
||||||
let muted = cx.theme().muted_foreground;
|
let muted = cx.theme().muted_foreground;
|
||||||
let mono = cx.theme().mono_font_family.clone();
|
let mono = cx.theme().mono_font_family.clone();
|
||||||
let direction_label = if received { "接收" } else { "发布" };
|
let direction_label = if received { i18n::t("接收") } else { i18n::t("发布") };
|
||||||
|
|
||||||
let header = h_flex()
|
let header = h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -245,7 +246,8 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().foreground)
|
||||||
.pt_0p5()
|
.pt_0p5()
|
||||||
.font_family(mono.clone())
|
.font_family(mono.clone())
|
||||||
.child(preview_payload(&record.payload)),
|
// 预览在入环形缓冲时已算好(JSON 美化/压平+截断),此处零解析
|
||||||
|
.child(SharedString::from(record.preview.clone())),
|
||||||
);
|
);
|
||||||
|
|
||||||
if expanded {
|
if expanded {
|
||||||
@@ -265,7 +267,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
seq,
|
seq,
|
||||||
f.index()
|
f.index()
|
||||||
)))
|
)))
|
||||||
.label(f.label())
|
.label(i18n::t(f.label()))
|
||||||
.small()
|
.small()
|
||||||
.selected(f == fmt)
|
.selected(f == fmt)
|
||||||
}),
|
}),
|
||||||
@@ -289,40 +291,43 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("msg-copy-topic-{}", seq)))
|
Button::new(SharedString::from(format!("msg-copy-topic-{}", seq)))
|
||||||
.icon(IconName::Copy)
|
.icon(IconName::Copy)
|
||||||
.label("主题")
|
.label(i18n::t("主题"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("复制主题")
|
.tooltip(i18n::t("复制主题"))
|
||||||
|
.accessibility_label(i18n::t("复制主题"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_topic.to_string()));
|
cx.write_to_clipboard(ClipboardItem::new_string(copy_topic.to_string()));
|
||||||
window.push_notification(Notification::success("已复制主题"), cx);
|
window.push_notification(Notification::success(i18n::t("已复制主题")), cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("msg-copy-payload-{}", seq)))
|
Button::new(SharedString::from(format!("msg-copy-payload-{}", seq)))
|
||||||
.icon(IconName::Copy)
|
.icon(IconName::Copy)
|
||||||
.label("负载")
|
.label(i18n::t("负载"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("复制负载原文")
|
.tooltip(i18n::t("复制负载原文"))
|
||||||
|
.accessibility_label(i18n::t("复制负载原文"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_payload.to_string()));
|
cx.write_to_clipboard(ClipboardItem::new_string(copy_payload.to_string()));
|
||||||
window.push_notification(Notification::success("已复制负载"), cx);
|
window.push_notification(Notification::success(i18n::t("已复制负载")), cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("msg-copy-detail-{}", seq)))
|
Button::new(SharedString::from(format!("msg-copy-detail-{}", seq)))
|
||||||
.icon(IconName::Copy)
|
.icon(IconName::Copy)
|
||||||
.label("详情")
|
.label(i18n::t("详情"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("复制完整详情(含 v5 属性)")
|
.tooltip(i18n::t("复制完整详情(含 v5 属性)"))
|
||||||
|
.accessibility_label(i18n::t("复制完整详情(含 v5 属性)"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_detail.clone()));
|
cx.write_to_clipboard(ClipboardItem::new_string(copy_detail.clone()));
|
||||||
window.push_notification(Notification::success("已复制详情"), cx);
|
window.push_notification(Notification::success(i18n::t("已复制详情")), cx);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
details = details.child(detail_toolbar).child(
|
details = details.child(detail_toolbar).child(
|
||||||
@@ -345,7 +350,7 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
details = details
|
details = details
|
||||||
.child(div().text_xs().font_semibold().child("用户属性"))
|
.child(div().text_xs().font_semibold().child(i18n::t("用户属性")))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.font_family(mono.clone())
|
.font_family(mono.clone())
|
||||||
@@ -358,10 +363,10 @@ pub(super) fn render_top_bar(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
("Response Topic", record.response_topic.clone()),
|
("Response Topic", record.response_topic.clone()),
|
||||||
("Correlation Data", record.correlation_data.clone()),
|
("Correlation Data", record.correlation_data.clone()),
|
||||||
(
|
(
|
||||||
"消息过期",
|
i18n::t("消息过期"),
|
||||||
record
|
record
|
||||||
.message_expiry_interval
|
.message_expiry_interval
|
||||||
.map(|v| format!("{v} 秒")),
|
.map(|v| i18n::tf("{n} 秒", &[("n", &v.to_string())])),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"订阅标识符",
|
"订阅标识符",
|
||||||
@@ -508,9 +513,9 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(if filtering {
|
.child(if filtering {
|
||||||
"没有匹配的消息"
|
i18n::t("没有匹配的消息")
|
||||||
} else {
|
} else {
|
||||||
"暂无消息,订阅主题后消息会显示在这里"
|
i18n::t("暂无消息,订阅主题后消息会显示在这里")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -538,7 +543,7 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
self.conn_id,
|
self.conn_id,
|
||||||
d.index()
|
d.index()
|
||||||
)))
|
)))
|
||||||
.label(d.label())
|
.label(i18n::t(d.label()))
|
||||||
.small()
|
.small()
|
||||||
.selected(d == dir)
|
.selected(d == dir)
|
||||||
},
|
},
|
||||||
@@ -580,7 +585,8 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.icon(IconName::Close)
|
.icon(IconName::Close)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("清除订阅过滤")
|
.tooltip(i18n::t("清除订阅过滤"))
|
||||||
|
.accessibility_label(i18n::t("清除订阅过滤"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.sub_filter = None;
|
this.sub_filter = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -629,7 +635,7 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.px_1p5()
|
.px_1p5()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
// 计数显示过滤命中总数,不受渲染截断影响
|
// 计数显示过滤命中总数,不受渲染截断影响
|
||||||
.child(format!("{} 条", matched_total)),
|
.child(i18n::tf("{n} 条", &[("n", &matched_total.to_string())])),
|
||||||
)
|
)
|
||||||
.when(matched_total > records.len(), |h| {
|
.when(matched_total > records.len(), |h| {
|
||||||
h.child(
|
h.child(
|
||||||
@@ -637,7 +643,10 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.px_1p5()
|
.px_1p5()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(format!("仅显示最新 {} 条", MAX_RENDERED_MESSAGES)),
|
.child(i18n::tf(
|
||||||
|
"仅显示最新 {n} 条",
|
||||||
|
&[("n", &MAX_RENDERED_MESSAGES.to_string())],
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child({
|
.child({
|
||||||
@@ -653,14 +662,19 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
} else {
|
} else {
|
||||||
IconName::Pause
|
IconName::Pause
|
||||||
})
|
})
|
||||||
.label(if paused { "恢复" } else { "暂停" })
|
.label(if paused { i18n::t("恢复") } else { i18n::t("暂停") })
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.when(paused, |b| b.selected(true))
|
.when(paused, |b| b.selected(true))
|
||||||
.tooltip(if paused {
|
.tooltip(if paused {
|
||||||
"恢复实时滚动"
|
i18n::t("恢复实时滚动")
|
||||||
} else {
|
} else {
|
||||||
"暂停消息流滚动(消息仍在后台接收)"
|
i18n::t("暂停消息流滚动(消息仍在后台接收)")
|
||||||
|
})
|
||||||
|
.accessibility_label(if paused {
|
||||||
|
i18n::t("恢复")
|
||||||
|
} else {
|
||||||
|
i18n::t("暂停")
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.paused = !this.paused;
|
this.paused = !this.paused;
|
||||||
@@ -675,11 +689,12 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
let empty = total == 0;
|
let empty = total == 0;
|
||||||
Button::new(SharedString::from(format!("msg-clear-{}", self.conn_id)))
|
Button::new(SharedString::from(format!("msg-clear-{}", self.conn_id)))
|
||||||
.icon(IconName::Eraser)
|
.icon(IconName::Eraser)
|
||||||
.label("清空")
|
.label(i18n::t("清空"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.disabled(empty)
|
.disabled(empty)
|
||||||
.tooltip("清空当前连接的消息")
|
.tooltip(i18n::t("清空当前连接的消息"))
|
||||||
|
.accessibility_label(i18n::t("清空当前连接的消息"))
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
// 清空不可撤销,先弹二次确认
|
// 清空不可撤销,先弹二次确认
|
||||||
let weak = weak.clone();
|
let weak = weak.clone();
|
||||||
@@ -688,13 +703,13 @@ pub(super) fn render_messages(&self, cx: &mut Context<Self>) -> impl IntoElement
|
|||||||
let weak = weak.clone();
|
let weak = weak.clone();
|
||||||
let clear_id = clear_id.clone();
|
let clear_id = clear_id.clone();
|
||||||
alert
|
alert
|
||||||
.title("清空消息")
|
.title(i18n::t("清空消息"))
|
||||||
.description("确定清空当前连接的全部消息吗?此操作不可撤销。")
|
.description(i18n::t("确定清空当前连接的全部消息吗?此操作不可撤销。"))
|
||||||
.button_props(
|
.button_props(
|
||||||
DialogButtonProps::default()
|
DialogButtonProps::default()
|
||||||
.show_cancel(true)
|
.show_cancel(true)
|
||||||
.cancel_text("取消")
|
.cancel_text(i18n::t("取消"))
|
||||||
.ok_text("清空")
|
.ok_text(i18n::t("清空"))
|
||||||
.ok_variant(ButtonVariant::Danger),
|
.ok_variant(ButtonVariant::Danger),
|
||||||
)
|
)
|
||||||
.on_ok(move |_, _, cx| {
|
.on_ok(move |_, _, cx| {
|
||||||
@@ -754,7 +769,7 @@ pub(super) fn render_logs(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child("暂无日志"),
|
.child(i18n::t("暂无日志")),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -815,7 +830,7 @@ pub(super) fn render_logs(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.font_family(cx.theme().mono_font_family.clone())
|
.font_family(cx.theme().mono_font_family.clone())
|
||||||
.child("连接日志"),
|
.child(i18n::t("连接日志")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(body)
|
.child(body)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use gpui_kit::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::model::ConnectionConfig;
|
use crate::model::ConnectionConfig;
|
||||||
|
use crate::ui::i18n;
|
||||||
use crate::mqtt::MqttEngine;
|
use crate::mqtt::MqttEngine;
|
||||||
use crate::ui::app::MqttXApp;
|
use crate::ui::app::MqttXApp;
|
||||||
use crate::ui::widgets::{make_select, KvEditor, OptionDelegate};
|
use crate::ui::widgets::{make_select, KvEditor, OptionDelegate};
|
||||||
@@ -39,7 +40,9 @@ use util::*;
|
|||||||
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"];
|
const QOS: [&str; 3] = ["QoS 0", "QoS 1", "QoS 2"];
|
||||||
const PAYLOAD_FORMATS: [&str; 4] = ["Plaintext", "JSON", "Base64", "Hex"];
|
const PAYLOAD_FORMATS: [&str; 4] = ["Plaintext", "JSON", "Base64", "Hex"];
|
||||||
/// Retain Handling 0/1/2 的下拉文案
|
/// Retain Handling 0/1/2 的下拉文案
|
||||||
const RETAIN_HANDLING: [&str; 3] = ["0 每次发送", "1 仅新订阅", "2 不发送"];
|
fn retain_handling_labels() -> [&'static str; 3] {
|
||||||
|
[i18n::t("0 每次发送"), i18n::t("1 仅新订阅"), i18n::t("2 不发送")]
|
||||||
|
}
|
||||||
const MAX_RENDERED_MESSAGES: usize = 300;
|
const MAX_RENDERED_MESSAGES: usize = 300;
|
||||||
/// 日志渲染上限,与 app.rs 的 MAX_LOGS 保持一致
|
/// 日志渲染上限,与 app.rs 的 MAX_LOGS 保持一致
|
||||||
const MAX_RENDERED_LOGS: usize = 3000;
|
const MAX_RENDERED_LOGS: usize = 3000;
|
||||||
@@ -205,29 +208,29 @@ impl ConnectionView {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
||||||
let sub_topic = cx.new(|cx| {
|
let sub_topic = cx.new(|cx| {
|
||||||
InputState::new(window, cx).placeholder("订阅主题,支持 # +,多个用逗号/空格分隔")
|
InputState::new(window, cx).placeholder(i18n::t("订阅主题,支持 # +,多个用逗号/空格分隔"))
|
||||||
});
|
});
|
||||||
let sub_qos = make_select(&QOS, 0, window, cx);
|
let sub_qos = make_select(&QOS, 0, window, cx);
|
||||||
let sub_alias = cx.new(|cx| InputState::new(window, cx).placeholder("别名(可选)"));
|
let sub_alias = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("别名(可选)")));
|
||||||
let sub_identifier = cx.new(|cx| InputState::new(window, cx).placeholder("订阅标识符"));
|
let sub_identifier = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("订阅标识符")));
|
||||||
let sub_retain_handling = make_select(&RETAIN_HANDLING, 0, window, cx);
|
let sub_retain_handling = make_select(&retain_handling_labels(), 0, window, cx);
|
||||||
let pub_topic = cx.new(|cx| InputState::new(window, cx).placeholder("发布主题"));
|
let pub_topic = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("发布主题")));
|
||||||
let payload = cx.new(|cx| {
|
let payload = cx.new(|cx| {
|
||||||
TextareaState::new(window, cx)
|
TextareaState::new(window, cx)
|
||||||
.placeholder("输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}")
|
.placeholder(i18n::t("输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}"))
|
||||||
});
|
});
|
||||||
let pub_qos = make_select(&QOS, 0, window, cx);
|
let pub_qos = make_select(&QOS, 0, window, cx);
|
||||||
let payload_format = make_select(&PAYLOAD_FORMATS, 0, window, cx);
|
let payload_format = make_select(&PAYLOAD_FORMATS, 0, window, cx);
|
||||||
let content_type = cx.new(|cx| InputState::new(window, cx).placeholder("Content-Type(可选)"));
|
let content_type = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("Content-Type(可选)")));
|
||||||
let msg_expiry = cx.new(|cx| InputState::new(window, cx).placeholder("秒,如 60"));
|
let msg_expiry = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("秒,如 60")));
|
||||||
let response_topic =
|
let response_topic =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("Response Topic(可选)"));
|
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("Response Topic(可选)")));
|
||||||
let correlation_data =
|
let correlation_data =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("Correlation Data(可选)"));
|
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("Correlation Data(可选)")));
|
||||||
let filter = cx.new(|cx| InputState::new(window, cx).placeholder("过滤主题或内容…"));
|
let filter = cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("过滤主题或内容…")));
|
||||||
let user_props = cx.new(|cx| KvEditor::new("pub", &[], window, cx));
|
let user_props = cx.new(|cx| KvEditor::new("pub", &[], window, cx));
|
||||||
let preset_name =
|
let preset_name =
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("预设名称,如 温度上报"));
|
cx.new(|cx| InputState::new(window, cx).placeholder(i18n::t("预设名称,如 温度上报")));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
conn_id,
|
conn_id,
|
||||||
@@ -290,6 +293,13 @@ impl ConnectionView {
|
|||||||
self.paused
|
self.paused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ctrl+F:把焦点移到消息过滤框(键盘直达过滤入口)。
|
||||||
|
pub fn focus_filter(&self, window: &mut Window, cx: &mut App) {
|
||||||
|
let handle = gpui_kit::component::input::AnyInputState::from(self.filter.clone())
|
||||||
|
.focus_handle(cx);
|
||||||
|
window.focus(&handle, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +332,7 @@ impl Render for ConnectionView {
|
|||||||
.selected_index(if panel == Panel::Messages { 0 } else { 1 })
|
.selected_index(if panel == Panel::Messages { 0 } else { 1 })
|
||||||
.child(
|
.child(
|
||||||
Tab::new()
|
Tab::new()
|
||||||
.label("消息流")
|
.label(i18n::t("消息流"))
|
||||||
.selected(panel == Panel::Messages)
|
.selected(panel == Panel::Messages)
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.panel = Panel::Messages;
|
this.panel = Panel::Messages;
|
||||||
@@ -331,7 +341,7 @@ impl Render for ConnectionView {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Tab::new()
|
Tab::new()
|
||||||
.label("日志")
|
.label(i18n::t("日志"))
|
||||||
.selected(panel == Panel::Logs)
|
.selected(panel == Panel::Logs)
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.panel = Panel::Logs;
|
this.panel = Panel::Logs;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::model::{ render_template, PayloadFormat, PublishParams, };
|
|||||||
use crate::ui::widgets::{ field, KvEditor };
|
use crate::ui::widgets::{ field, KvEditor };
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl ConnectionView {
|
impl ConnectionView {
|
||||||
/// 发布栏收起后的细条:抽屉把手。
|
/// 发布栏收起后的细条:抽屉把手。
|
||||||
@@ -35,7 +36,8 @@ impl ConnectionView {
|
|||||||
.icon(IconName::PanelBottomOpen)
|
.icon(IconName::PanelBottomOpen)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("展开发布面板")
|
.tooltip(i18n::t("展开发布面板"))
|
||||||
|
.accessibility_label(i18n::t("展开发布面板"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.publish_collapsed = false;
|
this.publish_collapsed = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -45,7 +47,7 @@ impl ConnectionView {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child("发布面板已收起"),
|
.child(i18n::t("发布面板已收起")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,18 +56,18 @@ impl ConnectionView {
|
|||||||
pub(crate) fn do_publish(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn do_publish(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
// 未连接时消息发不出去,先拦截给出与订阅路径一致的提示
|
// 未连接时消息发不出去,先拦截给出与订阅路径一致的提示
|
||||||
if !self.engine.is_connected(&self.conn_id) {
|
if !self.engine.is_connected(&self.conn_id) {
|
||||||
window.push_notification(Notification::warning("未连接:消息未发送"), cx);
|
window.push_notification(Notification::warning(i18n::t("未连接:消息未发送")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut params = self.collect_publish_params(cx);
|
let mut params = self.collect_publish_params(cx);
|
||||||
if params.topic.trim().is_empty() {
|
if params.topic.trim().is_empty() {
|
||||||
window.push_notification(Notification::warning("发布主题不能为空"), cx);
|
window.push_notification(Notification::warning(i18n::t("发布主题不能为空")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 过期秒数输入非法时 collect 会静默得到 None,这里显式拦截避免误发
|
// 过期秒数输入非法时 collect 会静默得到 None,这里显式拦截避免误发
|
||||||
let expiry_raw = self.msg_expiry.read(cx).value().to_string();
|
let expiry_raw = self.msg_expiry.read(cx).value().to_string();
|
||||||
if !expiry_raw.trim().is_empty() && params.message_expiry_interval.is_none() {
|
if !expiry_raw.trim().is_empty() && params.message_expiry_interval.is_none() {
|
||||||
window.push_notification(Notification::warning("消息过期须为非负整数(秒)"), cx);
|
window.push_notification(Notification::warning(i18n::t("消息过期须为非负整数(秒)")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ impl ConnectionView {
|
|||||||
params.payload = render_template(¶ms.payload, &vars);
|
params.payload = render_template(¶ms.payload, &vars);
|
||||||
// {{topic}} 等变量渲染后可能变空串,渲染完成后再校验一次
|
// {{topic}} 等变量渲染后可能变空串,渲染完成后再校验一次
|
||||||
if params.topic.trim().is_empty() {
|
if params.topic.trim().is_empty() {
|
||||||
window.push_notification(Notification::warning("发布主题不能为空"), cx);
|
window.push_notification(Notification::warning(i18n::t("发布主题不能为空")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// {{变量}} 同样作用于用户属性的 key/value
|
// {{变量}} 同样作用于用户属性的 key/value
|
||||||
@@ -91,7 +93,7 @@ impl ConnectionView {
|
|||||||
if let PayloadFormat::Json = params.payload_format
|
if let PayloadFormat::Json = params.payload_format
|
||||||
&& let Err(e) = serde_json::from_str::<serde_json::Value>(¶ms.payload) {
|
&& let Err(e) = serde_json::from_str::<serde_json::Value>(¶ms.payload) {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::warning(format!("JSON 格式无效: {e}")),
|
Notification::warning(i18n::tf("JSON 格式无效: {e}", &[("e", &e.to_string())])),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -127,7 +129,7 @@ impl ConnectionView {
|
|||||||
self.engine.publish(self.conn_id.clone(), params);
|
self.engine.publish(self.conn_id.clone(), params);
|
||||||
if hidden {
|
if hidden {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::success("已发送(被当前过滤条件隐藏,消息流中不显示)"),
|
Notification::success(i18n::t("已发送(被当前过滤条件隐藏,消息流中不显示)")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -277,10 +279,14 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.small()
|
.small()
|
||||||
.when(show_vars, |b| b.selected(true))
|
.when(show_vars, |b| b.selected(true))
|
||||||
.tooltip(if count > 0 {
|
.tooltip(if count > 0 {
|
||||||
format!("变量注入({count} 个占位符)")
|
i18n::tf(
|
||||||
|
"变量注入({n} 个占位符)",
|
||||||
|
&[("n", &count.to_string())],
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
"变量注入".to_string()
|
i18n::t("变量注入").to_string()
|
||||||
})
|
})
|
||||||
|
.accessibility_label(i18n::t("变量注入").to_string())
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
if this.show_vars {
|
if this.show_vars {
|
||||||
this.show_vars = false;
|
this.show_vars = false;
|
||||||
@@ -309,7 +315,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.when(show_props, |b| b.selected(true))
|
.when(show_props, |b| b.selected(true))
|
||||||
.tooltip("MQTT 5 属性")
|
.tooltip(i18n::t("MQTT 5 属性"))
|
||||||
|
.accessibility_label(i18n::t("MQTT 5 属性"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.show_props = !this.show_props;
|
this.show_props = !this.show_props;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -319,12 +326,12 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("publish-{}", self.conn_id)))
|
Button::new(SharedString::from(format!("publish-{}", self.conn_id)))
|
||||||
.icon(IconName::SendHorizontal)
|
.icon(IconName::SendHorizontal)
|
||||||
.label("发送")
|
.label(i18n::t("发送"))
|
||||||
.primary()
|
.primary()
|
||||||
.small()
|
.small()
|
||||||
// 未连接时禁用,Ctrl+Enter 路径由 do_publish 内的前置检查兜底
|
// 未连接时禁用,Ctrl+Enter 路径由 do_publish 内的前置检查兜底
|
||||||
.disabled(!connected)
|
.disabled(!connected)
|
||||||
.when(!connected, |b| b.tooltip("未连接,无法发送"))
|
.when(!connected, |b| b.tooltip(i18n::t("未连接,无法发送")))
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.do_publish(window, cx))),
|
.on_click(cx.listener(|this, _, window, cx| this.do_publish(window, cx))),
|
||||||
)
|
)
|
||||||
// 发布栏抽屉把手:收起整个底部面板
|
// 发布栏抽屉把手:收起整个底部面板
|
||||||
@@ -333,7 +340,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.icon(IconName::PanelBottom)
|
.icon(IconName::PanelBottom)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("收起发布面板")
|
.tooltip(i18n::t("收起发布面板"))
|
||||||
|
.accessibility_label(i18n::t("收起发布面板"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.publish_collapsed = true;
|
this.publish_collapsed = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -412,7 +420,8 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.icon(IconName::Bookmark)
|
.icon(IconName::Bookmark)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("消息预设")
|
.tooltip(i18n::t("消息预设"))
|
||||||
|
.accessibility_label(i18n::t("消息预设"))
|
||||||
.dropdown_menu(move |mut menu, window, cx| {
|
.dropdown_menu(move |mut menu, window, cx| {
|
||||||
// 应用预设
|
// 应用预设
|
||||||
for p in &presets {
|
for p in &presets {
|
||||||
@@ -436,7 +445,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
// 保存当前
|
// 保存当前
|
||||||
let weak_save = weak_view.clone();
|
let weak_save = weak_view.clone();
|
||||||
let name_input = preset_name.clone();
|
let name_input = preset_name.clone();
|
||||||
menu = menu.item(PopupMenuItem::new("保存当前为预设…").on_click(
|
menu = menu.item(PopupMenuItem::new(i18n::t("保存当前为预设…")).on_click(
|
||||||
move |_ev, window, cx| {
|
move |_ev, window, cx| {
|
||||||
if let Some(view) = weak_save.upgrade() {
|
if let Some(view) = weak_save.upgrade() {
|
||||||
view.update(cx, |view, cx| {
|
view.update(cx, |view, cx| {
|
||||||
@@ -447,7 +456,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
));
|
));
|
||||||
// 打开预设管理对话框(新建 / 编辑 / 删除)
|
// 打开预设管理对话框(新建 / 编辑 / 删除)
|
||||||
let manage_app = app_weak.clone();
|
let manage_app = app_weak.clone();
|
||||||
menu = menu.item(PopupMenuItem::new("管理预设…").on_click(
|
menu = menu.item(PopupMenuItem::new(i18n::t("管理预设…")).on_click(
|
||||||
move |_ev, window, cx| {
|
move |_ev, window, cx| {
|
||||||
if let Some(app) = manage_app.upgrade() {
|
if let Some(app) = manage_app.upgrade() {
|
||||||
crate::ui::dialogs::presets_dialog::open(app, window, cx);
|
crate::ui::dialogs::presets_dialog::open(app, window, cx);
|
||||||
@@ -468,7 +477,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
let weak2 = weak_del.clone();
|
let weak2 = weak_del.clone();
|
||||||
let id = p.id.clone();
|
let id = p.id.clone();
|
||||||
let name = p.name.clone();
|
let name = p.name.clone();
|
||||||
let label = format!("删除「{}」", p.name);
|
let label = i18n::tf("删除「{name}」", &[("name", &p.name)]);
|
||||||
submenu = submenu.item(
|
submenu = submenu.item(
|
||||||
PopupMenuItem::new(SharedString::from(label)).on_click(
|
PopupMenuItem::new(SharedString::from(label)).on_click(
|
||||||
move |_ev, window, cx| {
|
move |_ev, window, cx| {
|
||||||
@@ -480,15 +489,16 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
let weak = weak.clone();
|
let weak = weak.clone();
|
||||||
let pid = pid.clone();
|
let pid = pid.clone();
|
||||||
alert
|
alert
|
||||||
.title("删除预设")
|
.title(i18n::t("删除预设"))
|
||||||
.description(format!(
|
.description(i18n::tf(
|
||||||
"确定删除预设「{pname}」吗?此操作不可撤销。"
|
"确定删除预设「{name}」吗?此操作不可撤销。",
|
||||||
|
&[("name", &pname)],
|
||||||
))
|
))
|
||||||
.button_props(
|
.button_props(
|
||||||
DialogButtonProps::default()
|
DialogButtonProps::default()
|
||||||
.show_cancel(true)
|
.show_cancel(true)
|
||||||
.cancel_text("取消")
|
.cancel_text(i18n::t("取消"))
|
||||||
.ok_text("删除")
|
.ok_text(i18n::t("删除"))
|
||||||
.ok_variant(ButtonVariant::Danger),
|
.ok_variant(ButtonVariant::Danger),
|
||||||
)
|
)
|
||||||
.on_ok(move |_, _, cx| {
|
.on_ok(move |_, _, cx| {
|
||||||
@@ -538,9 +548,9 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
window.open_dialog(cx, move |dialog, _, _| {
|
window.open_dialog(cx, move |dialog, _, _| {
|
||||||
dialog
|
dialog
|
||||||
.w(px(420.))
|
.w(px(420.))
|
||||||
.title("保存消息预设")
|
.title(i18n::t("保存消息预设"))
|
||||||
.child(v_flex().gap_3().child(
|
.child(v_flex().gap_3().child(
|
||||||
crate::ui::widgets::field("预设名称", Input::new(&name_for_ok)),
|
crate::ui::widgets::field(i18n::t("预设名称"), Input::new(&name_for_ok)),
|
||||||
))
|
))
|
||||||
.footer(
|
.footer(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -549,13 +559,13 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("preset-save-cancel")
|
Button::new("preset-save-cancel")
|
||||||
.label("取消")
|
.label(i18n::t("取消"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(move |_, window, cx| window.close_dialog(cx)),
|
.on_click(move |_, window, cx| window.close_dialog(cx)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("preset-save-ok")
|
Button::new("preset-save-ok")
|
||||||
.label("保存")
|
.label(i18n::t("保存"))
|
||||||
.primary()
|
.primary()
|
||||||
.on_click({
|
.on_click({
|
||||||
let name_input = name_for_ok.clone();
|
let name_input = name_for_ok.clone();
|
||||||
@@ -568,7 +578,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
.to_string();
|
.to_string();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::warning("请填写预设名称"),
|
Notification::warning(i18n::t("请填写预设名称")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -594,9 +604,7 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
&& params.message_expiry_interval.is_none()
|
&& params.message_expiry_interval.is_none()
|
||||||
{
|
{
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::warning(
|
Notification::warning(i18n::t("消息过期须为非负整数(秒)")),
|
||||||
"消息过期须为非负整数(秒)",
|
|
||||||
),
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -613,9 +621,9 @@ pub(super) fn render_publish_bar(&mut self, cx: &mut Context<Self>) -> impl Into
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
let msg = if existed {
|
let msg = if existed {
|
||||||
format!("已覆盖预设「{name}」")
|
i18n::tf("已覆盖预设「{name}」", &[("name", &name)])
|
||||||
} else {
|
} else {
|
||||||
"预设已保存".to_string()
|
i18n::t("预设已保存").to_string()
|
||||||
};
|
};
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::success(msg),
|
Notification::success(msg),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::model::{ SubscribeOptions, Subscription, };
|
|||||||
use crate::ui::widgets::field;
|
use crate::ui::widgets::field;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl ConnectionView {
|
impl ConnectionView {
|
||||||
/// 订阅面板收起后的窄 rail:抽屉把手。
|
/// 订阅面板收起后的窄 rail:抽屉把手。
|
||||||
@@ -34,7 +35,8 @@ impl ConnectionView {
|
|||||||
.icon(IconName::PanelLeftOpen)
|
.icon(IconName::PanelLeftOpen)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("展开订阅面板")
|
.tooltip(i18n::t("展开订阅面板"))
|
||||||
|
.accessibility_label(i18n::t("展开订阅面板"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.subs_collapsed = false;
|
this.subs_collapsed = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -167,7 +169,7 @@ impl ConnectionView {
|
|||||||
let raw = self.sub_topic.read(cx).value().to_string();
|
let raw = self.sub_topic.read(cx).value().to_string();
|
||||||
let topics = split_topic_list(&raw);
|
let topics = split_topic_list(&raw);
|
||||||
if topics.is_empty() {
|
if topics.is_empty() {
|
||||||
window.push_notification(Notification::warning("订阅主题不能为空"), cx);
|
window.push_notification(Notification::warning(i18n::t("订阅主题不能为空")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let qos = self.sub_qos.read(cx).selected_value().copied().unwrap_or(0) as u8;
|
let qos = self.sub_qos.read(cx).selected_value().copied().unwrap_or(0) as u8;
|
||||||
@@ -187,7 +189,7 @@ impl ConnectionView {
|
|||||||
Ok(v) if v > 0 => sub_identifier = Some(v),
|
Ok(v) if v > 0 => sub_identifier = Some(v),
|
||||||
_ => {
|
_ => {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::warning("订阅标识符须为正整数"),
|
Notification::warning(i18n::t("订阅标识符须为正整数")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -301,9 +303,9 @@ impl ConnectionView {
|
|||||||
.map(|c| c.auto_resubscribe)
|
.map(|c| c.auto_resubscribe)
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
"未连接:订阅已保存,连接后自动恢复"
|
i18n::t("未连接:订阅已保存,连接后自动恢复")
|
||||||
} else {
|
} else {
|
||||||
"未连接:订阅已保存"
|
i18n::t("未连接:订阅已保存")
|
||||||
};
|
};
|
||||||
window.push_notification(Notification::warning(hint), cx);
|
window.push_notification(Notification::warning(hint), cx);
|
||||||
}
|
}
|
||||||
@@ -362,7 +364,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
|||||||
.bg(cx.theme().warning.alpha(0.15))
|
.bg(cx.theme().warning.alpha(0.15))
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().warning)
|
.text_color(cx.theme().warning)
|
||||||
.child(format!("正在编辑:{topic}")),
|
.child(i18n::tf("正在编辑:{topic}", &[("topic", &topic)])),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
@@ -401,11 +403,12 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
|||||||
h.child(
|
h.child(
|
||||||
Button::new(SharedString::from(format!("sub-adv-{}", self.conn_id)))
|
Button::new(SharedString::from(format!("sub-adv-{}", self.conn_id)))
|
||||||
.icon(IconName::SlidersHorizontal)
|
.icon(IconName::SlidersHorizontal)
|
||||||
.label("高级")
|
.label(i18n::t("高级"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.when(show_advanced, |b| b.selected(true))
|
.when(show_advanced, |b| b.selected(true))
|
||||||
.tooltip("MQTT 5 订阅选项")
|
.tooltip(i18n::t("MQTT 5 订阅选项"))
|
||||||
|
.accessibility_label(i18n::t("MQTT 5 订阅选项"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.sub_show_advanced = !this.sub_show_advanced;
|
this.sub_show_advanced = !this.sub_show_advanced;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -415,7 +418,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
|||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("subscribe-{}", self.conn_id)))
|
Button::new(SharedString::from(format!("subscribe-{}", self.conn_id)))
|
||||||
.icon(if editing { IconName::Check } else { IconName::Plus })
|
.icon(if editing { IconName::Check } else { IconName::Plus })
|
||||||
.label(if editing { "更新" } else { "订阅" })
|
.label(if editing { i18n::t("更新") } else { i18n::t("订阅") })
|
||||||
// 编辑用 warning 色与新增的 primary 区分
|
// 编辑用 warning 色与新增的 primary 区分
|
||||||
.when(editing, |b| b.warning())
|
.when(editing, |b| b.warning())
|
||||||
.when(!editing, |b| b.primary())
|
.when(!editing, |b| b.primary())
|
||||||
@@ -428,7 +431,8 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
|||||||
.icon(IconName::Close)
|
.icon(IconName::Close)
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.tooltip("取消编辑")
|
.tooltip(i18n::t("取消编辑"))
|
||||||
|
.accessibility_label(i18n::t("取消编辑"))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.editing = None;
|
this.editing = None;
|
||||||
// 清空回填到输入框的主题/别名/高级字段,回到新增态
|
// 清空回填到输入框的主题/别名/高级字段,回到新增态
|
||||||
@@ -466,7 +470,7 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.child(div().text_xs().child("No Local")),
|
.child(div().text_xs().child(i18n::t("No Local"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -479,11 +483,11 @@ pub(super) fn render_subscribe_bar(&mut self, cx: &mut Context<Self>) -> impl In
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.child(div().text_xs().child("Retain As Published")),
|
.child(div().text_xs().child(i18n::t("Retain As Published"))),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(150.)).child(field(
|
div().w(px(150.)).child(field(
|
||||||
"Retain Handling",
|
i18n::t("Retain Handling"),
|
||||||
Select::new(&sub_retain_handling).small(),
|
Select::new(&sub_retain_handling).small(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -523,7 +527,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.text_center()
|
.text_center()
|
||||||
.child("订阅主题后,消息会显示在右侧"),
|
.child(i18n::t("订阅主题后,消息会显示在右侧")),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -544,7 +548,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
Button::new(trigger_id)
|
Button::new(trigger_id)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("订阅颜色")
|
.tooltip(i18n::t("订阅颜色"))
|
||||||
|
.accessibility_label(i18n::t("订阅颜色"))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.size(px(11.))
|
.size(px(11.))
|
||||||
@@ -557,7 +562,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
Button::new(trigger_id)
|
Button::new(trigger_id)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("订阅颜色")
|
.tooltip(i18n::t("订阅颜色"))
|
||||||
|
.accessibility_label(i18n::t("订阅颜色"))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.size(px(11.))
|
.size(px(11.))
|
||||||
@@ -605,7 +611,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
menu = menu.separator();
|
menu = menu.separator();
|
||||||
let weak_rand = weak_menu.clone();
|
let weak_rand = weak_menu.clone();
|
||||||
let sid_rand = sid.clone();
|
let sid_rand = sid.clone();
|
||||||
menu = menu.item(PopupMenuItem::new("随机颜色").on_click(move |_, _, cx| {
|
menu = menu.item(PopupMenuItem::new(i18n::t("随机颜色")).on_click(move |_, _, cx| {
|
||||||
let hue = (uuid::Uuid::new_v4().as_u128() % 360) as f32;
|
let hue = (uuid::Uuid::new_v4().as_u128() % 360) as f32;
|
||||||
weak_rand
|
weak_rand
|
||||||
.update(cx, |view, cx| {
|
.update(cx, |view, cx| {
|
||||||
@@ -615,7 +621,7 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
}));
|
}));
|
||||||
let weak_clear = weak_menu.clone();
|
let weak_clear = weak_menu.clone();
|
||||||
let sid_clear = sid.clone();
|
let sid_clear = sid.clone();
|
||||||
menu = menu.item(PopupMenuItem::new("清除颜色").on_click(move |_, _, cx| {
|
menu = menu.item(PopupMenuItem::new(i18n::t("清除颜色")).on_click(move |_, _, cx| {
|
||||||
weak_clear
|
weak_clear
|
||||||
.update(cx, |view, cx| {
|
.update(cx, |view, cx| {
|
||||||
view.set_subscription_color(&sid_clear, None, cx);
|
view.set_subscription_color(&sid_clear, None, cx);
|
||||||
@@ -631,7 +637,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
.icon(if enabled { IconName::Eye } else { IconName::EyeOff })
|
.icon(if enabled { IconName::Eye } else { IconName::EyeOff })
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip(if enabled { "停用订阅" } else { "启用订阅" })
|
.tooltip(if enabled { i18n::t("停用订阅") } else { i18n::t("启用订阅") })
|
||||||
|
.accessibility_label(if enabled { i18n::t("停用订阅") } else { i18n::t("启用订阅") })
|
||||||
.on_click(cx.listener(move |this, _, _, cx| {
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
this.toggle_subscription_enabled(&eye_sid, cx);
|
this.toggle_subscription_enabled(&eye_sid, cx);
|
||||||
}));
|
}));
|
||||||
@@ -641,7 +648,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
.icon(IconName::Pencil)
|
.icon(IconName::Pencil)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("编辑订阅")
|
.tooltip(i18n::t("编辑订阅"))
|
||||||
|
.accessibility_label(i18n::t("编辑订阅"))
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
let sub = this
|
let sub = this
|
||||||
.with_app(cx, |app| {
|
.with_app(cx, |app| {
|
||||||
@@ -662,7 +670,8 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
.icon(IconName::Close)
|
.icon(IconName::Close)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("取消订阅")
|
.tooltip(i18n::t("取消订阅"))
|
||||||
|
.accessibility_label(i18n::t("取消订阅"))
|
||||||
.on_click(cx.listener(move |this, _, _, cx| {
|
.on_click(cx.listener(move |this, _, _, cx| {
|
||||||
// 若删除的是当前过滤主题,一并清掉消息过滤
|
// 若删除的是当前过滤主题,一并清掉消息过滤
|
||||||
if this.sub_filter.as_deref() == Some(del_topic.as_str()) {
|
if this.sub_filter.as_deref() == Some(del_topic.as_str()) {
|
||||||
@@ -769,13 +778,14 @@ pub(super) fn render_subscriptions(&self, cx: &mut Context<Self>) -> impl IntoEl
|
|||||||
.px_3()
|
.px_3()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(div().text_sm().font_semibold().child("订阅"))
|
.child(div().text_sm().font_semibold().child(i18n::t("订阅")))
|
||||||
.child(
|
.child(
|
||||||
Button::new("subs-collapse")
|
Button::new("subs-collapse")
|
||||||
.icon(IconName::PanelLeftClose)
|
.icon(IconName::PanelLeftClose)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("收起订阅面板")
|
.tooltip(i18n::t("收起订阅面板"))
|
||||||
|
.accessibility_label(i18n::t("收起订阅面板"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.subs_collapsed = true;
|
this.subs_collapsed = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
@@ -157,54 +157,6 @@ pub(super) fn details_copy_text(record: &MqttRecord, show_millis: bool) -> Strin
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 折叠态预览行数与字符上限:JSON 美化后多行完整展示,超大负载兜底截断
|
|
||||||
const PREVIEW_MAX_LINES: usize = 40;
|
|
||||||
const PREVIEW_MAX_CHARS: usize = 4000;
|
|
||||||
|
|
||||||
/// 折叠态预览:JSON 负载按原始数据美化展示(缩进多行),非 JSON
|
|
||||||
/// 压平换行/制表符截 200 字符;含替换字符(U+FFFD)提示切 Hex。
|
|
||||||
pub(super) fn preview_payload(payload: &str) -> String {
|
|
||||||
let parsed = serde_json::from_str::<serde_json::Value>(payload).ok();
|
|
||||||
let mut out = match &parsed {
|
|
||||||
Some(value) => {
|
|
||||||
let pretty = serde_json::to_string_pretty(value).unwrap_or_default();
|
|
||||||
truncate_preview(pretty)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let flat: String = payload
|
|
||||||
.chars()
|
|
||||||
.map(|c| match c {
|
|
||||||
'\n' | '\r' | '\t' => ' ',
|
|
||||||
_ => c,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let mut s: String = flat.chars().take(200).collect();
|
|
||||||
if flat.chars().count() > 200 {
|
|
||||||
s.push('…');
|
|
||||||
}
|
|
||||||
s
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if payload.contains('\u{FFFD}') {
|
|
||||||
out.push_str("(非文本,详情可切 Hex)");
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 截断预览文本:最多 [`PREVIEW_MAX_LINES`] 行、[`PREVIEW_MAX_CHARS`] 字符。
|
|
||||||
fn truncate_preview(text: String) -> String {
|
|
||||||
let mut lines: Vec<&str> = text.lines().take(PREVIEW_MAX_LINES).collect();
|
|
||||||
let more = text.lines().count() > PREVIEW_MAX_LINES;
|
|
||||||
if more {
|
|
||||||
lines.push("…");
|
|
||||||
}
|
|
||||||
let mut out = lines.join("\n");
|
|
||||||
if out.chars().count() > PREVIEW_MAX_CHARS {
|
|
||||||
out = out.chars().take(PREVIEW_MAX_CHARS).collect::<String>() + "…";
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 大小写不敏感子串匹配。`needle` 已由调用方 to_lowercase 一次;
|
/// 大小写不敏感子串匹配。`needle` 已由调用方 to_lowercase 一次;
|
||||||
/// 先做零拷贝的字面匹配,未命中且原文含大写时才回退整串小写,
|
/// 先做零拷贝的字面匹配,未命中且原文含大写时才回退整串小写,
|
||||||
/// 避免渲染热路径对全部记录做 to_lowercase 拷贝。
|
/// 避免渲染热路径对全部记录做 to_lowercase 拷贝。
|
||||||
@@ -233,7 +185,6 @@ pub(super) fn contains_ignore_case(hay: &str, needle: &str) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::extract_placeholder_keys;
|
use super::extract_placeholder_keys;
|
||||||
use super::preview_payload;
|
|
||||||
|
|
||||||
// 修复回归:提取须 trim、排除内置变量、去重且保持首次出现顺序
|
// 修复回归:提取须 trim、排除内置变量、去重且保持首次出现顺序
|
||||||
#[test]
|
#[test]
|
||||||
@@ -255,31 +206,4 @@ mod tests {
|
|||||||
assert_eq!(keys, vec!["名称"]);
|
assert_eq!(keys, vec!["名称"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn preview_json_is_pretty_and_complete_for_typical_payloads() {
|
|
||||||
let payload = r#"{"type":"heartbeat","ts":1790751279869,"data":{"version":"0.1.0","lon":117.229,"lat":31.8206,"uptime":1716450,"rssi":-52}}"#;
|
|
||||||
let out = preview_payload(payload);
|
|
||||||
// JSON 被美化(多行缩进)且常规负载完整展示、不带任何加工注释
|
|
||||||
assert!(out.contains('\n'), "JSON 应多行展示: {out}");
|
|
||||||
assert!(out.contains("\"rssi\": -52"), "常规负载应完整显示: {out}");
|
|
||||||
assert!(!out.contains("//"), "预览不应附加注释: {out}");
|
|
||||||
assert!(!out.contains('…'), "常规负载不应截断: {out}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn preview_truncates_very_deep_json() {
|
|
||||||
// 50 个元素的数组美化后超过 40 行,应截断并带省略号
|
|
||||||
let items: Vec<String> = (0..50).map(|i| i.to_string()).collect();
|
|
||||||
let payload = format!("{{\"a\":[{}]}}", items.join(","));
|
|
||||||
let deep = preview_payload(&payload);
|
|
||||||
assert!(deep.lines().count() <= 41, "超行应截断: {deep}");
|
|
||||||
assert!(deep.contains('…'), "截断应有省略号: {deep}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn preview_non_json_still_flat() {
|
|
||||||
assert_eq!(preview_payload("hello\nworld"), "hello world");
|
|
||||||
assert_eq!(preview_payload("纯文本"), "纯文本");
|
|
||||||
assert!(preview_payload("bad\ntext\u{FFFD}").contains("非文本"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use gpui_kit::{ div, px, Context, IntoElement, Window, };
|
|||||||
use crate::model::{ render_template, GlobalVariable, };
|
use crate::model::{ render_template, GlobalVariable, };
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
impl ConnectionView {
|
impl ConnectionView {
|
||||||
/// 输入框创建需要 Window,因此只能在按钮回调里调用(无法在 render 中同步)。
|
/// 输入框创建需要 Window,因此只能在按钮回调里调用(无法在 render 中同步)。
|
||||||
@@ -20,7 +21,7 @@ pub(super) fn sync_var_rows(&mut self, window: &mut Window, cx: &mut Context<Sel
|
|||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
self.show_vars = false;
|
self.show_vars = false;
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::warning("主题/负载中没有 {{变量}} 引用"),
|
Notification::warning(i18n::t("主题/负载中没有 {{变量}} 引用")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -39,7 +40,7 @@ pub(super) fn sync_var_rows(&mut self, window: &mut Window, cx: &mut Context<Sel
|
|||||||
.map(|v| v.value.clone())
|
.map(|v| v.value.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
let mut s = InputState::new(window, cx).placeholder("变量值");
|
let mut s = InputState::new(window, cx).placeholder(i18n::t("变量值"));
|
||||||
// set_value 不会触发 InputEvent::Change,不会误写回
|
// set_value 不会触发 InputEvent::Change,不会误写回
|
||||||
if !initial.is_empty() {
|
if !initial.is_empty() {
|
||||||
s.set_value(initial.as_str(), window, cx);
|
s.set_value(initial.as_str(), window, cx);
|
||||||
@@ -117,7 +118,10 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(format!("变量({})", self.var_rows.len())),
|
.child(i18n::tf(
|
||||||
|
"变量({n})",
|
||||||
|
&[("n", &self.var_rows.len().to_string())],
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
.child(
|
.child(
|
||||||
@@ -125,7 +129,8 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
|||||||
.icon(IconName::RefreshCw)
|
.icon(IconName::RefreshCw)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("重新提取占位符")
|
.tooltip(i18n::t("重新提取占位符"))
|
||||||
|
.accessibility_label(i18n::t("重新提取占位符"))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.sync_var_rows(window, cx);
|
this.sync_var_rows(window, cx);
|
||||||
})),
|
})),
|
||||||
@@ -135,7 +140,8 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
|||||||
.icon(IconName::ChevronUp)
|
.icon(IconName::ChevronUp)
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.tooltip("收起")
|
.tooltip(i18n::t("收起"))
|
||||||
|
.accessibility_label(i18n::t("收起"))
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
this.show_vars = false;
|
this.show_vars = false;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -147,13 +153,16 @@ pub(super) fn render_vars_panel(&self, cx: &mut Context<Self>) -> impl IntoEleme
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child("内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入"),
|
.child(i18n::t("内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child(format!("预览:{topic_rendered} | {payload_preview}")),
|
.child(i18n::tf(
|
||||||
|
"预览:{tp} | {pl}",
|
||||||
|
&[("tp", &topic_rendered), ("pl", &payload_preview)],
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::aliyun::{AliyunAuthMode, AliyunPreset};
|
|||||||
use crate::model::{render_will_templates, ConnectionConfig};
|
use crate::model::{render_will_templates, ConnectionConfig};
|
||||||
use crate::ui::app::MqttXApp;
|
use crate::ui::app::MqttXApp;
|
||||||
use crate::ui::widgets::{field, make_select, OptionDelegate};
|
use crate::ui::widgets::{field, make_select, OptionDelegate};
|
||||||
|
use crate::ui::i18n;
|
||||||
|
|
||||||
const AUTH_MODES: [&str; 2] = ["Group 令牌 (Token)", "一机一密"];
|
const AUTH_MODES: [&str; 2] = ["Group 令牌 (Token)", "一机一密"];
|
||||||
|
|
||||||
@@ -59,10 +60,10 @@ impl AliyunDialog {
|
|||||||
app,
|
app,
|
||||||
presets,
|
presets,
|
||||||
selected,
|
selected,
|
||||||
name: text_field!(cx, window, &first.name, "预设名称"),
|
name: text_field!(cx, window, &first.name, i18n::t("预设名称")),
|
||||||
group: text_field!(cx, window, &first.group, "分组(可选)"),
|
group: text_field!(cx, window, &first.group, i18n::t("分组(可选)")),
|
||||||
instance_id: text_field!(cx, window, &first.instance_id, "post-cn-xxxx"),
|
instance_id: text_field!(cx, window, &first.instance_id, "post-cn-xxxx"),
|
||||||
region: text_field!(cx, window, &first.region, "如 cn-shanghai(可留空)"),
|
region: text_field!(cx, window, &first.region, i18n::t("如 cn-shanghai(可留空)")),
|
||||||
auth_mode: make_select(
|
auth_mode: make_select(
|
||||||
&AUTH_MODES,
|
&AUTH_MODES,
|
||||||
match first.auth_mode {
|
match first.auth_mode {
|
||||||
@@ -80,12 +81,12 @@ impl AliyunDialog {
|
|||||||
.masked(true)
|
.masked(true)
|
||||||
}),
|
}),
|
||||||
group_id: text_field!(cx, window, &first.group_id, "GID-xxx"),
|
group_id: text_field!(cx, window, &first.group_id, "GID-xxx"),
|
||||||
device_id: text_field!(cx, window, &first.device_id, "设备 ID"),
|
device_id: text_field!(cx, window, &first.device_id, i18n::t("设备 ID")),
|
||||||
product_key: text_field!(cx, window, &first.product_key, "一机一密:ProductKey"),
|
product_key: text_field!(cx, window, &first.product_key, i18n::t("一机一密:ProductKey")),
|
||||||
device_secret: cx.new(|cx| {
|
device_secret: cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.default_value(&first.device_secret)
|
.default_value(&first.device_secret)
|
||||||
.placeholder("一机一密:DeviceSecret")
|
.placeholder(i18n::t("一机一密:DeviceSecret"))
|
||||||
.masked(true)
|
.masked(true)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -185,7 +186,7 @@ impl AliyunDialog {
|
|||||||
fn save_preset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn save_preset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let preset = self.collect(cx);
|
let preset = self.collect(cx);
|
||||||
if preset.name.trim().is_empty() {
|
if preset.name.trim().is_empty() {
|
||||||
window.push_notification(Notification::warning("请填写预设名称"), cx);
|
window.push_notification(Notification::warning(i18n::t("请填写预设名称")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match self.selected {
|
match self.selected {
|
||||||
@@ -197,7 +198,7 @@ impl AliyunDialog {
|
|||||||
a.save_aliyun(saved);
|
a.save_aliyun(saved);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
window.push_notification(Notification::success("预设已保存"), cx);
|
window.push_notification(Notification::success(i18n::t("预设已保存")), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn connect(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@@ -216,7 +217,7 @@ impl AliyunDialog {
|
|||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
window.push_notification(Notification::error(format!("无法生成连接: {e}")), cx);
|
window.push_notification(Notification::error(i18n::tf("无法生成连接: {e}", &[("e", &e.to_string())])), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,7 +233,7 @@ impl Render for AliyunDialog {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.w(px(160.))
|
.w(px(160.))
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(div().text_xs().font_semibold().child("已保存预设"))
|
.child(div().text_xs().font_semibold().child(i18n::t("已保存预设")))
|
||||||
.children(
|
.children(
|
||||||
self.presets
|
self.presets
|
||||||
.iter()
|
.iter()
|
||||||
@@ -256,7 +257,7 @@ impl Render for AliyunDialog {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("aliyun-new")
|
Button::new("aliyun-new")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.label("新建")
|
.label(i18n::t("新建"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -270,36 +271,36 @@ impl Render for AliyunDialog {
|
|||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w(px(0.))
|
.min_w(px(0.))
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(field("预设名称", Input::new(&self.name)))
|
.child(field(i18n::t("预设名称"), Input::new(&self.name)))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"实例 ID",
|
i18n::t("实例 ID"),
|
||||||
Input::new(&self.instance_id),
|
Input::new(&self.instance_id),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"地域",
|
i18n::t("地域"),
|
||||||
Input::new(&self.region),
|
Input::new(&self.region),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(field("鉴权方式", Select::new(&self.auth_mode)))
|
.child(field(i18n::t("鉴权方式"), Select::new(&self.auth_mode)))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"AccessKey ID",
|
i18n::t("AccessKey ID"),
|
||||||
Input::new(&self.access_key_id),
|
Input::new(&self.access_key_id),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"AccessKey Secret",
|
i18n::t("AccessKey Secret"),
|
||||||
Input::new(&self.access_key_secret).mask_toggle(),
|
Input::new(&self.access_key_secret).mask_toggle(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -309,13 +310,13 @@ impl Render for AliyunDialog {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"Group ID",
|
i18n::t("Group ID"),
|
||||||
Input::new(&self.group_id),
|
Input::new(&self.group_id),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"设备 ID",
|
i18n::t("设备 ID"),
|
||||||
Input::new(&self.device_id),
|
Input::new(&self.device_id),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -325,13 +326,13 @@ impl Render for AliyunDialog {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"ProductKey(一机一密)",
|
i18n::t("ProductKey(一机一密)"),
|
||||||
Input::new(&self.product_key),
|
Input::new(&self.product_key),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex_1().min_w(px(0.)).child(field(
|
div().flex_1().min_w(px(0.)).child(field(
|
||||||
"DeviceSecret(一机一密)",
|
i18n::t("DeviceSecret(一机一密)"),
|
||||||
Input::new(&self.device_secret).mask_toggle(),
|
Input::new(&self.device_secret).mask_toggle(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -348,7 +349,7 @@ impl Render for AliyunDialog {
|
|||||||
.justify_end()
|
.justify_end()
|
||||||
.child(
|
.child(
|
||||||
Button::new("aliyun-save")
|
Button::new("aliyun-save")
|
||||||
.label("保存预设")
|
.label(i18n::t("保存预设"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.save_preset(window, cx)
|
this.save_preset(window, cx)
|
||||||
@@ -356,7 +357,7 @@ impl Render for AliyunDialog {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("aliyun-connect")
|
Button::new("aliyun-connect")
|
||||||
.label("生成并连接")
|
.label(i18n::t("生成并连接"))
|
||||||
.primary()
|
.primary()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
this.connect(window, cx)
|
this.connect(window, cx)
|
||||||
@@ -378,7 +379,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
window.open_dialog(cx, move |dialog, _, _| {
|
window.open_dialog(cx, move |dialog, _, _| {
|
||||||
dialog
|
dialog
|
||||||
.w(px(760.))
|
.w(px(760.))
|
||||||
.title("阿里云 IoT 设备")
|
.title(i18n::t("阿里云 IoT 设备"))
|
||||||
.child(dialog_view.clone())
|
.child(dialog_view.clone())
|
||||||
.footer(gpui_kit::div())
|
.footer(gpui_kit::div())
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use gpui_kit::component::{
|
|||||||
};
|
};
|
||||||
use gpui_kit::prelude::FluentBuilder as _;
|
use gpui_kit::prelude::FluentBuilder as _;
|
||||||
use crate::ui::IconName;
|
use crate::ui::IconName;
|
||||||
|
use crate::ui::i18n;
|
||||||
use gpui_kit::{
|
use gpui_kit::{
|
||||||
div, px, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
div, px, App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement,
|
||||||
ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window,
|
ParentElement as _, Render, StatefulInteractiveElement as _, Styled as _, Window,
|
||||||
@@ -80,17 +81,17 @@ impl PresetsDialog {
|
|||||||
let name_value = first.as_ref().map(|p| p.name.clone()).unwrap_or_default();
|
let name_value = first.as_ref().map(|p| p.name.clone()).unwrap_or_default();
|
||||||
let name = cx.new(|cx| {
|
let name = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("预设名称,如 温度上报")
|
.placeholder(i18n::t("预设名称,如 温度上报"))
|
||||||
.default_value(name_value)
|
.default_value(name_value)
|
||||||
});
|
});
|
||||||
let topic = cx.new(|cx| {
|
let topic = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("发布主题,支持 {{变量名}}")
|
.placeholder(i18n::t("发布主题,支持 {{变量名}}"))
|
||||||
.default_value(params.topic.clone())
|
.default_value(params.topic.clone())
|
||||||
});
|
});
|
||||||
let payload = cx.new(|cx| {
|
let payload = cx.new(|cx| {
|
||||||
TextareaState::new(window, cx)
|
TextareaState::new(window, cx)
|
||||||
.placeholder("消息负载,支持 {{变量名}}")
|
.placeholder(i18n::t("消息负载,支持 {{变量名}}"))
|
||||||
.default_value(params.payload.clone())
|
.default_value(params.payload.clone())
|
||||||
});
|
});
|
||||||
let qos = make_select(&QOS, params.qos.min(2) as usize, window, cx);
|
let qos = make_select(&QOS, params.qos.min(2) as usize, window, cx);
|
||||||
@@ -101,11 +102,11 @@ impl PresetsDialog {
|
|||||||
let payload_format = make_select(&PAYLOAD_FORMATS, fmt_idx, window, cx);
|
let payload_format = make_select(&PAYLOAD_FORMATS, fmt_idx, window, cx);
|
||||||
let content_type = cx.new(|cx| {
|
let content_type = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("Content-Type(可选)")
|
.placeholder(i18n::t("Content-Type(可选)"))
|
||||||
.default_value(params.content_type.clone().unwrap_or_default())
|
.default_value(params.content_type.clone().unwrap_or_default())
|
||||||
});
|
});
|
||||||
let msg_expiry = cx.new(|cx| {
|
let msg_expiry = cx.new(|cx| {
|
||||||
InputState::new(window, cx).placeholder("秒,如 60").default_value(
|
InputState::new(window, cx).placeholder(i18n::t("秒,如 60")).default_value(
|
||||||
params
|
params
|
||||||
.message_expiry_interval
|
.message_expiry_interval
|
||||||
.map(|v| v.to_string())
|
.map(|v| v.to_string())
|
||||||
@@ -114,12 +115,12 @@ impl PresetsDialog {
|
|||||||
});
|
});
|
||||||
let response_topic = cx.new(|cx| {
|
let response_topic = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("Response Topic(可选)")
|
.placeholder(i18n::t("Response Topic(可选)"))
|
||||||
.default_value(params.response_topic.clone().unwrap_or_default())
|
.default_value(params.response_topic.clone().unwrap_or_default())
|
||||||
});
|
});
|
||||||
let correlation_data = cx.new(|cx| {
|
let correlation_data = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder("Correlation Data(可选)")
|
.placeholder(i18n::t("Correlation Data(可选)"))
|
||||||
.default_value(params.correlation_data.clone().unwrap_or_default())
|
.default_value(params.correlation_data.clone().unwrap_or_default())
|
||||||
});
|
});
|
||||||
let user_props =
|
let user_props =
|
||||||
@@ -267,10 +268,10 @@ impl PresetsDialog {
|
|||||||
|
|
||||||
/// 「新建」:创建默认名称预设(重名自动加序号)并立即落盘、进入编辑。
|
/// 「新建」:创建默认名称预设(重名自动加序号)并立即落盘、进入编辑。
|
||||||
fn create_new(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn create_new(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let mut name = "新预设".to_string();
|
let mut name = i18n::t("新预设").to_string();
|
||||||
let mut n = 2;
|
let mut n = 2;
|
||||||
while self.presets.iter().any(|p| p.name == name) {
|
while self.presets.iter().any(|p| p.name == name) {
|
||||||
name = format!("新预设 {n}");
|
name = i18n::tf("新预设 {n}", &[("n", &n.to_string())]);
|
||||||
n += 1;
|
n += 1;
|
||||||
}
|
}
|
||||||
self.presets.push(PublishPreset::new(name, PublishParams::default()));
|
self.presets.push(PublishPreset::new(name, PublishParams::default()));
|
||||||
@@ -282,12 +283,12 @@ impl PresetsDialog {
|
|||||||
/// 「保存」:name 校验 + 冲突检查通过后按 id 更新,再落盘。
|
/// 「保存」:name 校验 + 冲突检查通过后按 id 更新,再落盘。
|
||||||
fn save(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn save(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(idx) = self.selected else {
|
let Some(idx) = self.selected else {
|
||||||
window.push_notification(Notification::warning("请先选择或新建预设"), cx);
|
window.push_notification(Notification::warning(i18n::t("请先选择或新建预设")), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let name = self.name.read(cx).value().trim().to_string();
|
let name = self.name.read(cx).value().trim().to_string();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
window.push_notification(Notification::warning("请填写预设名称"), cx);
|
window.push_notification(Notification::warning(i18n::t("请填写预设名称")), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 与其他预设(不含自身)重名 → 报错且不落盘
|
// 与其他预设(不含自身)重名 → 报错且不落盘
|
||||||
@@ -298,7 +299,10 @@ impl PresetsDialog {
|
|||||||
.any(|p| p.id != own_id && p.name == name)
|
.any(|p| p.id != own_id && p.name == name)
|
||||||
{
|
{
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error(format!("预设名称「{name}」已存在")),
|
Notification::error(i18n::tf(
|
||||||
|
"预设名称「{name}」已存在",
|
||||||
|
&[("name", &name)],
|
||||||
|
)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -308,7 +312,7 @@ impl PresetsDialog {
|
|||||||
let params = self.collect(cx);
|
let params = self.collect(cx);
|
||||||
if !expiry_raw.trim().is_empty() && params.message_expiry_interval.is_none() {
|
if !expiry_raw.trim().is_empty() && params.message_expiry_interval.is_none() {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::warning("消息过期须为非负整数(秒)"),
|
Notification::warning(i18n::t("消息过期须为非负整数(秒)")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -319,13 +323,16 @@ impl PresetsDialog {
|
|||||||
p.params = params;
|
p.params = params;
|
||||||
}
|
}
|
||||||
self.persist(cx);
|
self.persist(cx);
|
||||||
window.push_notification(Notification::success(format!("预设「{name}」已保存")), cx);
|
window.push_notification(Notification::success(i18n::tf(
|
||||||
|
"预设「{name}」已保存",
|
||||||
|
&[("name", &name)],
|
||||||
|
)), cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 「删除」:`open_alert_dialog` 二次确认后从列表移除并落盘。
|
/// 「删除」:`open_alert_dialog` 二次确认后从列表移除并落盘。
|
||||||
fn ask_delete(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn ask_delete(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(idx) = self.selected else {
|
let Some(idx) = self.selected else {
|
||||||
window.push_notification(Notification::warning("请先选择要删除的预设"), cx);
|
window.push_notification(Notification::warning(i18n::t("请先选择要删除的预设")), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let pid = self.presets[idx].id.clone();
|
let pid = self.presets[idx].id.clone();
|
||||||
@@ -336,13 +343,16 @@ impl PresetsDialog {
|
|||||||
let weak = weak.clone();
|
let weak = weak.clone();
|
||||||
let pid = pid.clone();
|
let pid = pid.clone();
|
||||||
alert
|
alert
|
||||||
.title("删除预设")
|
.title(i18n::t("删除预设"))
|
||||||
.description(format!("确定删除预设「{pname}」吗?此操作不可撤销。"))
|
.description(i18n::tf(
|
||||||
|
"确定删除预设「{name}」吗?此操作不可撤销。",
|
||||||
|
&[("name", &pname)],
|
||||||
|
))
|
||||||
.button_props(
|
.button_props(
|
||||||
DialogButtonProps::default()
|
DialogButtonProps::default()
|
||||||
.show_cancel(true)
|
.show_cancel(true)
|
||||||
.cancel_text("取消")
|
.cancel_text(i18n::t("取消"))
|
||||||
.ok_text("删除")
|
.ok_text(i18n::t("删除"))
|
||||||
.ok_variant(ButtonVariant::Danger),
|
.ok_variant(ButtonVariant::Danger),
|
||||||
)
|
)
|
||||||
.on_ok(move |_, window, cx| {
|
.on_ok(move |_, window, cx| {
|
||||||
@@ -388,7 +398,7 @@ impl Render for PresetsDialog {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.text_center()
|
.text_center()
|
||||||
.child("暂无发布预设,点击上方「新建」创建"),
|
.child(i18n::t("暂无发布预设,点击上方「新建」创建")),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -450,10 +460,10 @@ impl Render for PresetsDialog {
|
|||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w(px(0.))
|
.min_w(px(0.))
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(field("预设名称", Input::new(&self.name)))
|
.child(field(i18n::t("预设名称"), Input::new(&self.name)))
|
||||||
.child(field("发布主题", Input::new(&self.topic)))
|
.child(field(i18n::t("发布主题"), Input::new(&self.topic)))
|
||||||
.child(field(
|
.child(field(
|
||||||
"Payload(原文保存,发送时渲染变量)",
|
i18n::t("Payload(原文保存,发送时渲染变量)"),
|
||||||
Textarea::new(&self.payload).h(px(96.)),
|
Textarea::new(&self.payload).h(px(96.)),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
@@ -484,13 +494,13 @@ impl Render for PresetsDialog {
|
|||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w(px(0.))
|
.min_w(px(0.))
|
||||||
.child(field(
|
.child(field(
|
||||||
"Payload 格式",
|
i18n::t("Payload 格式"),
|
||||||
Select::new(&self.payload_format).small(),
|
Select::new(&self.payload_format).small(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().h(px(1.)).bg(cx.theme().border).w_full())
|
.child(div().h(px(1.)).bg(cx.theme().border).w_full())
|
||||||
.child(div().text_xs().font_semibold().child("MQTT 5 属性"))
|
.child(div().text_xs().font_semibold().child(i18n::t("MQTT 5 属性")))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -502,7 +512,7 @@ impl Render for PresetsDialog {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().w(px(120.)).child(field(
|
div().w(px(120.)).child(field(
|
||||||
"消息过期(秒)",
|
i18n::t("消息过期(秒)"),
|
||||||
Input::new(&self.msg_expiry).small(),
|
Input::new(&self.msg_expiry).small(),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
@@ -523,7 +533,7 @@ impl Render for PresetsDialog {
|
|||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(field("用户属性", self.user_props.clone()));
|
.child(field(i18n::t("用户属性"), self.user_props.clone()));
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
@@ -538,11 +548,11 @@ impl Render for PresetsDialog {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(div().text_sm().font_semibold().child("发布预设"))
|
.child(div().text_sm().font_semibold().child(i18n::t("发布预设")))
|
||||||
.child(
|
.child(
|
||||||
Button::new("preset-new")
|
Button::new("preset-new")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.label("新建")
|
.label(i18n::t("新建"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -574,7 +584,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
let has_selection = dialog_view.read(cx).selected.is_some();
|
let has_selection = dialog_view.read(cx).selected.is_some();
|
||||||
dialog
|
dialog
|
||||||
.w(px(760.))
|
.w(px(760.))
|
||||||
.title("管理发布预设")
|
.title(i18n::t("管理发布预设"))
|
||||||
.child(body)
|
.child(body)
|
||||||
.footer(
|
.footer(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -583,7 +593,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("preset-delete")
|
Button::new("preset-delete")
|
||||||
.icon(IconName::Trash)
|
.icon(IconName::Trash)
|
||||||
.label("删除")
|
.label(i18n::t("删除"))
|
||||||
.danger()
|
.danger()
|
||||||
.outline()
|
.outline()
|
||||||
.disabled(!has_selection)
|
.disabled(!has_selection)
|
||||||
@@ -596,13 +606,13 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
Button::new("preset-close")
|
Button::new("preset-close")
|
||||||
.label("关闭")
|
.label(i18n::t("关闭"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("preset-save")
|
Button::new("preset-save")
|
||||||
.label("保存")
|
.label(i18n::t("保存"))
|
||||||
.primary()
|
.primary()
|
||||||
.disabled(!has_selection)
|
.disabled(!has_selection)
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use gpui_kit::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::model::MAX_CONNECTION_MESSAGE_BYTES;
|
use crate::model::MAX_CONNECTION_MESSAGE_BYTES;
|
||||||
|
use crate::ui::i18n;
|
||||||
use crate::sysmon::{Sample, Sampler};
|
use crate::sysmon::{Sample, Sampler};
|
||||||
use crate::ui::app::MqttXApp;
|
use crate::ui::app::MqttXApp;
|
||||||
use crate::ui::IconName;
|
use crate::ui::IconName;
|
||||||
@@ -89,7 +90,7 @@ impl ResourceMonitorDialog {
|
|||||||
.get(&c.id)
|
.get(&c.id)
|
||||||
.copied()
|
.copied()
|
||||||
.map(|s| s.label())
|
.map(|s| s.label())
|
||||||
.unwrap_or("未连接"),
|
.unwrap_or(i18n::t("未连接")),
|
||||||
connected: app.engine.is_connected(&c.id),
|
connected: app.engine.is_connected(&c.id),
|
||||||
messages,
|
messages,
|
||||||
bytes,
|
bytes,
|
||||||
@@ -246,7 +247,7 @@ impl ResourceMonitorDialog {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child(format!("{} 条消息", c.messages)),
|
.child(i18n::tf("{n} 条消息", &[("n", &c.messages.to_string())])),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -309,7 +310,7 @@ impl Render for ResourceMonitorDialog {
|
|||||||
cx.theme().primary,
|
cx.theme().primary,
|
||||||
cx.theme().primary_foreground,
|
cx.theme().primary_foreground,
|
||||||
))
|
))
|
||||||
.child(div().text_base().font_semibold().child("资源监控"))
|
.child(div().text_base().font_semibold().child(i18n::t("资源监控")))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -318,7 +319,7 @@ impl Render for ResourceMonitorDialog {
|
|||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(cx.theme().muted)
|
.bg(cx.theme().muted)
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child(format!("运行 {uptime_text}")),
|
.child(i18n::tf("运行 {t}", &[("t", &uptime_text)])),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -333,7 +334,7 @@ impl Render for ResourceMonitorDialog {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child("自动刷新"),
|
.child(i18n::t("自动刷新")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Switch::new("resmon-auto")
|
Switch::new("resmon-auto")
|
||||||
@@ -347,7 +348,7 @@ impl Render for ResourceMonitorDialog {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("resmon-refresh")
|
Button::new("resmon-refresh")
|
||||||
.icon(IconName::RefreshCw)
|
.icon(IconName::RefreshCw)
|
||||||
.label("立即刷新")
|
.label(i18n::t("立即刷新"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, _, cx| {
|
.on_click(cx.listener(|this, _, _, cx| {
|
||||||
@@ -366,36 +367,39 @@ impl Render for ResourceMonitorDialog {
|
|||||||
.child(self.tile(
|
.child(self.tile(
|
||||||
"tile-cpu",
|
"tile-cpu",
|
||||||
IconName::Cpu,
|
IconName::Cpu,
|
||||||
"CPU 占用",
|
i18n::t("CPU 占用"),
|
||||||
format!("{cpu:.1}%"),
|
format!("{cpu:.1}%"),
|
||||||
Some("按可用核数归一".into()),
|
Some(i18n::t("按可用核数归一").into()),
|
||||||
Some(cpu),
|
Some(cpu),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(self.tile(
|
.child(self.tile(
|
||||||
"tile-ws",
|
"tile-ws",
|
||||||
IconName::MemoryStick,
|
IconName::MemoryStick,
|
||||||
"工作集内存",
|
i18n::t("工作集内存"),
|
||||||
Self::fmt_bytes(ws),
|
Self::fmt_bytes(ws),
|
||||||
Some(format!("峰值 {}", Self::fmt_bytes(s.peak_working_set))),
|
Some(i18n::tf(
|
||||||
|
"峰值 {v}",
|
||||||
|
&[("v", &Self::fmt_bytes(s.peak_working_set))],
|
||||||
|
)),
|
||||||
Some(ws as f32 / peak as f32 * 100.),
|
Some(ws as f32 / peak as f32 * 100.),
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(self.tile(
|
.child(self.tile(
|
||||||
"tile-private",
|
"tile-private",
|
||||||
IconName::HardDrive,
|
IconName::HardDrive,
|
||||||
"提交内存",
|
i18n::t("提交内存"),
|
||||||
Self::fmt_bytes(s.private_bytes),
|
Self::fmt_bytes(s.private_bytes),
|
||||||
Some("进程独占的物理页承诺".into()),
|
Some(i18n::t("进程独占的物理页承诺").into()),
|
||||||
None,
|
None,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.child(self.tile(
|
.child(self.tile(
|
||||||
"tile-threads",
|
"tile-threads",
|
||||||
IconName::Layers,
|
IconName::Layers,
|
||||||
"线程 · 句柄",
|
i18n::t("线程 · 句柄"),
|
||||||
format!("{} / {}", s.threads, s.handles),
|
format!("{} / {}", s.threads, s.handles),
|
||||||
Some("tokio 引擎 + GPUI 渲染".into()),
|
Some(i18n::t("tokio 引擎 + GPUI 渲染").into()),
|
||||||
None,
|
None,
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
@@ -412,7 +416,7 @@ impl Render for ResourceMonitorDialog {
|
|||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().border)
|
.border_color(cx.theme().border)
|
||||||
.child(Icon::new(IconName::Info).size_4().text_color(muted))
|
.child(Icon::new(IconName::Info).size_4().text_color(muted))
|
||||||
.child(div().text_sm().text_color(muted).child("暂无连接")),
|
.child(div().text_sm().text_color(muted).child(i18n::t("暂无连接"))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (i, c) in self.conns.iter().enumerate() {
|
for (i, c) in self.conns.iter().enumerate() {
|
||||||
@@ -427,7 +431,7 @@ impl Render for ResourceMonitorDialog {
|
|||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(self.section_header(IconName::Database, "消息与连接", cx))
|
.child(self.section_header(IconName::Database, i18n::t("消息与连接"), cx))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -502,7 +506,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
window.open_dialog(cx, move |dialog, _, _| {
|
window.open_dialog(cx, move |dialog, _, _| {
|
||||||
dialog
|
dialog
|
||||||
.w(px(640.))
|
.w(px(640.))
|
||||||
.title("资源监控")
|
.title(i18n::t("资源监控"))
|
||||||
.child(dialog_view.clone())
|
.child(dialog_view.clone())
|
||||||
.footer(
|
.footer(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -510,7 +514,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("res-close")
|
Button::new("res-close")
|
||||||
.label("关闭")
|
.label(i18n::t("关闭"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,14 +17,20 @@ use gpui_kit::{
|
|||||||
Styled as _, Window,
|
Styled as _, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::model::{AppSettings, ThemeModePref, MAX_CONNECTION_MESSAGE_BYTES};
|
use crate::model::{AppSettings, LanguagePref, ThemeModePref, MAX_CONNECTION_MESSAGE_BYTES};
|
||||||
use crate::mqtt::MqttEngine;
|
use crate::mqtt::MqttEngine;
|
||||||
use crate::ui::app::MqttXApp;
|
use crate::ui::app::MqttXApp;
|
||||||
use crate::ui::widgets::{field, make_select, OptionDelegate};
|
use crate::ui::widgets::{field, make_select, OptionDelegate};
|
||||||
use crate::ui::IconName;
|
use crate::ui::IconName;
|
||||||
|
use crate::ui::i18n;
|
||||||
use crate::update::{self, UpdateInfo};
|
use crate::update::{self, UpdateInfo};
|
||||||
|
|
||||||
const THEMES: [&str; 3] = ["跟随系统", "浅色", "深色"];
|
fn theme_labels() -> [&'static str; 3] {
|
||||||
|
[i18n::t("跟随系统"), i18n::t("浅色"), i18n::t("深色")]
|
||||||
|
}
|
||||||
|
fn language_labels() -> [&'static str; 3] {
|
||||||
|
[i18n::t("跟随系统 / System"), "中文", "English"]
|
||||||
|
}
|
||||||
|
|
||||||
/// 消息缓存条数的合法范围:太少不够回看,太多占内存
|
/// 消息缓存条数的合法范围:太少不够回看,太多占内存
|
||||||
const MAX_MESSAGES_MIN: usize = 100;
|
const MAX_MESSAGES_MIN: usize = 100;
|
||||||
@@ -49,6 +55,7 @@ enum UpdateUi {
|
|||||||
struct SettingsDialog {
|
struct SettingsDialog {
|
||||||
engine: Arc<MqttEngine>,
|
engine: Arc<MqttEngine>,
|
||||||
theme: Entity<SelectState<OptionDelegate>>,
|
theme: Entity<SelectState<OptionDelegate>>,
|
||||||
|
language: Entity<SelectState<OptionDelegate>>,
|
||||||
max_messages: Entity<InputState>,
|
max_messages: Entity<InputState>,
|
||||||
show_millis: bool,
|
show_millis: bool,
|
||||||
auto_check_update: bool,
|
auto_check_update: bool,
|
||||||
@@ -71,9 +78,15 @@ impl SettingsDialog {
|
|||||||
ThemeModePref::Light => 1,
|
ThemeModePref::Light => 1,
|
||||||
ThemeModePref::Dark => 2,
|
ThemeModePref::Dark => 2,
|
||||||
};
|
};
|
||||||
|
let lang_idx = match initial.language {
|
||||||
|
LanguagePref::System => 0,
|
||||||
|
LanguagePref::Zh => 1,
|
||||||
|
LanguagePref::En => 2,
|
||||||
|
};
|
||||||
Self {
|
Self {
|
||||||
engine,
|
engine,
|
||||||
theme: make_select(&THEMES, theme_idx, window, cx),
|
theme: make_select(&theme_labels(), theme_idx, window, cx),
|
||||||
|
language: make_select(&language_labels(), lang_idx, window, cx),
|
||||||
max_messages: cx.new(|cx| {
|
max_messages: cx.new(|cx| {
|
||||||
InputState::new(window, cx).default_value(initial.max_messages.to_string())
|
InputState::new(window, cx).default_value(initial.max_messages.to_string())
|
||||||
}),
|
}),
|
||||||
@@ -166,32 +179,32 @@ impl SettingsDialog {
|
|||||||
match &self.update {
|
match &self.update {
|
||||||
UpdateUi::Idle => row(
|
UpdateUi::Idle => row(
|
||||||
Button::new("check-update")
|
Button::new("check-update")
|
||||||
.label("检查更新")
|
.label(i18n::t("检查更新"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
"检查 GitHub Releases 上的新版本".into_any_element(),
|
i18n::t("检查 GitHub Releases 上的新版本").into_any_element(),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
UpdateUi::Checking => row(
|
UpdateUi::Checking => row(
|
||||||
Button::new("check-update")
|
Button::new("check-update")
|
||||||
.label("检查中…")
|
.label(i18n::t("检查中…"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.loading(true)
|
.loading(true)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
"正在检查更新…".into_any_element(),
|
i18n::t("正在检查更新…").into_any_element(),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
UpdateUi::UpToDate => row(
|
UpdateUi::UpToDate => row(
|
||||||
Button::new("check-update")
|
Button::new("check-update")
|
||||||
.label("重新检查")
|
.label(i18n::t("重新检查"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
"已是最新版本".into_any_element(),
|
i18n::t("已是最新版本").into_any_element(),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
UpdateUi::Available(info) => {
|
UpdateUi::Available(info) => {
|
||||||
@@ -199,14 +212,21 @@ impl SettingsDialog {
|
|||||||
let info_for_click = info.clone();
|
let info_for_click = info.clone();
|
||||||
row(
|
row(
|
||||||
Button::new("apply-update")
|
Button::new("apply-update")
|
||||||
.label(format!("立即更新到 v{}", ver))
|
.label(i18n::tf(
|
||||||
|
"立即更新到 v{v}",
|
||||||
|
&[("v", &ver)],
|
||||||
|
))
|
||||||
.primary()
|
.primary()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.run_update(&info_for_click, window, cx)
|
this.run_update(&info_for_click, window, cx)
|
||||||
}))
|
}))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
format!("发现新版本 v{},下载后自动替换重启", ver).into_any_element(),
|
i18n::tf(
|
||||||
|
"发现新版本 v{v},下载后自动替换重启",
|
||||||
|
&[("v", &ver)],
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -219,9 +239,9 @@ impl SettingsDialog {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().accent)
|
.text_color(cx.theme().accent)
|
||||||
.child(format!(
|
.child(i18n::tf(
|
||||||
"新版本 v{} 已下载并通过校验,等待安装",
|
"新版本 v{v} 已下载并通过校验,等待安装",
|
||||||
ver
|
&[("v", &ver)],
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -229,7 +249,7 @@ impl SettingsDialog {
|
|||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.child(
|
.child(
|
||||||
Button::new("upd-now")
|
Button::new("upd-now")
|
||||||
.label("立即安装")
|
.label(i18n::t("立即安装"))
|
||||||
.primary()
|
.primary()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
@@ -247,7 +267,7 @@ impl SettingsDialog {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("upd-later")
|
Button::new("upd-later")
|
||||||
.label("下次启动安装")
|
.label(i18n::t("下次启动安装"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -258,9 +278,9 @@ impl SettingsDialog {
|
|||||||
}
|
}
|
||||||
this.update = UpdateUi::Idle;
|
this.update = UpdateUi::Idle;
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::info(
|
Notification::info(i18n::t(
|
||||||
"已暂存,下次启动时将自动安装",
|
"已暂存,下次启动时将自动安装",
|
||||||
),
|
)),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -268,7 +288,7 @@ impl SettingsDialog {
|
|||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("upd-discard")
|
Button::new("upd-discard")
|
||||||
.label("放弃此次更新")
|
.label(i18n::t("放弃此次更新"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
@@ -277,7 +297,7 @@ impl SettingsDialog {
|
|||||||
}
|
}
|
||||||
this.update = UpdateUi::Idle;
|
this.update = UpdateUi::Idle;
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::info("已删除暂存的更新包"),
|
Notification::info(i18n::t("已删除暂存的更新包")),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -292,14 +312,19 @@ impl SettingsDialog {
|
|||||||
0.
|
0.
|
||||||
};
|
};
|
||||||
let status = if *total > 0 {
|
let status = if *total > 0 {
|
||||||
format!(
|
i18n::tf(
|
||||||
"下载中 {:.0}%({} / {})",
|
"下载中 {pct}%({done} / {total})",
|
||||||
pct * 100.,
|
&[
|
||||||
Self::fmt_mb(*downloaded),
|
("pct", &format!("{:.0}", pct * 100.)),
|
||||||
Self::fmt_mb(*total)
|
("done", &Self::fmt_mb(*downloaded)),
|
||||||
|
("total", &Self::fmt_mb(*total)),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!("下载中 {}", Self::fmt_mb(*downloaded))
|
i18n::tf(
|
||||||
|
"下载中 {done}",
|
||||||
|
&[("done", &Self::fmt_mb(*downloaded))],
|
||||||
|
)
|
||||||
};
|
};
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -316,12 +341,12 @@ impl SettingsDialog {
|
|||||||
}
|
}
|
||||||
UpdateUi::Failed(e) => row(
|
UpdateUi::Failed(e) => row(
|
||||||
Button::new("check-update")
|
Button::new("check-update")
|
||||||
.label("重试")
|
.label(i18n::t("重试"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
.on_click(cx.listener(|this, _, window, cx| this.run_check(window, cx)))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
format!("更新失败:{e}").into_any_element(),
|
i18n::tf("更新失败:{e}", &[("e", &e)]).into_any_element(),
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -334,6 +359,11 @@ impl SettingsDialog {
|
|||||||
Some(2) => ThemeModePref::Dark,
|
Some(2) => ThemeModePref::Dark,
|
||||||
_ => ThemeModePref::System,
|
_ => ThemeModePref::System,
|
||||||
};
|
};
|
||||||
|
let language = match self.language.read(cx).selected_value() {
|
||||||
|
Some(1) => LanguagePref::Zh,
|
||||||
|
Some(2) => LanguagePref::En,
|
||||||
|
_ => LanguagePref::System,
|
||||||
|
};
|
||||||
// 显式校验范围而非静默钳制:越界直接报错,不落盘
|
// 显式校验范围而非静默钳制:越界直接报错,不落盘
|
||||||
let max_messages = self
|
let max_messages = self
|
||||||
.max_messages
|
.max_messages
|
||||||
@@ -352,6 +382,7 @@ impl SettingsDialog {
|
|||||||
// 四个字段全部显式写出,避免 ..Default::default() 把开关项重置为默认值
|
// 四个字段全部显式写出,避免 ..Default::default() 把开关项重置为默认值
|
||||||
Ok(AppSettings {
|
Ok(AppSettings {
|
||||||
theme,
|
theme,
|
||||||
|
language,
|
||||||
max_messages,
|
max_messages,
|
||||||
show_millis: self.show_millis,
|
show_millis: self.show_millis,
|
||||||
auto_check_update: self.auto_check_update,
|
auto_check_update: self.auto_check_update,
|
||||||
@@ -370,7 +401,7 @@ fn open_in_file_manager(path: &Path, window: &mut Window, cx: &mut App) {
|
|||||||
|
|
||||||
cmd.arg(path);
|
cmd.arg(path);
|
||||||
if let Err(e) = cmd.spawn() {
|
if let Err(e) = cmd.spawn() {
|
||||||
window.push_notification(Notification::error(format!("打开目录失败: {e}")), cx);
|
window.push_notification(Notification::error(i18n::tf("打开目录失败: {e}", &[("e", &e.to_string())])), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,24 +415,27 @@ impl Render for SettingsDialog {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
.w(px(500.))
|
.w(px(500.))
|
||||||
.child(field("主题", Select::new(&self.theme)))
|
.child(field(i18n::t("界面主题"), Select::new(&self.theme)))
|
||||||
|
.child(field(i18n::t("界面语言"), Select::new(&self.language)))
|
||||||
.child(field(
|
.child(field(
|
||||||
"每条连接内存中保留的消息条数(100~100000)",
|
i18n::t("每条连接内存中保留的消息条数(100~100000)"),
|
||||||
Input::new(&self.max_messages),
|
Input::new(&self.max_messages),
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(format!(
|
.child(i18n::tf(
|
||||||
"另有每连接 {}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐",
|
"另有每连接 {n}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐",
|
||||||
MAX_CONNECTION_MESSAGE_BYTES / (1024 * 1024)
|
&[
|
||||||
|
("n", &format!("{}", MAX_CONNECTION_MESSAGE_BYTES / (1024 * 1024))),
|
||||||
|
],
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(div().text_sm().child("时间戳显示毫秒"))
|
.child(div().text_sm().child(i18n::t("时间戳显示毫秒")))
|
||||||
.child(
|
.child(
|
||||||
Switch::new("show-millis")
|
Switch::new("show-millis")
|
||||||
.checked(self.show_millis)
|
.checked(self.show_millis)
|
||||||
@@ -414,7 +448,7 @@ impl Render for SettingsDialog {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.child(div().text_sm().child("自动检查更新(启动时)"))
|
.child(div().text_sm().child(i18n::t("自动检查更新(启动时)")))
|
||||||
.child(
|
.child(
|
||||||
Switch::new("auto-check-update")
|
Switch::new("auto-check-update")
|
||||||
.checked(self.auto_check_update)
|
.checked(self.auto_check_update)
|
||||||
@@ -426,7 +460,7 @@ impl Render for SettingsDialog {
|
|||||||
)
|
)
|
||||||
.child(div().h(px(1.)).bg(cx.theme().border).w_full())
|
.child(div().h(px(1.)).bg(cx.theme().border).w_full())
|
||||||
.child(field(
|
.child(field(
|
||||||
"数据目录",
|
i18n::t("数据目录"),
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -445,7 +479,7 @@ impl Render for SettingsDialog {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("open-data-dir")
|
Button::new("open-data-dir")
|
||||||
.icon(IconName::FolderOpen)
|
.icon(IconName::FolderOpen)
|
||||||
.label("打开目录")
|
.label(i18n::t("打开目录"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
@@ -454,7 +488,7 @@ impl Render for SettingsDialog {
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
.child(field(
|
.child(field(
|
||||||
"日志目录",
|
i18n::t("日志目录"),
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -472,14 +506,14 @@ impl Render for SettingsDialog {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("open-log-dir")
|
Button::new("open-log-dir")
|
||||||
.icon(IconName::FolderOpen)
|
.icon(IconName::FolderOpen)
|
||||||
.label("打开日志目录")
|
.label(i18n::t("打开日志目录"))
|
||||||
.outline()
|
.outline()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
// 日志目录可能尚未创建(从未产生过日志)
|
// 日志目录可能尚未创建(从未产生过日志)
|
||||||
if let Err(e) = std::fs::create_dir_all(&log_dir_for_open) {
|
if let Err(e) = std::fs::create_dir_all(&log_dir_for_open) {
|
||||||
window.push_notification(
|
window.push_notification(
|
||||||
Notification::error(format!("创建日志目录失败: {e}")),
|
Notification::error(i18n::tf("创建日志目录失败: {e}", &[("e", &e.to_string())])),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -493,25 +527,27 @@ impl Render for SettingsDialog {
|
|||||||
v_flex().gap_1().child(
|
v_flex().gap_1().child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(div().text_sm().font_semibold().child("关于"))
|
.child(div().text_sm().font_semibold().child(i18n::t("关于")))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(format!(
|
.child(i18n::tf(
|
||||||
"版本 {}(更新源 {})",
|
"版本 {v}(更新源 {src})",
|
||||||
update::current_version(),
|
&[
|
||||||
update::UPDATE_SOURCE
|
("v", &update::current_version().to_string()),
|
||||||
|
("src", update::UPDATE_SOURCE),
|
||||||
|
],
|
||||||
))
|
))
|
||||||
.child("Apache-2.0 许可证"),
|
.child(i18n::t("Apache-2.0 许可证")),
|
||||||
)
|
)
|
||||||
.child(self.render_update_row(window, cx))
|
.child(self.render_update_row(window, cx))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child("对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端"),
|
.child(i18n::t("对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex().gap_1().child(
|
h_flex().gap_1().child(
|
||||||
@@ -569,13 +605,13 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("settings-cancel")
|
Button::new("settings-cancel")
|
||||||
.label("取消")
|
.label(i18n::t("取消"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("settings-ok")
|
Button::new("settings-ok")
|
||||||
.label("保存")
|
.label(i18n::t("保存"))
|
||||||
.primary()
|
.primary()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let result = for_collect.read(cx).collect(cx);
|
let result = for_collect.read(cx).collect(cx);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use gpui_kit::{
|
|||||||
|
|
||||||
use crate::model::GlobalVariable;
|
use crate::model::GlobalVariable;
|
||||||
use crate::ui::app::MqttXApp;
|
use crate::ui::app::MqttXApp;
|
||||||
|
use crate::ui::i18n;
|
||||||
use crate::ui::widgets::KvEditor;
|
use crate::ui::widgets::KvEditor;
|
||||||
|
|
||||||
pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
||||||
@@ -32,7 +33,7 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
let app_ok = app.clone();
|
let app_ok = app.clone();
|
||||||
dialog
|
dialog
|
||||||
.w(px(560.))
|
.w(px(560.))
|
||||||
.title("全局变量")
|
.title(i18n::t("全局变量"))
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -40,8 +41,8 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child("在发布主题、消息负载中使用 {{变量名}} 引用。")
|
.child(i18n::t("在发布主题、消息负载中使用 {{变量名}} 引用。"))
|
||||||
.child("内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}"),
|
.child(i18n::t("内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}")),
|
||||||
)
|
)
|
||||||
.child(editor_body),
|
.child(editor_body),
|
||||||
)
|
)
|
||||||
@@ -52,13 +53,13 @@ pub fn open(app: Entity<MqttXApp>, window: &mut Window, cx: &mut App) {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
Button::new("vars-cancel")
|
Button::new("vars-cancel")
|
||||||
.label("取消")
|
.label(i18n::t("取消"))
|
||||||
.outline()
|
.outline()
|
||||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("vars-ok")
|
Button::new("vars-ok")
|
||||||
.label("保存")
|
.label(i18n::t("保存"))
|
||||||
.primary()
|
.primary()
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
let vars = for_ok
|
let vars = for_ok
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
//! 极简 i18n:以中文原文为 key 的双语文案表。
|
||||||
|
//!
|
||||||
|
//! 设计取舍:
|
||||||
|
//! - 中文是基准语言,`t("订阅")` 直接返回原文,代码里不引入间接 key,
|
||||||
|
//! 漏翻时回落中文而不是出现裸 key;
|
||||||
|
//! - 英文表集中在本文件一个 `match`,新增文案先写中文、补翻英文即可;
|
||||||
|
//! - 带参数的文案用 [`tf`],占位符 `{name}` 在中英模式中同名替换;
|
||||||
|
//! - 语言是进程级全局(UI 渲染热路径每帧取用,用原子量免锁)。
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Lang {
|
||||||
|
Zh,
|
||||||
|
En,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 0 = 中文,1 = English。默认中文(基准语言)。
|
||||||
|
static LANG: AtomicU8 = AtomicU8::new(0);
|
||||||
|
|
||||||
|
pub fn set_lang(lang: Lang) {
|
||||||
|
LANG.store(lang as u8, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lang() -> Lang {
|
||||||
|
if LANG.load(Ordering::Relaxed) == 1 {
|
||||||
|
Lang::En
|
||||||
|
} else {
|
||||||
|
Lang::Zh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按系统语言解析初始语言(AppSettings::System 时使用)。
|
||||||
|
pub fn from_system() -> Lang {
|
||||||
|
match sys_locale::get_locale().unwrap_or_default().to_lowercase() {
|
||||||
|
l if l.starts_with("zh") => Lang::Zh,
|
||||||
|
_ => Lang::En,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取当前语言的界面文案。中文原文即 key,英文缺失时回退中文。
|
||||||
|
pub fn t(zh: &'static str) -> &'static str {
|
||||||
|
if lang() == Lang::Zh {
|
||||||
|
return zh;
|
||||||
|
}
|
||||||
|
en(zh)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 带参数的界面文案:中英模式均为 `{name}` 占位符,按同名替换。
|
||||||
|
pub fn tf(zh: &'static str, args: &[(&str, &str)]) -> String {
|
||||||
|
let pattern = if lang() == Lang::Zh {
|
||||||
|
zh.to_string()
|
||||||
|
} else {
|
||||||
|
en(zh).to_string()
|
||||||
|
};
|
||||||
|
let mut out = pattern;
|
||||||
|
for (name, value) in args {
|
||||||
|
out = out.replace(&format!("{{{name}}}"), value);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 英文文案表。key = 中文原文(与调用处字面量一一对应)。
|
||||||
|
fn en(zh: &'static str) -> &'static str {
|
||||||
|
match zh {
|
||||||
|
// ── 通用 ──
|
||||||
|
"保存" => "Save",
|
||||||
|
"取消" => "Cancel",
|
||||||
|
"删除" => "Delete",
|
||||||
|
"确定" => "OK",
|
||||||
|
"全部" => "All",
|
||||||
|
"清空" => "Clear",
|
||||||
|
"发送" => "Send",
|
||||||
|
"连接" => "Connect",
|
||||||
|
"断开" => "Disconnect",
|
||||||
|
"订阅" => "Subscriptions",
|
||||||
|
"编辑" => "Edit",
|
||||||
|
"高级" => "Advanced",
|
||||||
|
"错误" => "Error",
|
||||||
|
"未连接" => "Disconnected",
|
||||||
|
"连接中" => "Connecting",
|
||||||
|
"连接中…" => "Connecting…",
|
||||||
|
"已连接" => "Connected",
|
||||||
|
"暂停" => "Pause",
|
||||||
|
"恢复" => "Resume",
|
||||||
|
"恢复实时滚动" => "Resume live scrolling",
|
||||||
|
"暂停消息流滚动(消息仍在后台接收)" => {
|
||||||
|
"Pause message stream (messages keep arriving in background)"
|
||||||
|
}
|
||||||
|
"MQTT 调试客户端" => "MQTT debug client",
|
||||||
|
"新建连接" => "New Connection",
|
||||||
|
|
||||||
|
// ── 标题栏 ──
|
||||||
|
"置顶窗口" => "Pin window on top",
|
||||||
|
"取消窗口置顶" => "Unpin window",
|
||||||
|
"置顶操作失败" => "Failed to set always-on-top",
|
||||||
|
"切换为浅色" => "Switch to light theme",
|
||||||
|
"切换为深色" => "Switch to dark theme",
|
||||||
|
"阿里云设备" => "Alibaba Cloud Devices",
|
||||||
|
"全局变量" => "Global Variables",
|
||||||
|
"资源监控" => "Resource Monitor",
|
||||||
|
"导出连接" => "Export Connections",
|
||||||
|
"导入连接" => "Import Connections",
|
||||||
|
"设置" => "Settings",
|
||||||
|
|
||||||
|
// ── 侧边栏 ──
|
||||||
|
"搜索连接…" => "Search connections…",
|
||||||
|
"还没有连接,点击上方 + 新建" => "No connections yet. Click + above to create one",
|
||||||
|
"没有匹配的连接" => "No matching connections",
|
||||||
|
"收起侧栏(Ctrl+B)" => "Collapse sidebar (Ctrl+B)",
|
||||||
|
"展开连接侧栏(Ctrl+B)" => "Expand sidebar (Ctrl+B)",
|
||||||
|
"复制连接" => "Duplicate",
|
||||||
|
"删除连接" => "Delete Connection",
|
||||||
|
"未分组" => "Ungrouped",
|
||||||
|
|
||||||
|
// ── 主区 / 标签页 ──
|
||||||
|
"开始使用 MQTTX" => "Get started with MQTTX",
|
||||||
|
"从左侧选择一个连接,或新建连接开始调试" => {
|
||||||
|
"Pick a connection on the left, or create a new one to start debugging"
|
||||||
|
}
|
||||||
|
"消息流" => "Messages",
|
||||||
|
"日志" => "Logs",
|
||||||
|
"关闭标签页(Ctrl+W)" => "Close tab (Ctrl+W)",
|
||||||
|
|
||||||
|
// ── 连接工作区 ──
|
||||||
|
"收起侧栏" => "Collapse sidebar",
|
||||||
|
"收起订阅面板" => "Collapse subscriptions panel",
|
||||||
|
"展开订阅面板" => "Expand subscriptions panel",
|
||||||
|
"订阅主题后,消息会显示在右侧" => "Subscribe to a topic and messages will show up here",
|
||||||
|
"订阅颜色" => "Subscription color",
|
||||||
|
"随机颜色" => "Random color",
|
||||||
|
"清除颜色" => "Clear color",
|
||||||
|
"停用订阅" => "Disable subscription",
|
||||||
|
"启用订阅" => "Enable subscription",
|
||||||
|
"编辑订阅" => "Edit subscription",
|
||||||
|
"取消订阅" => "Unsubscribe",
|
||||||
|
"订阅主题,支持 # +,多个用逗号/空格分隔" => {
|
||||||
|
"Topic filter, supports # +; separate multiple with commas/spaces"
|
||||||
|
}
|
||||||
|
"别名(可选)" => "Alias (optional)",
|
||||||
|
"订阅标识符" => "Subscription Identifier",
|
||||||
|
"No Local" => "No Local",
|
||||||
|
"Retain As Published" => "Retain As Published",
|
||||||
|
"Retain Handling" => "Retain Handling",
|
||||||
|
"0 每次发送" => "0 Send at every subscribe",
|
||||||
|
"1 仅新订阅" => "1 Send only for new subscription",
|
||||||
|
"2 不发送" => "2 Do not send",
|
||||||
|
"正在编辑:{topic}" => "Editing: {topic}",
|
||||||
|
"订阅主题不能为空" => "Topic filter cannot be empty",
|
||||||
|
"订阅标识符须为正整数" => "Subscription identifier must be a positive integer",
|
||||||
|
"未连接:订阅已保存,连接后自动恢复" => {
|
||||||
|
"Offline: subscription saved, restored on connect"
|
||||||
|
}
|
||||||
|
"未连接:订阅已保存" => "Offline: subscription saved",
|
||||||
|
"QoS 0" => "QoS 0",
|
||||||
|
"QoS 1" => "QoS 1",
|
||||||
|
"QoS 2" => "QoS 2",
|
||||||
|
"更新" => "Update",
|
||||||
|
|
||||||
|
// ── 消息流 ──
|
||||||
|
"过滤主题或内容…" => "Filter by topic or content…",
|
||||||
|
"{n} 条" => "{n} msgs",
|
||||||
|
"仅显示最新 {n} 条" => "Showing latest {n} only",
|
||||||
|
"清空当前连接的消息" => "Clear messages of this connection",
|
||||||
|
"清空消息" => "Clear Messages",
|
||||||
|
"确定清空当前连接的全部消息吗?此操作不可撤销。" => {
|
||||||
|
"Clear all messages of this connection? This cannot be undone."
|
||||||
|
}
|
||||||
|
"接收" => "Received",
|
||||||
|
"发布" => "Published",
|
||||||
|
"没有匹配的消息" => "No matching messages",
|
||||||
|
"暂无消息,订阅主题后消息会显示在这里" => {
|
||||||
|
"No messages yet — they will appear here once you subscribe"
|
||||||
|
}
|
||||||
|
"retain" => "retain",
|
||||||
|
"(非文本,详情可切 Hex)" => " (binary — switch to Hex in details)",
|
||||||
|
"用户属性" => "User Properties",
|
||||||
|
"Content-Type" => "Content-Type",
|
||||||
|
"Response Topic" => "Response Topic",
|
||||||
|
"Correlation Data" => "Correlation Data",
|
||||||
|
"消息过期" => "Message Expiry",
|
||||||
|
"{n} 秒" => "{n}s",
|
||||||
|
"复制主题" => "Copy topic",
|
||||||
|
"复制负载原文" => "Copy raw payload",
|
||||||
|
"复制完整详情(含 v5 属性)" => "Copy full details (incl. MQTT 5 props)",
|
||||||
|
"已复制主题" => "Topic copied",
|
||||||
|
"已复制负载" => "Payload copied",
|
||||||
|
"已复制详情" => "Details copied",
|
||||||
|
"主题" => "Topic",
|
||||||
|
"界面主题" => "Theme",
|
||||||
|
"负载" => "Payload",
|
||||||
|
"详情" => "Details",
|
||||||
|
"自动" => "Auto",
|
||||||
|
"文本" => "Text",
|
||||||
|
"连接日志" => "Connection Logs",
|
||||||
|
"暂无日志" => "No logs yet",
|
||||||
|
|
||||||
|
// ── 发布面板 ──
|
||||||
|
"发布主题" => "Publish topic",
|
||||||
|
"输入消息负载,支持 {{变量名}} 与 {{$ts}} {{$uuid}}" => {
|
||||||
|
"Message payload; supports {{variables}} and {{$ts}} {{$uuid}}"
|
||||||
|
}
|
||||||
|
"消息预设" => "Message Presets",
|
||||||
|
"保存当前为预设…" => "Save current as preset…",
|
||||||
|
"管理预设…" => "Manage presets…",
|
||||||
|
"删除预设" => "Delete Preset",
|
||||||
|
"确定删除预设「{name}」吗?此操作不可撤销。" => {
|
||||||
|
"Delete preset \"{name}\"? This cannot be undone."
|
||||||
|
}
|
||||||
|
"预设已保存" => "Preset saved",
|
||||||
|
"已覆盖预设「{name}」" => "Preset \"{name}\" overwritten",
|
||||||
|
"请填写预设名称" => "Preset name is required",
|
||||||
|
"预设名称,如 温度上报" => "Preset name, e.g. temperature-report",
|
||||||
|
"保存消息预设" => "Save Message Preset",
|
||||||
|
"预设名称" => "Preset name",
|
||||||
|
"变量注入" => "Variable injection",
|
||||||
|
"变量注入({n} 个占位符)" => "Variable injection ({n} placeholders)",
|
||||||
|
"MQTT 5 属性" => "MQTT 5 Properties",
|
||||||
|
"MQTT 5 订阅选项" => "MQTT 5 Subscription Options",
|
||||||
|
"未连接:消息未发送" => "Offline: message not sent",
|
||||||
|
"发布主题不能为空" => "Publish topic cannot be empty",
|
||||||
|
"消息过期须为非负整数(秒)" => "Message expiry must be a non-negative integer (seconds)",
|
||||||
|
"JSON 格式无效: {e}" => "Invalid JSON: {e}",
|
||||||
|
"订阅失败: {e}" => "Subscribe failed: {e}",
|
||||||
|
"Keep Alive" => "Keep Alive",
|
||||||
|
"接收上限" => "Receive Maximum",
|
||||||
|
"主题别名" => "Topic Alias",
|
||||||
|
"需要是 0~65535 的整数" => "must be an integer 0-65535",
|
||||||
|
"必须大于 0(0 是 MQTT 5 协议错误)" => "must be > 0 (0 is an MQTT 5 protocol error)",
|
||||||
|
"需要是非负整数" => "must be a non-negative integer",
|
||||||
|
"已启用" => "Enabled",
|
||||||
|
"添加属性" => "Add property",
|
||||||
|
"删除此属性" => "Remove this property",
|
||||||
|
"收起" => "Collapse",
|
||||||
|
"变量值" => "Value",
|
||||||
|
"主题/负载中没有 {{变量}} 引用" => "No {{variable}} reference in topic/payload",
|
||||||
|
"重新提取占位符" => "Re-extract placeholders",
|
||||||
|
"内置:{{$ts}}(秒) {{$ts_ms}}(毫秒) {{$uuid}} · 发布时注入" => "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}} · injected on publish",
|
||||||
|
"预览:{tp} | {pl}" => "Preview: {tp} | {pl}",
|
||||||
|
"变量({n})" => "Variables ({n})",
|
||||||
|
"清除订阅过滤" => "Clear subscription filter",
|
||||||
|
"取消编辑" => "Cancel editing",
|
||||||
|
"鉴权方式" => "Auth mode",
|
||||||
|
"地域" => "Region",
|
||||||
|
"实例 ID" => "Instance ID",
|
||||||
|
"已保存预设" => "Saved presets",
|
||||||
|
"保存预设" => "Save preset",
|
||||||
|
"生成并连接" => "Generate & connect",
|
||||||
|
"无法生成连接: {e}" => "Failed to build connection: {e}",
|
||||||
|
"分组(可选)" => "Group (optional)",
|
||||||
|
"如 cn-shanghai(可留空)" => "e.g. cn-shanghai (optional)",
|
||||||
|
"设备 ID" => "Device ID",
|
||||||
|
"一机一密:ProductKey" => "Triple: ProductKey",
|
||||||
|
"一机一密:DeviceSecret" => "Triple: DeviceSecret",
|
||||||
|
"阿里云 IoT 设备" => "Alibaba Cloud IoT",
|
||||||
|
"按可用核数归一" => "Normalized by core count",
|
||||||
|
"峰值 {v}" => "Peak {v}",
|
||||||
|
"进程独占的物理页承诺" => "Physical pages committed by the process",
|
||||||
|
"tokio 引擎 + GPUI 渲染" => "tokio engine + GPUI renderer",
|
||||||
|
"暂无连接" => "No connections",
|
||||||
|
"消息与连接" => "Messages & Connections",
|
||||||
|
"{n} 条消息" => "{n} msgs",
|
||||||
|
"运行 {t}" => "Up {t}",
|
||||||
|
"立即刷新" => "Refresh now",
|
||||||
|
"发布主题,支持 {{变量名}}" => "Publish topic; supports {{variables}}",
|
||||||
|
"消息负载,支持 {{变量名}}" => "Message payload; supports {{variables}}",
|
||||||
|
"Payload(原文保存,发送时渲染变量)" => "Payload (stored raw; variables rendered on send)",
|
||||||
|
"Payload 格式" => "Payload format",
|
||||||
|
"暂无发布预设,点击上方「新建」创建" => "No presets yet — click \"New\" above to create one",
|
||||||
|
"请先选择或新建预设" => "Select or create a preset first",
|
||||||
|
"请先选择要删除的预设" => "Select a preset to delete first",
|
||||||
|
"预设「{name}」已保存" => "Preset \"{name}\" saved",
|
||||||
|
"预设名称「{name}」已存在" => "Preset name \"{name}\" already exists",
|
||||||
|
"新预设 {n}" => "New preset {n}",
|
||||||
|
"新预设" => "New preset",
|
||||||
|
"管理发布预设" => "Manage Publish Presets",
|
||||||
|
"在发布主题、消息负载中使用 {{变量名}} 引用。" => "Reference {{variables}} in publish topics and payloads.",
|
||||||
|
"内置变量:{{$ts}}(秒)、{{$ts_ms}}(毫秒)、{{$uuid}}" => "Built-in: {{$ts}} (s), {{$ts_ms}} (ms), {{$uuid}}",
|
||||||
|
"测试任务丢失" => "Test task lost",
|
||||||
|
"Hex 长度必须为偶数(每两个字符一个字节)" => "Hex length must be even (two chars per byte)",
|
||||||
|
"Hex 解码失败: 含非十六进制字符" => "Hex decode failed: non-hex character present",
|
||||||
|
"Hex 解码失败: {e}" => "Hex decode failed: {e}",
|
||||||
|
"Base64 解码失败: {e}" => "Base64 decode failed: {e}",
|
||||||
|
"已发送(被当前过滤条件隐藏,消息流中不显示)" => {
|
||||||
|
"Sent (hidden by current filter — not shown in the stream)"
|
||||||
|
}
|
||||||
|
"收起发布面板" => "Collapse publish panel",
|
||||||
|
"展开发布面板" => "Expand publish panel",
|
||||||
|
"发布面板已收起" => "Publish panel collapsed",
|
||||||
|
"Content-Type(可选)" => "Content-Type (optional)",
|
||||||
|
"秒,如 60" => "seconds, e.g. 60",
|
||||||
|
"Response Topic(可选)" => "Response Topic (optional)",
|
||||||
|
"Correlation Data(可选)" => "Correlation Data (optional)",
|
||||||
|
"消息过期(秒)" => "Expiry (s)",
|
||||||
|
|
||||||
|
// ── 变量面板 ──
|
||||||
|
"检测到模板变量,发布时注入全局变量值" => {
|
||||||
|
"Template variables detected — global values injected on publish"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 连接表单 ──
|
||||||
|
"连接配置" => "Connection Settings",
|
||||||
|
"基本信息" => "Basics",
|
||||||
|
"主机" => "Host",
|
||||||
|
"端口" => "Port",
|
||||||
|
"名称" => "Name",
|
||||||
|
"协议" => "Protocol",
|
||||||
|
"传输" => "Transport",
|
||||||
|
"分组" => "Group",
|
||||||
|
"新建分组" => "New group",
|
||||||
|
"填写后优先于左侧所选分组" => "Overrides the selected group when filled",
|
||||||
|
"Client ID" => "Client ID",
|
||||||
|
"重新生成 Client ID" => "Regenerate Client ID",
|
||||||
|
"WS 路径" => "WS Path",
|
||||||
|
"broker.example.com" => "broker.example.com",
|
||||||
|
"连接名称" => "Connection name",
|
||||||
|
"无分组" => "No group",
|
||||||
|
"认证与心跳" => "Auth & Keep-alive",
|
||||||
|
"用户名" => "Username",
|
||||||
|
"密码" => "Password",
|
||||||
|
"用户名(可选)" => "Username (optional)",
|
||||||
|
"密码(可选)" => "Password (optional)",
|
||||||
|
"Keep Alive (秒)" => "Keep Alive (s)",
|
||||||
|
"Keep Alive (秒) 需要是 0~65535 的整数" => "Keep Alive must be an integer 0-65535",
|
||||||
|
"连接超时 (秒)" => "Connect timeout (s)",
|
||||||
|
"最大重连次数 (0=无限)" => "Max reconnects (0 = unlimited)",
|
||||||
|
"Clean Start / Clean Session" => "Clean Start / Clean Session",
|
||||||
|
"断线自动重连" => "Auto reconnect on disconnect",
|
||||||
|
"连接后自动恢复订阅" => "Auto-resubscribe on connect",
|
||||||
|
"启动时自动连接" => "Connect on app start",
|
||||||
|
"SSL/TLS" => "SSL/TLS",
|
||||||
|
"CA 证书 (PEM)" => "CA certificate (PEM)",
|
||||||
|
"客户端证书 (PEM)" => "Client certificate (PEM)",
|
||||||
|
"客户端密钥 (PEM)" => "Client key (PEM)",
|
||||||
|
"浏览…" => "Browse…",
|
||||||
|
"选择证书 / 密钥文件" => "Select certificate / key file",
|
||||||
|
"忽略 CA 校验(不验证服务器证书)" => "Skip CA verification (trust any server cert)",
|
||||||
|
"已配置 {n} 项" => "{n} configured",
|
||||||
|
"MQTT 5 属性已配置" => "MQTT 5 properties configured",
|
||||||
|
"遗嘱消息 (Last Will)" => "Last Will",
|
||||||
|
"启用遗嘱消息" => "Enable last will",
|
||||||
|
"遗嘱主题" => "Will topic",
|
||||||
|
"遗嘱内容" => "Will payload",
|
||||||
|
"遗嘱主题不能为空" => "Will topic cannot be empty",
|
||||||
|
"遗嘱主题不能包含通配符 + 或 #" => "Will topic cannot contain wildcards + or #",
|
||||||
|
"遗嘱消息已启用" => "Last will enabled",
|
||||||
|
"Retain" => "Retain",
|
||||||
|
"内容类型 (Content Type)" => "Content Type",
|
||||||
|
"响应主题 (Response Topic)" => "Response Topic",
|
||||||
|
"连接名称不能为空" => "Connection name cannot be empty",
|
||||||
|
"主机地址不能为空" => "Host cannot be empty",
|
||||||
|
"Client ID 不能为空" => "Client ID cannot be empty",
|
||||||
|
"会话过期间隔(秒)" => "Session expiry (s)",
|
||||||
|
"接收最大值" => "Receive Maximum",
|
||||||
|
"最大报文长度" => "Maximum Packet Size",
|
||||||
|
"主题别名上限" => "Topic Alias Maximum",
|
||||||
|
"测试连接" => "Test Connection",
|
||||||
|
"测试中…" => "Testing…",
|
||||||
|
"连接测试成功" => "Connection test succeeded",
|
||||||
|
"配置已保存,重连后生效" => "Saved. Takes effect after reconnect",
|
||||||
|
|
||||||
|
// ── 设置 ──
|
||||||
|
"跟随系统" => "Follow system",
|
||||||
|
"浅色" => "Light",
|
||||||
|
"深色" => "Dark",
|
||||||
|
"界面语言" => "Language",
|
||||||
|
"每条连接内存中保留的消息条数(100~100000)" => {
|
||||||
|
"Messages kept in memory per connection (100-100000)"
|
||||||
|
}
|
||||||
|
"另有每连接 {n}MB 的字节预算兜底:大报文流会优先按字节从最旧开始驱逐" => {
|
||||||
|
"Plus a per-connection byte budget of {n} MB: under heavy traffic, oldest messages are evicted by size first"
|
||||||
|
}
|
||||||
|
"时间戳显示毫秒" => "Show milliseconds in timestamps",
|
||||||
|
"自动检查更新(启动时)" => "Check for updates on start",
|
||||||
|
"数据目录" => "Data directory",
|
||||||
|
"日志目录" => "Log directory",
|
||||||
|
"打开目录" => "Open folder",
|
||||||
|
"打开日志目录" => "Open log folder",
|
||||||
|
"打开目录失败: {e}" => "Failed to open folder: {e}",
|
||||||
|
"创建日志目录失败: {e}" => "Failed to create log folder: {e}",
|
||||||
|
"关于" => "About",
|
||||||
|
"版本 {v}(更新源 {src})" => "Version {v} (update source: {src})",
|
||||||
|
"Apache-2.0 许可证" => "Apache-2.0 license",
|
||||||
|
"对标 MQTTX 的 GPUI 跨平台 MQTT 调试客户端" => {
|
||||||
|
"A cross-platform MQTT debug client built with GPUI, aiming at MQTTX"
|
||||||
|
}
|
||||||
|
"检查更新" => "Check for updates",
|
||||||
|
"检查 GitHub Releases 上的新版本" => "Check GitHub Releases for a new version",
|
||||||
|
"检查中…" => "Checking…",
|
||||||
|
"正在检查更新…" => "Checking for updates…",
|
||||||
|
"重新检查" => "Check again",
|
||||||
|
"已是最新版本" => "You are up to date",
|
||||||
|
"立即更新到 v{v}" => "Update to v{v} now",
|
||||||
|
"发现新版本 v{v},下载后自动替换重启" => {
|
||||||
|
"New version v{v} found — downloads, replaces itself and restarts"
|
||||||
|
}
|
||||||
|
"新版本 v{v} 已下载并通过校验,等待安装" => {
|
||||||
|
"Version v{v} downloaded and verified, ready to install"
|
||||||
|
}
|
||||||
|
"立即安装" => "Install now",
|
||||||
|
"下次启动安装" => "Install on next start",
|
||||||
|
"已暂存,下次启动时将自动安装" => "Staged — it will install on next start",
|
||||||
|
"放弃此次更新" => "Discard this update",
|
||||||
|
"已删除暂存的更新包" => "Staged update discarded",
|
||||||
|
"下载中 {pct}%({done} / {total})" => "Downloading {pct}% ({done} / {total})",
|
||||||
|
"下载中 {done}" => "Downloading {done}",
|
||||||
|
"重试" => "Retry",
|
||||||
|
"更新失败:{e}" => "Update failed: {e}",
|
||||||
|
"更新就绪" => "Update Ready",
|
||||||
|
"新版本 v{v} 已下载完成(已通过 sha256 校验)。" => {
|
||||||
|
"Version v{v} has been downloaded (sha256 verified)."
|
||||||
|
}
|
||||||
|
"暂不更新" => "Not now",
|
||||||
|
"安装失败:{e}" => "Install failed: {e}",
|
||||||
|
|
||||||
|
// ── 导入 / 导出 / 通知 ──
|
||||||
|
"导出 {n} 条连接到 {path}" => "Exported {n} connections to {path}",
|
||||||
|
"导入 {n} 条,跳过 {n2} 条" => "Imported {n}, skipped {n2}",
|
||||||
|
"已保存" => "Saved",
|
||||||
|
|
||||||
|
// ── 预设管理 ──
|
||||||
|
"发布预设" => "Publish Presets",
|
||||||
|
"新建预设" => "New Preset",
|
||||||
|
"预设列表" => "Presets",
|
||||||
|
|
||||||
|
// ── 资源监控 / 其他对话框 ──
|
||||||
|
"自身进程资源采样" => "Self-process resource sampling",
|
||||||
|
|
||||||
|
_ => zh,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ pub mod app;
|
|||||||
pub mod connection_form;
|
pub mod connection_form;
|
||||||
pub mod connection_view;
|
pub mod connection_view;
|
||||||
pub mod dialogs;
|
pub mod dialogs;
|
||||||
|
pub mod i18n;
|
||||||
pub mod logo;
|
pub mod logo;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ impl Render for KvEditor {
|
|||||||
.child(
|
.child(
|
||||||
Button::new(SharedString::from(format!("kv-add-{prefix}")))
|
Button::new(SharedString::from(format!("kv-add-{prefix}")))
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.label("添加属性")
|
.label(crate::ui::i18n::t("添加属性"))
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
|||||||
Reference in New Issue
Block a user