Commit Graph
7 Commits
Author SHA1 Message Date
EdisonJwa 25b5bb3a6d fix(ui): wide-mode banner placement + channel-pill overflow
Two related VoiceBar / scaffold issues on wide windows:

1. Banner placement
   The 'not production ready' tertiaryContainer banner sat full-width
   above the body Column. In wide layouts (>=840 dp) where the connected
   view splits into Voice Bar (320 dp) + channel tree (Expanded), the
   banner spanned both columns and dwarfed the channel-tree pane.
   Rework: wrap the body in an outer LayoutBuilder so the placement
   decision can read bodyConstraints.maxWidth. When wide AND connected
   AND snapshot != null, render the banner inside the left 320 dp
   SizedBox above the VoiceBar. In every other state (narrow, idle,
   connecting) the banner stays pinned full-width at the top.

2. Channel-name pill overflow
   The Container holding the channel pill had no width constraint and
   Text(channelName) had no overflow handling. Long channel names made
   the pill extend past the column's 320 dp; mute icons slid under the
   adjacent channel tree.
   Rework: pill wrapped in Flexible(flex: 100, fit: FlexFit.loose);
   inner Text gets maxLines: 1, overflow: TextOverflow.ellipsis,
   softWrap: false. Spacer keeps default flex 1; the 100:1 ratio means
   short names hug their intrinsic width and long names take ~99% of
   the remaining space then ellipsize. Mute icons stay pinned right.

flutter analyze: clean (6 pre-existing Radio.groupValue infos only).
2026-05-16 12:15:13 +08:00
EdisonJwa 181b3368d4 fix(audio,voice,log): six P0 issues from Korean Windows test
1. Voice Bar 'Leave voice' button removed entirely. TeamSpeak users
   are always in some channel; Discord/Mumble-style leave is the
   wrong model. To stop being heard / hearing, mute mic / speaker.
   To physically move, tap a different channel. The voiceLeave
   bridge call + _onLeaveVoice stay as dead code for now (marked
   unused) so existing tests/integrations don't break.

2. voice_join now confirms the move actually applied server-side
   by polling the snapshot for up to 1.5 s and matching our own
   client's channel against the requested one. If the server
   rejected the move (no permission, wrong password, channel
   full), voice_join rolls the selector back to in_channel=false
   and returns Err so the UI surfaces the failure instead of
   showing a fake 'joined' state.

3. ServerSnapshot + BridgeSnapshot gain own_client_id so the UI
   can identify our row without name-matching. find_own_in reads
   it directly.

4. set_self_muted now also clamps the TransmitModeSelector's
   hard_mute when input is muted server-side. Without this, the
   Opus encoder kept producing frames after setInputMuted(true),
   tsclientlib refused each one with 'Sending audio while muted',
   and the log grew to 200 MB on the Korean host.

5. tsclientlib WARN spam suppressed via tracing filter
   (tsclientlib=error). Belt-and-braces on top of fix 4.

6. Log file is now rotated at every launch (not just when >4 MiB).
   Two generations kept: chanora.log.1 (previous) and
   chanora.log.2 (the one before). The bug that produced 200 MB
   files was a chatty subsystem flooding a single session; the
   per-launch rotate keeps disk use bounded by what one session
   can produce in its lifetime.

Bonus Windows fix (separate from the six but found in the same
log): the Raw Input + Hook backends now signal readiness BEFORE
blocking on GetMessageW. Previously init_tx.send was called after
the loop returned (i.e. on WM_QUIT, which never happens during
arming), so the main thread's 2 s readiness probe always timed
out and the backend reported L0Focused even when registration
succeeded. Both run_raw_input_loop and run_hook_loop now take an
init_tx parameter and call report!(true) right after a successful
registration, and report!(false) on every early-fail return.

cargo check --workspace: clean.
cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored.
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
2026-05-16 01:52:47 +08:00
EdisonJwa 6d4975bd6e fix(ptt,ui): Continuous mode no longer self-disables after 30 s; rename PTT label to Mic
Issue 1: in Continuous transmit mode the talk indicator turned
gray-out / mic disabled after ~30 s and could only be revived by
toggling mic mute. Root cause: SAD-079 MissedKeyUpWatchdog
subscribed to AudioTransmitGate.transmit_active and force-cleared
it after 30 s of true. In PTT mode this is correct (stuck key =
bug). In Continuous mode transmit_active is *supposed* to stay
true indefinitely; the watchdog assumption doesn't hold.

Fix: the watchdog now subscribes to a new ptt_held watch on the
TransmitModeSelector (the raw key-state input, not the resolved
gate). In Continuous mode ptt_held is never set true, so the
watchdog never fires. In PTT mode it still fires on a stuck
key-down as before. The session owns the watchdog (was on the
engine) so it survives engine restarts; it's spawned lazily on the
first start_audio.

MissedKeyUpWatchdog gains spawn_on_signal(rx, on_timeout, timeout)
alongside the existing spawn(gate, timeout) — old shape preserved
for backwards compat. run_watchdog generalised to take any
watch::Receiver<bool> + Box<dyn Fn() + Send + Sync>.

Two new tests:
  - watchdog_on_signal_does_not_fire_when_ptt_held_stays_false
    (the Continuous-mode regression test)
  - watchdog_on_signal_fires_when_signal_stays_true
    (the stuck-key case still fires)

Issue 2: the Voice Bar stats line said 'PTT on/off' even when the
user was in Continuous mode where no PTT key is involved. Renamed
to 'Mic on/off' (mode-neutral) and l10n-ised the on/off literal:
  - en: 'Mic on' / 'Mic off'
  - zh: '麦克风 开启' / '麦克风 关闭'

cargo test --workspace --lib: 80 passed / 0 failed / 1 ignored
(was 78, +2 watchdog tests).
flutter analyze: clean (6 pre-existing Radio.groupValue infos).
2026-05-16 00:57:10 +08:00
EdisonJwa 82b99ebcff feat(ui,voice): add speaker mute button to VoiceBar
Speaker (output) mute existed in the legacy _AudioControls widget
and the rust.setOutputMuted bridge call but was lost when SDD-097
replaced _AudioControls with VoiceBar. Mic mute carried over;
speaker mute did not.

Wire it back: VoiceBar gains an outputMuted prop + onToggleOutputMute
callback and renders a headset/headset_off icon next to the
existing mic mute. main.dart wires the existing _toggleOutputMute
handler (previously dead-code with // ignore: unused_element). The
bridge call setOutputMuted already does both effects together:
local engine silencer + server-broadcast ClientOutputMuted flag.

l10n: rename voiceHardMuteLabel to 'Mute microphone'/'麦克风静音'
to distinguish from the new voiceOutputMuteLabel 'Mute speakers'/
'扬声器静音'.

flutter analyze: clean (6 pre-existing Radio.groupValue infos).
2026-05-16 00:18:18 +08:00
EdisonJwa 64878e3a7d fix(ui,voice): consolidate to single Voice settings entry point
The capability badge had its own 'Configure' TextButton that opened
the bind-key flow, while the Voice Bar's settings gear also reached
bind-key through the settings dialog. Two paths, same destination —
confusing and pointless duplication that the user flagged.

Resolution: the gear is the only configuration entry point. The
capability badge becomes information-only — it still shows the
detected PTT level + backend and (for L0Focused) the info-icon
explanation sheet, but no Configure button. The badge no longer
takes  or  props. The Voice Bar drops
the  callback added in 6a41a0b.

Also removed the dead  legacy widget class (lines
1001-1181) — it had no callers since the VoiceBar refactor in
ba444d9 but was still cluttering the file and even held a stale
reference to PttCapabilityBadge's old constructor signature.

The bound-key string is no longer duplicated either: the Voice Bar's
PTT-only secondary line ('PTT: Space   ·   Release tail: 200ms')
remains the only place that shows the bound key, since it's also
the only PTT-mode-gated surface.

flutter analyze: clean (6 pre-existing Radio.groupValue infos).
2026-05-15 23:52:23 +08:00
EdisonJwa 6a41a0b4db fix(ui,voice): five Voice Bar / settings UX bugs
1. Hard-mute now informs the server (setInputMuted) in addition to
   clamping the local TransmitGate. Without the server-side flag,
   other clients keep seeing us un-muted; without the local clamp
   a beat of in-flight audio leaks through. Drive both together so
   the mic icon and the actual silence land at the same time.

2. Split the badge's Configure affordance from the Voice Bar's
   'Voice settings' gear. The gear opens the mode + release-tail
   dialog (onConfigure); the badge's configure opens the bind-key
   capture flow directly (new onBindKey). Previously both routed
   to the settings dialog, so 'Voice settings' and the badge's
   'Configure' were the same screen — useless duplication.

3. Bind-key label is now PTT-only. The mode-badge row no longer
   prints 'PTT: Space' when Continuous / Voice Activity is
   selected. A new PTT-only secondary line carries the bound key
   plus the release-tail value together, hidden entirely for
   non-PTT modes.

4. Release-tail row is now PTT-only in BOTH the Voice Bar and the
   Voice settings dialog. The dialog previously kept the slider
   visible across all modes; switching to Continuous left the
   user staring at a control that did nothing.

5. PTT capability badge is now PTT-only. In Continuous and Voice
   Activity modes there is no key binding to surface a capability
   for, so the 'L0Focused (focused)' line + its info sheet and
   the Configure button disappear from the Voice Bar when the
   user isn't in PTT mode.

All five fixes are pure UI; no Rust changes needed. flutter analyze
remains clean (6 pre-existing Radio.groupValue deprecation infos).
2026-05-15 23:34:53 +08:00
EdisonJwa ba444d94bd feat(audio,bridge,flutter): v1 audio + PTT lifecycle implementation (SDD-094..097)
Implement the SDD-094 / SDD-095 / SDD-096 / SDD-097 detailed designs
committed in dfa84ee.

Rust side
- chanora_audio::TransmitMode enum (Ptt/Continuous/VoiceActivity) with
  serde-friendly u8 repr (SDD-095).
- chanora_audio::TransmitModeSelector: lock-free Atomic-backed selector
  that is the sole writer of transmit_active (per SAD-083), applying
  hard_mute as a final clamp. VoiceActivity falls through to Continuous
  for v1 (DEC-030 placeholder).
- chanora_audio::ReleaseTailTimer: tokio-task-owning struct driving the
  selector's ptt_held input; default 200 ms tail, configurable 0–500 ms
  with AtomicU32 hot read; pending JoinHandle held in a std::sync::Mutex
  touched only on PTT edge transitions (SDD-096).
- chanora_storage: AudioMeta persisted as audio_meta.json next to
  identity.dek; get/set_transmit_mode + get/set_release_tail_ms with
  0..=500 clamp on write.
- chanora_core::ChanoraSession: voice_join(channel, password) and
  voice_leave() are the new lifecycle entry points; ensure_audio_running
  and shutdown_audio_if_idle are private helpers around the existing
  Option<AudioEngine> field. SessionEvent::VoiceState carries the
  in_channel / transmit_mode / mute / release_tail_ms tuple. Selector
  state survives reconnect; supervisor rewires it to each fresh engine
  gate.
- chanora_bridge: drop start_audio; add voice_join, voice_leave,
  set/get_transmit_mode, set/get_release_tail_ms, set_hard_mute.
  BridgeEvent::VoiceState mirrors the core event. AudioStarted/Stopped
  kept for backwards compat but Flutter ignores them in the new UI.

Flutter side
- New apps/chanora_flutter/lib/widgets/voice_bar.dart replaces the
  legacy _AudioControls widget. Renders channel pill, mode badge,
  mute toggle, level meter, PttCapabilityBadge, leave button. No
  manual Start affordance anywhere.
- New apps/chanora_flutter/lib/widgets/voice_settings.dart dialog with
  TransmitMode radio group (VoiceActivity disabled with 'Coming soon'
  trailing label per DEC-030), bind-key button, release-tail slider
  0–500 ms step 25.
- main.dart: state fields _inChannel, _transmitMode, _hardMute,
  _releaseTailMs driven by BridgeEvent_VoiceState. Channel-tap now
  calls voiceJoin instead of moveToChannel. Removed _onStartAudio,
  _audioStarted-gated branch, and the FilledButton.
- l10n: 11 new strings in app_en.arb + app_zh.arb.

Verification
- cargo check --workspace: clean.
- cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored
  (chanora_audio: +12 new tests for TransmitMode/Selector/ReleaseTail;
  chanora_storage: +2 new tests for audio_meta round-trip).
- flutter analyze: 0 errors, 0 warnings; 6 infos are the Flutter 3.32
  Radio.groupValue deprecation (pre-existing API usage).
- FRB Dart/Rust bindings regenerated via flutter_rust_bridge_codegen.

Follow-up (intentionally deferred)
- PttController and per-platform PTT backends still drive AudioTransmitGate
  directly via the legacy set_ptt path; routing those key edges through
  ChanoraSession::release_tail_timer().{key_down,key_up} so the tail
  applies to native PTT input is a contained wiring change in a follow-up.
- Real audio-level RMS in BridgeAudioStats (current meter is binary).
- VoiceActivity backend (DEC-030).
2026-05-15 23:05:37 +08:00