Files
chanora/apps/chanora_flutter/lib/src/rust/api.dart
T
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

530 lines
17 KiB
Dart

// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `log_file_path`, `log_sink`, `open_log_file`, `runtime`, `session`, `transmit_mode_from_u8`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
/// Return the platform-conventional log-file path as a string, or
/// an empty string if the platform does not have one (mobile).
String logFilePathStr() => RustLib.instance.api.crateApiLogFilePathStr();
/// Connect to a TeamSpeak-compatible server and return the initial
/// state snapshot. Honours the DEC-006 single-connection invariant
/// via [`BridgeError::AlreadyConnected`].
///
/// `password` is optional — pass an empty string for servers that
/// don't require one.
Future<BridgeSnapshot> connect({
required String host,
required String nickname,
required String password,
}) => RustLib.instance.api.crateApiConnect(
host: host,
nickname: nickname,
password: password,
);
/// Re-fetch a fresh snapshot from the active connection.
Future<BridgeSnapshot> snapshot() => RustLib.instance.api.crateApiSnapshot();
/// Disconnect from the server. No-op if not connected.
Future<void> disconnect() => RustLib.instance.api.crateApiDisconnect();
/// True if a connection is currently active.
Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
/// Set the push-to-talk state.
///
/// Superseded in v1 by [`set_transmit_mode`] + the binding capture
/// dialog. Retained so legacy callers and integration tests keep
/// working; the new VoiceBar UI no longer invokes this.
Future<void> setPtt({required bool active}) =>
RustLib.instance.api.crateApiSetPtt(active: active);
/// Join a voice channel (SDD-094). Moves the user to `channel_id`,
/// brings up the audio engine if needed, and emits
/// `BridgeEvent::VoiceState`. `password` may be empty.
Future<void> voiceJoin({required BigInt channelId, required String password}) =>
RustLib.instance.api.crateApiVoiceJoin(
channelId: channelId,
password: password,
);
/// Leave the current voice channel (SDD-094). Tears down the audio
/// engine and emits `BridgeEvent::VoiceState`.
Future<void> voiceLeave() => RustLib.instance.api.crateApiVoiceLeave();
/// Set the active transmit mode (SDD-095).
Future<void> setTransmitMode({required BridgeTransmitMode mode}) =>
RustLib.instance.api.crateApiSetTransmitMode(mode: mode);
/// Read the active transmit mode.
Future<BridgeTransmitMode> getTransmitMode() =>
RustLib.instance.api.crateApiGetTransmitMode();
/// Update the release-tail in milliseconds (SDD-096). Values are
/// clamped to `0..=500` on the Rust side; passing anything larger
/// silently saturates.
Future<void> setReleaseTailMs({required int ms}) =>
RustLib.instance.api.crateApiSetReleaseTailMs(ms: ms);
/// Read the current release-tail in milliseconds.
Future<int> getReleaseTailMs() =>
RustLib.instance.api.crateApiGetReleaseTailMs();
/// Engage or release the hard-mute clamp (SDD-094). When `true`
/// the audio engine transmits nothing regardless of mode.
Future<void> setHardMute({required bool muted}) =>
RustLib.instance.api.crateApiSetHardMute(muted: muted);
/// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The
/// platform_key string is opaque to the bridge — it identifies the
/// bound key inside the platform backend and never appears in any
/// log record or diagnostic export (DEC-027 enforced at the
/// diagnostics-sanitizer layer).
Future<void> setPttBinding({
required BridgePttInputClass inputClass,
required String platformKey,
}) => RustLib.instance.api.crateApiSetPttBinding(
inputClass: inputClass,
platformKey: platformKey,
);
/// Read the current PTT capability descriptor. Returns a
/// `(level, backend_id, bound_input_class)` triple matching the
/// privacy-safe `BridgeEvent::PttCapability` event shape; useful
/// for the initial UI render before the first event arrives.
Future<(String, String, String)> pttDescriptor() =>
RustLib.instance.api.crateApiPttDescriptor();
/// Return the persisted PTT binding as a
/// `(input_class, platform_key)` pair so the UI can hydrate its
/// display state at launch (e.g. show "PTT: Space" next to the
/// badge before the user re-opens the binding dialog). Empty
/// strings mean no binding has been persisted yet.
Future<(String, String)> getPttBinding() =>
RustLib.instance.api.crateApiGetPttBinding();
/// Move our own client to `channel_id`. Optional channel password
/// for password-protected channels — pass an empty string when not
/// required.
Future<void> moveToChannel({
required BigInt channelId,
required String password,
}) => RustLib.instance.api.crateApiMoveToChannel(
channelId: channelId,
password: password,
);
/// Toggle self input-mute (microphone) on the server. Independent
/// of push-to-talk: a muted client never transmits regardless of
/// PTT state.
Future<void> setInputMuted({required bool muted}) =>
RustLib.instance.api.crateApiSetInputMuted(muted: muted);
/// Toggle self output-mute (speaker). Mutes locally *and* informs
/// the server. The server uses this for the channel icon next to
/// the client name; the local mute kicks in immediately even
/// before the server acknowledges.
Future<void> setOutputMuted({required bool muted}) =>
RustLib.instance.api.crateApiSetOutputMuted(muted: muted);
/// Set master output gain. `1.0` is unity, `0.0` is silent. Values
/// above `1.0` amplify and can clip downstream. Errors when audio
/// is not started.
Future<void> setOutputGain({required double gain}) =>
RustLib.instance.api.crateApiSetOutputGain(gain: gain);
/// User-initiated diagnostic export. Returns a multi-line text
/// blob, redacted per the production policy, that the user can
/// share or copy. DEC-016 forbids automatic uploads — this is the
/// only path that surfaces logs.
String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
/// Wire the identity persistence store to a platform-private
/// directory. Should be called once on app start after Flutter has
/// resolved `getApplicationSupportDirectory()` (or equivalent).
///
/// Subsequent [`connect`] calls will reuse the persisted identity,
/// or generate-and-persist a fresh one on first use. This keeps the
/// server-visible UID stable across app restarts.
///
/// Beta caveat: the identity is stored as a plain file (mode 0600
/// on Unix). It is *not* encrypted at rest. RISK-PoC-002 documents
/// this gap; the v0.4 storage rework lands the proper Secret
/// Service + Android Keystore + iOS Keychain backends.
Future<void> initStorage({required String dir}) =>
RustLib.instance.api.crateApiInitStorage(dir: dir);
/// List persisted bookmarks.
Future<List<BridgeBookmark>> listBookmarks() =>
RustLib.instance.api.crateApiListBookmarks();
/// Insert a bookmark and return its assigned id. The `id` field on
/// the input is ignored.
Future<PlatformInt64> addBookmark({required BridgeBookmark b}) =>
RustLib.instance.api.crateApiAddBookmark(b: b);
/// Update an existing bookmark.
Future<void> updateBookmark({required BridgeBookmark b}) =>
RustLib.instance.api.crateApiUpdateBookmark(b: b);
/// Delete a bookmark by id.
Future<void> deleteBookmark({required PlatformInt64 id}) =>
RustLib.instance.api.crateApiDeleteBookmark(id: id);
/// Notify the core of the latest OS-reported connectivity state.
/// Called by the Flutter side from `connectivity_plus` callbacks.
/// The core's supervisor uses this to (a) pre-charge the watchdog
/// on Offline and (b) short-circuit reconnect backoff on Online.
void setNetworkState({required BridgeNetworkState state}) =>
RustLib.instance.api.crateApiSetNetworkState(state: state);
/// Subscribe to lifecycle events. Each call yields a fresh
/// subscription; multiple subscribers are supported. On slow
/// consumers, events are dropped rather than blocking the supervisor
/// (consistent with `tokio::sync::broadcast::Receiver` semantics).
Stream<BridgeEvent> eventsStream() =>
RustLib.instance.api.crateApiEventsStream();
/// Read audio statistics. Errors if no connection or audio not started.
Future<BridgeAudioStats> audioStats() =>
RustLib.instance.api.crateApiAudioStats();
/// Statistics from the audio engine.
class BridgeAudioStats {
/// Number of Opus frames sent since audio started.
final int framesSent;
/// Number of inbound voice packets decoded.
final int framesReceived;
/// Current push-to-talk state.
final bool pttActive;
const BridgeAudioStats({
required this.framesSent,
required this.framesReceived,
required this.pttActive,
});
@override
int get hashCode =>
framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeAudioStats &&
runtimeType == other.runtimeType &&
framesSent == other.framesSent &&
framesReceived == other.framesReceived &&
pttActive == other.pttActive;
}
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
class BridgeBookmark {
/// Row id assigned by SQLite. Use `0` when adding new rows;
/// the returned id is then meaningful.
final PlatformInt64 id;
/// User-facing label.
final String displayName;
/// `hostname[:port]` or TSDNS name.
final String host;
/// Nickname to use for this bookmark.
final String nickname;
/// Optional remembered password. Empty string = none.
final String password;
const BridgeBookmark({
required this.id,
required this.displayName,
required this.host,
required this.nickname,
required this.password,
});
@override
int get hashCode =>
id.hashCode ^
displayName.hashCode ^
host.hashCode ^
nickname.hashCode ^
password.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeBookmark &&
runtimeType == other.runtimeType &&
id == other.id &&
displayName == other.displayName &&
host == other.host &&
nickname == other.nickname &&
password == other.password;
}
/// Channel as seen by Dart. Matches `chanora_protocol::ChannelInfo`
/// but with primitive `u64` ids so the Dart side gets `BigInt`s
/// without any wrapper-type ceremony.
class BridgeChannel {
/// Stable channel id.
final BigInt id;
/// Parent channel id; 0 means top-level.
final BigInt parent;
/// Display name.
final String name;
/// Server-side ordering hint.
final PlatformInt64 order;
const BridgeChannel({
required this.id,
required this.parent,
required this.name,
required this.order,
});
@override
int get hashCode =>
id.hashCode ^ parent.hashCode ^ name.hashCode ^ order.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeChannel &&
runtimeType == other.runtimeType &&
id == other.id &&
parent == other.parent &&
name == other.name &&
order == other.order;
}
/// Client as seen by Dart.
class BridgeClient {
/// Stable client id.
final BigInt id;
/// Channel id the client is currently in.
final BigInt channel;
/// Nickname.
final String name;
const BridgeClient({
required this.id,
required this.channel,
required this.name,
});
@override
int get hashCode => id.hashCode ^ channel.hashCode ^ name.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeClient &&
runtimeType == other.runtimeType &&
id == other.id &&
channel == other.channel &&
name == other.name;
}
@freezed
sealed class BridgeEvent with _$BridgeEvent {
const BridgeEvent._();
/// Initial connect succeeded, or a reconnect attempt succeeded.
const factory BridgeEvent.connected({
/// Server name reported by the server snapshot.
required String serverName,
}) = BridgeEvent_Connected;
/// Connection lost; supervisor will retry.
const factory BridgeEvent.lost({
/// Reason classification from the protocol layer.
required String reason,
}) = BridgeEvent_Lost;
/// Supervisor is sleeping before its next reconnect attempt.
const factory BridgeEvent.reconnecting({
/// 1-based attempt counter for the current outage.
required int attempt,
/// Seconds the supervisor will sleep before this attempt.
required int delaySecs,
}) = BridgeEvent_Reconnecting;
/// Session ended (user-requested disconnect or unrecoverable).
const factory BridgeEvent.disconnected({
/// Reason classification.
required String reason,
}) = BridgeEvent_Disconnected;
/// Audio engine started.
const factory BridgeEvent.audioStarted() = BridgeEvent_AudioStarted;
/// Audio engine stopped.
const factory BridgeEvent.audioStopped() = BridgeEvent_AudioStopped;
/// Snapshot probe observed a change in channel/client counts.
/// UI uses this to drive an auto-refresh without active
/// polling.
const factory BridgeEvent.snapshotChanged({
/// Latest channel count.
required int channels,
/// Latest client count.
required int clients,
}) = BridgeEvent_SnapshotChanged;
/// Detected desktop Push-to-Talk capability (gen2 v0.9.3,
/// DEC-023..028). The fields carry only privacy-safe values per
/// DEC-027: the capability level, a stable backend identifier,
/// and the bound input class. No key codes, scan codes, or
/// virtual-key values cross this boundary.
const factory BridgeEvent.pttCapability({
/// Stable capability identifier (`"L0Focused"`,
/// `"L1GlobalShortcut"`, `"L2GlobalHoldToTalk"`,
/// `"L3GlobalWithMouseButtons"`, or `"L4DeviceAware"`).
required String level,
/// Stable backend identifier (e.g. `"focused"`).
required String backendId,
/// Coarse bound input class (e.g. `"keyboard"`,
/// `"mouse-side-button"`); empty when no binding is active.
required String boundInputClass,
}) = BridgeEvent_PttCapability;
/// Voice subsystem state snapshot (SDD-094). The Flutter
/// VoiceBar listens to this stream.
const factory BridgeEvent.voiceState({
/// True when the session is currently joined to a voice
/// channel and the audio engine is running.
required bool inChannel,
/// Active transmit mode.
required BridgeTransmitMode transmitMode,
/// True when the hard-mute clamp is engaged.
required bool mute,
/// Current release-tail in milliseconds (0..=500).
required int releaseTailMs,
}) = BridgeEvent_VoiceState;
}
/// Coarse OS-reported network state. Mirrors
/// [`chanora_core::NetworkState`] across the bridge.
enum BridgeNetworkState {
/// No signal seen yet.
unknown,
/// OS reports a usable network.
online,
/// OS reports no network.
offline,
}
/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings;
/// the bridge never carries raw key codes.
enum BridgePttInputClass {
/// No binding is active.
none,
/// A keyboard key.
keyboard,
/// A mouse side button (Mouse4 / Mouse5).
mouseSideButton,
}
/// Server snapshot as seen by Dart.
class BridgeSnapshot {
/// Server name.
final String serverName;
/// Welcome banner text.
final String welcomeMessage;
/// Server platform (e.g. "Linux").
final String platform;
/// Server version string.
final String version;
/// Channels currently known.
final List<BridgeChannel> channels;
/// Clients currently known.
final List<BridgeClient> clients;
/// Our own client id. Useful for the UI to highlight our row
/// in the client list and to know which channel we are in
/// without trusting the optimistic local state.
final BigInt ownClientId;
const BridgeSnapshot({
required this.serverName,
required this.welcomeMessage,
required this.platform,
required this.version,
required this.channels,
required this.clients,
required this.ownClientId,
});
@override
int get hashCode =>
serverName.hashCode ^
welcomeMessage.hashCode ^
platform.hashCode ^
version.hashCode ^
channels.hashCode ^
clients.hashCode ^
ownClientId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BridgeSnapshot &&
runtimeType == other.runtimeType &&
serverName == other.serverName &&
welcomeMessage == other.welcomeMessage &&
platform == other.platform &&
version == other.version &&
channels == other.channels &&
clients == other.clients &&
ownClientId == other.ownClientId;
}
/// Voice transmit mode mirror (SDD-095). Schema-controlled enum;
/// the wire encoding matches [`chanora_core::TransmitMode::as_u8`].
enum BridgeTransmitMode {
/// Push-to-talk (default).
ptt,
/// Continuous transmission while in channel and not muted.
continuous,
/// Voice-activity detection — reserved per DEC-030; v1 behaves
/// as `Continuous`.
voiceActivity,
}