feat: integrate chat voice and diagnostics client
This commit is contained in:
@@ -10,7 +10,7 @@ import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
||||
part 'api.freezed.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `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): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
|
||||
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate`
|
||||
|
||||
/// Return the platform-conventional log-file path as a string, or
|
||||
@@ -46,15 +46,6 @@ Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
|
||||
void handleRouteChange({required BridgeAudioRoute route}) =>
|
||||
RustLib.instance.api.crateApiHandleRouteChange(route: route);
|
||||
|
||||
/// Handle iOS AVAudioSession media-services reset (legacy, no route arg).
|
||||
///
|
||||
/// Called by the existing FRB-generated Dart binding. Uses
|
||||
/// `AudioRoute::Unknown` which triggers a route-change recompute.
|
||||
/// The AppDelegate now also calls `handle_media_services_reset_with_route`
|
||||
/// directly after rebuilding the session.
|
||||
void handleMediaServicesReset() =>
|
||||
RustLib.instance.api.crateApiHandleMediaServicesReset();
|
||||
|
||||
/// Handle iOS AVAudioSession media-services reset with the current
|
||||
/// route class. Called by AppDelegate after rebuilding the session.
|
||||
///
|
||||
@@ -74,11 +65,11 @@ void handleInterruptionEnded({required bool shouldResume}) => RustLib
|
||||
.api
|
||||
.crateApiHandleInterruptionEnded(shouldResume: shouldResume);
|
||||
|
||||
/// Set the push-to-talk state.
|
||||
/// Set the focused/on-screen push-to-talk hold 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.
|
||||
/// Binding capture chooses which physical key drives PTT, while this
|
||||
/// command carries the actual press/release edge for fallback focused
|
||||
/// keyboard handling and touch controls.
|
||||
Future<void> setPtt({required bool active}) =>
|
||||
RustLib.instance.api.crateApiSetPtt(active: active);
|
||||
|
||||
@@ -131,19 +122,17 @@ Future<void> setPttBinding({
|
||||
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() =>
|
||||
/// Read the current PTT capability descriptor. Matches the privacy-safe
|
||||
/// `BridgeEvent::PttCapability` event shape; useful for the initial UI
|
||||
/// render before the first event arrives.
|
||||
Future<BridgePttDescriptor> 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() =>
|
||||
/// Return the persisted PTT binding 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<BridgePttBinding> getPttBinding() =>
|
||||
RustLib.instance.api.crateApiGetPttBinding();
|
||||
|
||||
/// Move our own client to `channel_id`. Optional channel password
|
||||
@@ -176,6 +165,27 @@ Future<void> setOutputMuted({required bool muted}) =>
|
||||
Future<void> setOutputGain({required double gain}) =>
|
||||
RustLib.instance.api.crateApiSetOutputGain(gain: gain);
|
||||
|
||||
/// Set per-client output volume (SRS-075). `1.0` is unity, `0.0`
|
||||
/// mutes. No-op when client has no active voice queue. Volume is
|
||||
/// applied directly to the tsclientlib AudioQueue and takes effect
|
||||
/// immediately on the next render callback.
|
||||
Future<void> setClientVolume({
|
||||
required BigInt clientId,
|
||||
required double volume,
|
||||
}) => RustLib.instance.api.crateApiSetClientVolume(
|
||||
clientId: clientId,
|
||||
volume: volume,
|
||||
);
|
||||
|
||||
/// Send a text message to the specified target.
|
||||
Future<void> sendChatMessage({
|
||||
required String message,
|
||||
required BridgeMessageTarget target,
|
||||
}) => RustLib.instance.api.crateApiSendChatMessage(
|
||||
message: message,
|
||||
target: target,
|
||||
);
|
||||
|
||||
/// 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
|
||||
@@ -256,6 +266,19 @@ Future<BridgeAudioProcessingConfig> getAudioProcessingConfig() =>
|
||||
Future<BridgeAudioProcessingStats> audioProcessingStats() =>
|
||||
RustLib.instance.api.crateApiAudioProcessingStats();
|
||||
|
||||
/// List available audio input and output devices from the platform.
|
||||
Future<BridgeAudioDeviceList> listAudioDevices() =>
|
||||
RustLib.instance.api.crateApiListAudioDevices();
|
||||
|
||||
/// Set the preferred input device by name. Takes effect on next
|
||||
/// `start_audio`.
|
||||
Future<void> setInputDevice({String? name}) =>
|
||||
RustLib.instance.api.crateApiSetInputDevice(name: name);
|
||||
|
||||
/// Set the preferred output device by name.
|
||||
Future<void> setOutputDevice({String? name}) =>
|
||||
RustLib.instance.api.crateApiSetOutputDevice(name: name);
|
||||
|
||||
/// Configure the VAD model path.
|
||||
Future<void> setVadModelPath({required String path}) =>
|
||||
RustLib.instance.api.crateApiSetVadModelPath(path: path);
|
||||
@@ -273,6 +296,14 @@ Future<void> setIosVoiceProcessingMode({
|
||||
required BridgeIosVoiceProcessingMode mode,
|
||||
}) => RustLib.instance.api.crateApiSetIosVoiceProcessingMode(mode: mode);
|
||||
|
||||
/// Set the preferred audio output route (Android/iOS).
|
||||
void setAudioOutputRoute({required BridgeAudioRoute route}) =>
|
||||
RustLib.instance.api.crateApiSetAudioOutputRoute(route: route);
|
||||
|
||||
/// Called from Flutter when the app enters background/foreground.
|
||||
void recordLifecycleEvent({required String state}) =>
|
||||
RustLib.instance.api.crateApiRecordLifecycleEvent(state: state);
|
||||
|
||||
/// Bridge processing backend.
|
||||
enum BridgeAudioBackend {
|
||||
/// Platform voice processing.
|
||||
@@ -288,6 +319,53 @@ enum BridgeAudioBackend {
|
||||
noop,
|
||||
}
|
||||
|
||||
/// Audio device info from the platform.
|
||||
class BridgeAudioDevice {
|
||||
/// Human-readable device name.
|
||||
final String name;
|
||||
|
||||
/// True if the OS reports this as the default device.
|
||||
final bool isDefault;
|
||||
|
||||
const BridgeAudioDevice({required this.name, required this.isDefault});
|
||||
|
||||
@override
|
||||
int get hashCode => name.hashCode ^ isDefault.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BridgeAudioDevice &&
|
||||
runtimeType == other.runtimeType &&
|
||||
name == other.name &&
|
||||
isDefault == other.isDefault;
|
||||
}
|
||||
|
||||
/// List of available audio devices.
|
||||
class BridgeAudioDeviceList {
|
||||
/// Available input devices.
|
||||
final List<BridgeAudioDevice> inputDevices;
|
||||
|
||||
/// Available output devices.
|
||||
final List<BridgeAudioDevice> outputDevices;
|
||||
|
||||
const BridgeAudioDeviceList({
|
||||
required this.inputDevices,
|
||||
required this.outputDevices,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => inputDevices.hashCode ^ outputDevices.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BridgeAudioDeviceList &&
|
||||
runtimeType == other.runtimeType &&
|
||||
inputDevices == other.inputDevices &&
|
||||
outputDevices == other.outputDevices;
|
||||
}
|
||||
|
||||
/// P1 audio-processing configuration DTO.
|
||||
class BridgeAudioProcessingConfig {
|
||||
/// Route class.
|
||||
@@ -628,12 +706,17 @@ class BridgeChannel {
|
||||
/// True when the server marks the channel as password-protected.
|
||||
final bool hasPassword;
|
||||
|
||||
/// Talk power threshold required to speak in this channel.
|
||||
/// None means no talk-power restriction.
|
||||
final int? neededTalkPower;
|
||||
|
||||
const BridgeChannel({
|
||||
required this.id,
|
||||
required this.parent,
|
||||
required this.name,
|
||||
required this.order,
|
||||
required this.hasPassword,
|
||||
this.neededTalkPower,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -642,7 +725,8 @@ class BridgeChannel {
|
||||
parent.hashCode ^
|
||||
name.hashCode ^
|
||||
order.hashCode ^
|
||||
hasPassword.hashCode;
|
||||
hasPassword.hashCode ^
|
||||
neededTalkPower.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -653,7 +737,8 @@ class BridgeChannel {
|
||||
parent == other.parent &&
|
||||
name == other.name &&
|
||||
order == other.order &&
|
||||
hasPassword == other.hasPassword;
|
||||
hasPassword == other.hasPassword &&
|
||||
neededTalkPower == other.neededTalkPower;
|
||||
}
|
||||
|
||||
/// Client as seen by Dart.
|
||||
@@ -679,6 +764,12 @@ class BridgeClient {
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
final bool isServerQuery;
|
||||
|
||||
/// Current talk power value assigned by the server.
|
||||
final int talkPower;
|
||||
|
||||
/// True when the server has granted talk power regardless of numeric value.
|
||||
final bool talkPowerGranted;
|
||||
|
||||
const BridgeClient({
|
||||
required this.id,
|
||||
required this.channel,
|
||||
@@ -687,6 +778,8 @@ class BridgeClient {
|
||||
required this.outputMuted,
|
||||
required this.isSpeaking,
|
||||
required this.isServerQuery,
|
||||
required this.talkPower,
|
||||
required this.talkPowerGranted,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -697,7 +790,9 @@ class BridgeClient {
|
||||
inputMuted.hashCode ^
|
||||
outputMuted.hashCode ^
|
||||
isSpeaking.hashCode ^
|
||||
isServerQuery.hashCode;
|
||||
isServerQuery.hashCode ^
|
||||
talkPower.hashCode ^
|
||||
talkPowerGranted.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -710,7 +805,9 @@ class BridgeClient {
|
||||
inputMuted == other.inputMuted &&
|
||||
outputMuted == other.outputMuted &&
|
||||
isSpeaking == other.isSpeaking &&
|
||||
isServerQuery == other.isServerQuery;
|
||||
isServerQuery == other.isServerQuery &&
|
||||
talkPower == other.talkPower &&
|
||||
talkPowerGranted == other.talkPowerGranted;
|
||||
}
|
||||
|
||||
/// Bridge effect owner for AEC/NS/AGC.
|
||||
@@ -863,6 +960,27 @@ sealed class BridgeEvent with _$BridgeEvent {
|
||||
/// Resolved permission state.
|
||||
required PermissionStateKind state,
|
||||
}) = BridgeEvent_PermissionState;
|
||||
|
||||
/// A text message received from the server.
|
||||
const factory BridgeEvent.chatMessage({
|
||||
/// Client id of the sender.
|
||||
required BigInt senderId,
|
||||
|
||||
/// Nickname of the sender.
|
||||
required String senderName,
|
||||
|
||||
/// Message content.
|
||||
required String message,
|
||||
|
||||
/// Target scope (server/channel/private/poke).
|
||||
required BridgeMessageTarget target,
|
||||
}) = BridgeEvent_ChatMessage;
|
||||
|
||||
/// Audio route changed (speaker/earpiece/BT/wired).
|
||||
const factory BridgeEvent.audioRouteChanged({
|
||||
/// The new audio route.
|
||||
required BridgeAudioRoute route,
|
||||
}) = BridgeEvent_AudioRouteChanged;
|
||||
}
|
||||
|
||||
/// Bridge iOS voice-processing mode.
|
||||
@@ -874,6 +992,25 @@ enum BridgeIosVoiceProcessingMode {
|
||||
sonoraExperimental,
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class BridgeMessageTarget with _$BridgeMessageTarget {
|
||||
const BridgeMessageTarget._();
|
||||
|
||||
/// Broadcast to entire server.
|
||||
const factory BridgeMessageTarget.server() = BridgeMessageTarget_Server;
|
||||
|
||||
/// Broadcast to current channel.
|
||||
const factory BridgeMessageTarget.channel() = BridgeMessageTarget_Channel;
|
||||
|
||||
/// Private message to a specific client.
|
||||
const factory BridgeMessageTarget.client(BigInt field0) =
|
||||
BridgeMessageTarget_Client;
|
||||
|
||||
/// Poke a specific client.
|
||||
const factory BridgeMessageTarget.poke(BigInt field0) =
|
||||
BridgeMessageTarget_Poke;
|
||||
}
|
||||
|
||||
/// Coarse OS-reported network state. Mirrors
|
||||
/// [`chanora_core::NetworkState`] across the bridge.
|
||||
enum BridgeNetworkState {
|
||||
@@ -887,6 +1024,60 @@ enum BridgeNetworkState {
|
||||
offline,
|
||||
}
|
||||
|
||||
/// Persisted PTT binding display state for the UI.
|
||||
class BridgePttBinding {
|
||||
/// Stable input category string (`""`, `"keyboard"`, or
|
||||
/// `"mouse-side-button"`).
|
||||
final String inputClass;
|
||||
|
||||
/// Display-only key label; empty when no binding is active.
|
||||
final String keyLabel;
|
||||
|
||||
const BridgePttBinding({required this.inputClass, required this.keyLabel});
|
||||
|
||||
@override
|
||||
int get hashCode => inputClass.hashCode ^ keyLabel.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BridgePttBinding &&
|
||||
runtimeType == other.runtimeType &&
|
||||
inputClass == other.inputClass &&
|
||||
keyLabel == other.keyLabel;
|
||||
}
|
||||
|
||||
/// Privacy-safe PTT capability descriptor for the UI.
|
||||
class BridgePttDescriptor {
|
||||
/// Stable capability level name.
|
||||
final String level;
|
||||
|
||||
/// Stable backend identifier.
|
||||
final String backendId;
|
||||
|
||||
/// Coarse bound input class; empty when no binding is active.
|
||||
final String boundInputClass;
|
||||
|
||||
const BridgePttDescriptor({
|
||||
required this.level,
|
||||
required this.backendId,
|
||||
required this.boundInputClass,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
level.hashCode ^ backendId.hashCode ^ boundInputClass.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BridgePttDescriptor &&
|
||||
runtimeType == other.runtimeType &&
|
||||
level == other.level &&
|
||||
backendId == other.backendId &&
|
||||
boundInputClass == other.boundInputClass;
|
||||
}
|
||||
|
||||
/// Coarse PTT input class (gen2 v0.9.3 / DEC-026). Stable strings;
|
||||
/// the bridge never carries raw key codes.
|
||||
enum BridgePttInputClass {
|
||||
|
||||
Reference in New Issue
Block a user