fix(audio,protocol,ui): TC-2.3 + TC-10 + TC-13 + channel tree hierarchy
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.
This commit is contained in:
@@ -558,6 +558,48 @@ abstract class AppL10n {
|
||||
/// In en, this message translates to:
|
||||
/// **'off'**
|
||||
String get voiceMicOff;
|
||||
|
||||
/// No description provided for @channelJoinFailedPermission.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Insufficient permission to join this channel.'**
|
||||
String get channelJoinFailedPermission;
|
||||
|
||||
/// No description provided for @channelJoinFailedPassword.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Wrong channel password.'**
|
||||
String get channelJoinFailedPassword;
|
||||
|
||||
/// No description provided for @channelJoinFailedFull.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Channel is full.'**
|
||||
String get channelJoinFailedFull;
|
||||
|
||||
/// No description provided for @channelJoinFailedFamilyFull.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Channel family limit reached.'**
|
||||
String get channelJoinFailedFamilyFull;
|
||||
|
||||
/// No description provided for @channelJoinFailedPrivate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This channel is private.'**
|
||||
String get channelJoinFailedPrivate;
|
||||
|
||||
/// No description provided for @channelJoinFailedTimeout.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not join channel: the server didn\'t respond in time.'**
|
||||
String get channelJoinFailedTimeout;
|
||||
|
||||
/// No description provided for @channelJoinFailedGeneric.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not join channel: {message}'**
|
||||
String channelJoinFailedGeneric(String message);
|
||||
}
|
||||
|
||||
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
|
||||
|
||||
@@ -271,4 +271,29 @@ class AppL10nEn extends AppL10n {
|
||||
|
||||
@override
|
||||
String get voiceMicOff => 'off';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPermission =>
|
||||
'Insufficient permission to join this channel.';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPassword => 'Wrong channel password.';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedFull => 'Channel is full.';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedFamilyFull => 'Channel family limit reached.';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPrivate => 'This channel is private.';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedTimeout =>
|
||||
'Could not join channel: the server didn\'t respond in time.';
|
||||
|
||||
@override
|
||||
String channelJoinFailedGeneric(String message) {
|
||||
return 'Could not join channel: $message';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,4 +265,27 @@ class AppL10nZh extends AppL10n {
|
||||
|
||||
@override
|
||||
String get voiceMicOff => '关闭';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPermission => '权限不足,无法加入此频道。';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPassword => '频道密码错误。';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedFull => '频道已满。';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedFamilyFull => '频道家族人数已达上限。';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPrivate => '此频道为私有频道。';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedTimeout => '无法加入频道:服务器响应超时。';
|
||||
|
||||
@override
|
||||
String channelJoinFailedGeneric(String message) {
|
||||
return '无法加入频道:$message';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user