Five user-reported defects + one auto-test regression-catcher.
== TC-2.3: capability badge stuck at L0Focused on Korean Win 11 ==
Root cause: in WindowsRawInputBackend::start() (and the parallel
WindowsHookBackend), the worker thread's armed.store(ok, ...) only
ran AFTER GetMessageW returned (i.e. on WM_QUIT). During normal
arming the message pump runs forever, so armed stayed at its
initial false value, and descriptor() reported L0Focused even
though RegisterRawInputDevices had succeeded.
Fix: run_raw_input_loop and run_hook_loop now take armed as a
parameter and flip it to true inside the loop right after the
successful registration, before blocking on GetMessageW. The
outer setter is kept as a belt-and-braces clear-on-failure path.
Also drops the redundant outer 'Raw Input armed' / 'low-level
hook armed' log lines — the in-loop 'Raw Input devices
registered' / 'low-level hooks installed' messages already
convey arming success with full context.
New tests raw_input_backend_start_flips_armed_to_l2 and
hook_backend_start_flips_armed_to_l2 call real start(), sleep
80 ms, assert descriptor().level == L2GlobalHoldToTalk. Replaces
the previous #[ignore]'d real-start smoke test which never
asserted on the descriptor.
== TC-10: no-permission channel rejection invisible ==
Root cause 1: chanora_protocol::adapter::move_self_to used the
fire-and-forget send() on the client_move command. TS3 server
replies with a typed error event the adapter discarded, and
move_to_channel returned Ok regardless.
Root cause 2: even when chanora_core::voice_join detected the
non-confirmation via snapshot polling, the rolled-back error
flowed into the connect-form-area _error string which is hidden
post-connect. The user saw no feedback.
Fix:
* New ProtocolError::ServerRejected { code: u32, message: String }
carries the canonical TS3 error code per the official catalogue
at https://github.com/ReSpeak/tsdeclarations (Errors.csv).
* move_self_to now uses send_with_result, returns a MessageHandle.
The connection-task loop holds a pending_moves HashMap keyed by
MessageHandle, services StreamItem::MessageResult by looking up
and resolving the reply with either Ok or the typed
ServerRejected.
* Pending entries have a 3 s deadline so a server that never
replies doesn't leak the reply channel — expired entries fall
back to Ok and let the snapshot poll handle confirmation.
* voice_join short-circuits on ServerRejected (no need for the
full snapshot poll), still polls for confirmation as a
belt-and-braces fallback for legacy servers; on poll failure
emits ServerRejected with sentinel code 0x0001 (undefined).
* New BridgeError::ServerRejected mirror with the same fields;
CoreError → BridgeError mapping preserves the typed variant.
* Flutter _onJoinChannel shows a floating SnackBar with a
localised message selected by error code (channelJoinFailed*
l10n entries). 6 known codes mapped to specific messages
(insufficient permission, wrong password, channel full,
family limit, private channel, timeout); everything else
falls back to the server-supplied generic message.
== TC-13: mouse side-button capture only works on text field ==
The _PttBindingCaptureDialog wrapped its Column with a Listener
using the default HitTestBehavior.deferToChild. Pointer events
landing on the dialog's empty padding regions weren't claimed by
any child and so were never delivered to the Listener.
Fix: explicit HitTestBehavior.opaque so the entire dialog area
catches PointerDown events regardless of where the cursor sits.
== Channel tree hierarchy ==
Reported issue: tree rendered as flat list, no indication of
parent-child nesting. The bridge already carries the
field; the renderer just ignored it.
Fix in _SnapshotView: walk the (already DFS-sorted) channel list
and compute each row's depth from its parent's depth. Render
left-padding of depth * 18 dp. Cap depth at 6 to keep deep
hierarchies visually bounded; the cap plateaus silently (no
glyph, channel still tappable, data carries the real depth).
== Responsive layout ==
Connected layout is now LayoutBuilder-driven. Below 840 dp wide
(Material's tablet/desktop breakpoint) the original stacked
column layout is used (Voice Bar on top, channel tree below).
At 840 dp and above the layout becomes a side-by-side Row with
the Voice Bar pinned at 320 dp on the left and the channel tree
Expanded on the right.
Verification
- cargo check --workspace: clean.
- cargo test --workspace --lib: 80 / 0 / 1 (unchanged Linux total;
+2 new Windows-only tests not counted here).
- flutter analyze: clean (6 pre-existing Radio.groupValue infos).
- FRB bindings regenerated to expose BridgeError_ServerRejected.
102 lines
5.3 KiB
Plaintext
102 lines
5.3 KiB
Plaintext
{
|
||
"@@locale": "zh",
|
||
"@@x-source-of-truth": "DEC-015 (register v0.9.5). Simplified Chinese for the Beta key set.",
|
||
|
||
"appTitle": "Chanora",
|
||
"homeNotProductionReadyBanner": "Beta 版本——已接通语音收发;尚未达到生产环境质量。",
|
||
|
||
"fieldServerHost": "服务器地址",
|
||
"fieldNickname": "昵称",
|
||
"fieldServerPassword": "服务器密码",
|
||
"fieldServerPasswordHelp": "如果服务器无需密码,留空即可。",
|
||
"fieldPassword": "密码",
|
||
"fieldDisplayName": "显示名称",
|
||
|
||
"connectAction": "连接",
|
||
"disconnectAction": "断开连接",
|
||
"refreshAction": "刷新",
|
||
"diagnosticsAction": "诊断信息",
|
||
"aboutAction": "关于",
|
||
"aboutVersion": "版本 {version}",
|
||
"aboutNonAffiliation": "Chanora 是独立项目,与 TeamSpeak 之间不存在任何附属、认可、赞助或官方关联关系。",
|
||
"aboutLicenseHeading": "许可协议",
|
||
"aboutLicenseBody": "Chanora 采用 Apache License 2.0 或 MIT License 双协议授权,使用者可任选其一。完整协议文本以 LICENSE-APACHE 与 LICENSE-MIT 形式随仓库一同分发。",
|
||
"aboutThirdPartyHeading": "第三方组件",
|
||
"aboutThirdPartyBody": "Chanora 基于 tsclientlib、flutter_rust_bridge、cpal、rusqlite、Flutter 框架等开源组件构建。完整归属信息请参阅仓库根目录的 NOTICE 文件。",
|
||
"copyAction": "复制",
|
||
"closeAction": "关闭",
|
||
"startAudioAction": "启动语音",
|
||
"pttHoldToTalk": "按住说话",
|
||
"pttTransmitting": "正在发送…",
|
||
"pttHoldToTalkSemanticsHint": "按住进行语音发送,松开停止。",
|
||
"pttCapabilityBadge": "对讲能力:{level}({backend})",
|
||
"pttCapabilityBoundKey": "按键:{key}",
|
||
"pttCapabilityExplainTitle": "对讲能力说明",
|
||
"pttCapabilityExplainFocusedHeading": "聚焦对讲",
|
||
"pttCapabilityExplainFocusedBody": "Chanora 当前使用聚焦对讲:按键只在 Chanora 窗口处于聚焦时生效。这是所有平台在无法启用全局采集时的通用回退方案。",
|
||
"pttCapabilityExplainGoGlobalWindows": "在 Windows 上,绑定按键后会自动启用全局对讲,无需额外权限。",
|
||
"pttCapabilityExplainGoGlobalMacos": "在 macOS 上,启用全局对讲需要「输入监视」权限。请打开「系统设置 → 隐私与安全 → 输入监视」,授权 Chanora 后重新绑定按键。",
|
||
"pttCapabilityExplainGoGlobalLinux": "在 Linux 上,启用全局对讲需要带 GlobalShortcuts 门户的 GNOME-Wayland 桌面。请重新绑定按键,并在桌面弹出快捷键对话框时接受。",
|
||
"pttCapabilityExplainGoGlobalGeneric": "当前环境暂不支持全局对讲。聚焦对讲在 Chanora 窗口获得焦点时仍可正常使用。",
|
||
"pttConfigureAction": "配置",
|
||
"pttConfigureTitle": "配置对讲按键",
|
||
"pttConfigurePrompt": "按下您希望用于对讲的按键或鼠标侧键。",
|
||
"pttConfigureWaiting": "(等待输入…)",
|
||
"pttConfigureCaptured": "已捕获",
|
||
"pttConfigurePrivacyNote": "Chanora 不会记录具体的按键值。本对话框只会向应用提交输入类别(键盘 / 鼠标侧键)以及一个跨平台标签。",
|
||
"pttConfigureSaveAction": "保存",
|
||
"pttConfigurePortalRedirect": "您的桌面环境将打开自带的快捷键对话框,请在其中选择用于对讲的按键。",
|
||
"inputMuteAction": "静音麦克风",
|
||
"inputUnmuteAction": "取消麦克风静音",
|
||
"outputMuteAction": "静音扬声器",
|
||
"outputUnmuteAction": "取消扬声器静音",
|
||
"joinChannelAction": "加入频道",
|
||
"channelPasswordTitle": "频道密码",
|
||
"bookmarksHeading": "书签",
|
||
"bookmarksEmpty": "尚无书签。先在上方填写服务器,然后点击“保存书签”。",
|
||
"bookmarkAddAction": "保存书签",
|
||
"bookmarkAddTitle": "保存书签",
|
||
"bookmarkDeleteAction": "删除书签",
|
||
|
||
"statusIdle": "未连接",
|
||
"statusConnecting": "正在连接…",
|
||
"statusConnected": "已连接到 {server}",
|
||
"statusError": "错误:{message}",
|
||
|
||
"statusReconnecting": "正在重新连接……第 {attempt} 次尝试,{delay} 秒后",
|
||
"statusConnectionLost": "连接已断开:{reason}",
|
||
|
||
"audioStatsLine": "发送 {sent} 帧 • 接收 {received} 帧 • 麦克风 {ptt}",
|
||
|
||
"channelsHeading": "频道",
|
||
"clientsHeading": "在线用户",
|
||
"countChannelsAndClients": "{channels} 个频道 • {clients} 在线",
|
||
|
||
"voiceModePtt": "按键说话",
|
||
"voiceModeContinuous": "持续发送",
|
||
"voiceModeVoiceActivity": "语音激活",
|
||
"voiceModeComingSoon": "即将推出",
|
||
"voiceModeLabel": "发送模式",
|
||
"voiceReleaseTailLabel": "释放延迟",
|
||
"voiceReleaseTailHint": "毫秒",
|
||
"voiceLeaveAction": "离开语音",
|
||
"voiceHardMuteLabel": "麦克风静音",
|
||
"voiceOutputMuteLabel": "扬声器静音",
|
||
"voiceSettingsTitle": "语音设置",
|
||
"voiceBindKeyAction": "绑定 PTT 按键",
|
||
"voiceMicOn": "开启",
|
||
"voiceMicOff": "关闭",
|
||
"channelJoinFailedPermission": "权限不足,无法加入此频道。",
|
||
"channelJoinFailedPassword": "频道密码错误。",
|
||
"channelJoinFailedFull": "频道已满。",
|
||
"channelJoinFailedFamilyFull": "频道家族人数已达上限。",
|
||
"channelJoinFailedPrivate": "此频道为私有频道。",
|
||
"channelJoinFailedTimeout": "无法加入频道:服务器响应超时。",
|
||
"channelJoinFailedGeneric": "无法加入频道:{message}",
|
||
"@channelJoinFailedGeneric": {
|
||
"placeholders": {
|
||
"message": { "type": "String" }
|
||
}
|
||
}
|
||
}
|