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.
145 lines
6.4 KiB
Plaintext
145 lines
6.4 KiB
Plaintext
{
|
|
"@@locale": "en",
|
|
"@@x-source-of-truth": "DEC-015 (register v0.9.5). Template ARB for English. Server-provided content is NOT translated (ADR-008 / DEC-015).",
|
|
|
|
"appTitle": "Chanora",
|
|
|
|
"homeNotProductionReadyBanner": "Beta build — voice in/out wired; not production ready.",
|
|
|
|
"fieldServerHost": "Server address",
|
|
"fieldNickname": "Nickname",
|
|
"fieldServerPassword": "Server password",
|
|
"fieldServerPasswordHelp": "Leave blank if the server does not require one.",
|
|
"fieldPassword": "Password",
|
|
"fieldDisplayName": "Display name",
|
|
|
|
"connectAction": "Connect",
|
|
"disconnectAction": "Disconnect",
|
|
"refreshAction": "Refresh",
|
|
"diagnosticsAction": "Diagnostics",
|
|
"aboutAction": "About",
|
|
"aboutVersion": "Version {version}",
|
|
"@aboutVersion": {
|
|
"placeholders": { "version": { "type": "String" } }
|
|
},
|
|
"aboutNonAffiliation": "Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.",
|
|
"aboutLicenseHeading": "License",
|
|
"aboutLicenseBody": "Chanora is dual-licensed under the Apache License, Version 2.0 or the MIT License, at your option. The full license texts ship as LICENSE-APACHE and LICENSE-MIT at the repository root.",
|
|
"aboutThirdPartyHeading": "Third-party software",
|
|
"aboutThirdPartyBody": "Chanora is built on tsclientlib, flutter_rust_bridge, cpal, rusqlite, and the Flutter framework, among others. See the NOTICE file at the repository root for the current attribution list.",
|
|
"copyAction": "Copy",
|
|
"closeAction": "Close",
|
|
"startAudioAction": "Start audio",
|
|
"pttHoldToTalk": "Hold to talk",
|
|
"pttTransmitting": "Transmitting…",
|
|
"pttHoldToTalkSemanticsHint": "Press and hold to transmit voice; release to stop.",
|
|
"pttCapabilityBadge": "PTT: {level} ({backend})",
|
|
"@pttCapabilityBadge": {
|
|
"placeholders": {
|
|
"level": { "type": "String" },
|
|
"backend": { "type": "String" }
|
|
}
|
|
},
|
|
"pttCapabilityBoundKey": "Key: {key}",
|
|
"@pttCapabilityBoundKey": {
|
|
"placeholders": {
|
|
"key": { "type": "String" }
|
|
}
|
|
},
|
|
"pttCapabilityExplainTitle": "Push-to-Talk capability",
|
|
"pttCapabilityExplainFocusedHeading": "Focused PTT",
|
|
"pttCapabilityExplainFocusedBody": "Chanora is currently using Focused Push-to-Talk: the binding only fires while the Chanora window is focused. This is the universal fallback used on every platform when a global capture path is not available.",
|
|
"pttCapabilityExplainGoGlobalWindows": "On Windows, Global PTT is engaged automatically once you bind a key. No additional permission is required.",
|
|
"pttCapabilityExplainGoGlobalMacos": "On macOS, Global PTT requires Input Monitoring permission. Open System Settings → Privacy & Security → Input Monitoring, allow Chanora, then re-bind the key.",
|
|
"pttCapabilityExplainGoGlobalLinux": "On Linux, Global PTT requires a GNOME-Wayland desktop with the GlobalShortcuts portal. Re-bind the key and accept the desktop's shortcut dialog when it appears.",
|
|
"pttCapabilityExplainGoGlobalGeneric": "Global PTT is not available in this environment. Focused PTT will keep working while the Chanora window has focus.",
|
|
"pttConfigureAction": "Configure",
|
|
"pttConfigureTitle": "Configure Push-to-Talk binding",
|
|
"pttConfigurePrompt": "Press the key or mouse side button you want to use for Push-to-Talk.",
|
|
"pttConfigureWaiting": "(waiting for input…)",
|
|
"pttConfigureCaptured": "Captured",
|
|
"pttConfigurePrivacyNote": "Chanora never logs the actual key value. Only the input class (keyboard / mouse-side-button) and a platform-neutral label leave this dialog.",
|
|
"pttConfigureSaveAction": "Save",
|
|
"pttConfigurePortalRedirect": "Your desktop environment will open its own shortcut dialog. Pick the key you want to use for Push-to-Talk.",
|
|
"inputMuteAction": "Mute mic",
|
|
"inputUnmuteAction": "Unmute mic",
|
|
"outputMuteAction": "Mute speaker",
|
|
"outputUnmuteAction": "Unmute speaker",
|
|
"joinChannelAction": "Join channel",
|
|
"channelPasswordTitle": "Channel password",
|
|
"bookmarksHeading": "Bookmarks",
|
|
"bookmarksEmpty": "No bookmarks yet. Enter a server above and tap \"Save bookmark\".",
|
|
"bookmarkAddAction": "Save bookmark",
|
|
"bookmarkAddTitle": "Save bookmark",
|
|
"bookmarkDeleteAction": "Delete bookmark",
|
|
|
|
"statusIdle": "Not connected",
|
|
"statusConnecting": "Connecting…",
|
|
"statusConnected": "Connected to {server}",
|
|
"@statusConnected": {
|
|
"placeholders": { "server": { "type": "String" } }
|
|
},
|
|
"statusError": "Error: {message}",
|
|
"@statusError": {
|
|
"placeholders": { "message": { "type": "String" } }
|
|
},
|
|
|
|
"statusReconnecting": "Reconnecting… attempt {attempt}, in {delay}s",
|
|
"@statusReconnecting": {
|
|
"placeholders": {
|
|
"attempt": { "type": "int" },
|
|
"delay": { "type": "int" }
|
|
}
|
|
},
|
|
"statusConnectionLost": "Connection lost: {reason}",
|
|
"@statusConnectionLost": {
|
|
"placeholders": { "reason": { "type": "String" } }
|
|
},
|
|
|
|
"audioStatsLine": "TX {sent} frames • RX {received} frames • Mic {ptt}",
|
|
"@audioStatsLine": {
|
|
"placeholders": {
|
|
"sent": { "type": "int" },
|
|
"received": { "type": "int" },
|
|
"ptt": { "type": "String" }
|
|
}
|
|
},
|
|
|
|
"channelsHeading": "Channels",
|
|
"clientsHeading": "Online clients",
|
|
"countChannelsAndClients": "{channels} channels • {clients} online",
|
|
"@countChannelsAndClients": {
|
|
"placeholders": {
|
|
"channels": { "type": "int" },
|
|
"clients": { "type": "int" }
|
|
}
|
|
},
|
|
|
|
"voiceModePtt": "PTT",
|
|
"voiceModeContinuous": "Continuous",
|
|
"voiceModeVoiceActivity": "Voice activity",
|
|
"voiceModeComingSoon": "Coming soon",
|
|
"voiceModeLabel": "Transmit mode",
|
|
"voiceReleaseTailLabel": "Release tail",
|
|
"voiceReleaseTailHint": "ms",
|
|
"voiceLeaveAction": "Leave voice",
|
|
"voiceHardMuteLabel": "Mute microphone",
|
|
"voiceOutputMuteLabel": "Mute speakers",
|
|
"voiceSettingsTitle": "Voice settings",
|
|
"voiceBindKeyAction": "Bind PTT key",
|
|
"voiceMicOn": "on",
|
|
"voiceMicOff": "off",
|
|
"channelJoinFailedPermission": "Insufficient permission to join this channel.",
|
|
"channelJoinFailedPassword": "Wrong channel password.",
|
|
"channelJoinFailedFull": "Channel is full.",
|
|
"channelJoinFailedFamilyFull": "Channel family limit reached.",
|
|
"channelJoinFailedPrivate": "This channel is private.",
|
|
"channelJoinFailedTimeout": "Could not join channel: the server didn't respond in time.",
|
|
"channelJoinFailedGeneric": "Could not join channel: {message}",
|
|
"@channelJoinFailedGeneric": {
|
|
"placeholders": {
|
|
"message": { "type": "String" }
|
|
}
|
|
}
|
|
}
|