Compare commits

..
Author SHA1 Message Date
Edison Jwa e0060f3c19 feat(voice): unified mobile voice bar with gesture-isolated PTT row
Replace separate VoiceStatusChip + VoicePttButton with a single
CompactVoiceBar widget that combines both into a two-row layout:

- Control row (tap): status text, mute, deafen, settings chevron
- PTT row (hold): full-width hold-to-talk, shown only in PTT mode

Gesture isolation prevents mis-touch between rows: the control row
uses tap-only InkWell/IconButton while the PTT row uses a raw
Listener for pointer-down/up events.

Key changes:
- Add CompactVoiceBar widget with state-colored container (normal,
  muted, talk-power-blocked)
- Remove mute/deafen IconButtons from AppBar headerActions
- Restructure voice details sheet into primary section + collapsible
  ExpansionTiles (audio processing, PTT capability, debug)
- Optimistic state updates for mute/deafen to eliminate tap delay
- Instant PTT visual feedback (no AnimatedContainer fade)
- Constant geometry across all states (no layout shift on toggle)
2026-06-04 22:46:19 +09:00
Edison Jwa 8cb5a7a258 feat(macos): add macOS permissions service for Input Monitoring, Local Network, and Notifications
Add MacOSPermissionsService (Dart) + native MethodChannel handler (Swift)
for macOS-specific permissions not covered by permission_handler:

- Input Monitoring (CGPreflightListenEventAccess /
  CGRequestListenEventAccess) for global PTT via Event Tap
- Local Network Privacy prompt (NWBrowser for _ts3._tcp, macOS 15+)
- Notifications (UNUserNotificationCenter authorization)

Trace: SRS-198, SRS-297, SRS-300, SysRS-166, SDD-091

Changes:
- Info.plist: add NSBonjourServices array with _ts3._tcp
- macos_permissions_service.dart: Dart service with MethodChannel,
  ValueNotifier states, PTT capability derivation (L0Focused /
  L1MacOSEventTap), non-macOS short-circuit
- MainFlutterWindow.swift: native handler registered as FlutterPlugin,
  Input Monitoring check/request/polling, NWBrowser trigger with
  denial detection, UNUserNotificationCenter request
- main.dart: wire service into bootstrap lifecycle, listen for PTT
  capability changes from Input Monitoring state
- macos_permissions_service_test.dart: 17 unit tests covering inbound
  state changes, outbound calls, lifecycle, error handling, platform
  behavior (179/179 full suite pass)
2026-06-04 08:21:07 +09:00
Edison Jwa 484dad1072 fix(audio): eliminate Android output stutter via Oboe config + lock-free callback
Phase 1 — Oboe configuration:
- Change output stream from Usage::VoiceCommunication to Usage::Game with
  ContentType::Sonification to avoid forcing the Legacy (OpenSL ES) data
  path on most devices (Oboe issue #2075)
- Switch output format from i16 Mono to f32 Stereo, matching Qint's proven
  configuration and eliminating per-callback downmix conversion
- Set buffer size to 2x burst after stream open, reducing default buffer
  from 8-20x burst to 2x burst for lower latency
- Remove scratch Mutex<Vec<f32>>; callback writes directly to Oboe buffer

Phase 2 — Lock-free output callback:
- Add audio_event_queue.rs: lock-free SPSC bridge using crossbeam ArrayQueue
  with separate packet (lossy) and control (reliable) channels
- OutputCallback now owns AudioHandler directly (no Arc<Mutex<>> on Android)
- Inbound forwarder pushes packets via AudioEventProducer (no mutex)
- set_client_volume pushes control commands via event queue on Android
- iOS/desktop Arc<Mutex<AudioHandler>> path unchanged
2026-06-04 00:51:25 +09:00
228 changed files with 8685 additions and 37550 deletions
+31 -46
View File
@@ -1,49 +1,34 @@
# audiopus_sys calls cmake::build(opus_path), so downstream Cargo env cannot # Environment variables set for all cargo invocations in this workspace.
# call cmake-rs Config::define() to override CMake's MSVC Debug CRT defaults. # CMAKE_POLICY_VERSION_MINIMUM is required for audiopus_sys's bundled
# Instead, point cmake-rs at a small wrapper that injects -D cache/policy # Opus CMake build to succeed on CMake 4.x (which removed compatibility
# variables during configure while passing cmake --build / --version / -E / # with cmake_minimum_required < 3.5). audiopus_sys v0.2.2 bundles
# --install / --open through unchanged. This keeps Opus Debug builds on # Opus 1.3.1 whose CMakeLists.txt uses a very old minimum version.
# Rust's release dynamic CRT (/MD) instead of CMake's default debug CRT
# (/MDd), which otherwise pulls in unresolved __imp__CrtDbgReportW symbols
# at test link.
#
# IMPORTANT: Cargo's `[target.<triple>]` config sections only forward a
# fixed allowlist of keys (linker, runner, rustflags, rustdocflags, ar)
# to build scripts. Arbitrary keys such as `CMAKE` placed under
# `[target.<triple>]` are silently ignored and never reach the
# audiopus_sys build script. cmake-rs (via cc-style env resolution)
# looks up CMAKE in this order:
# 1. CMAKE_<target-triple-with-dashes>
# 2. CMAKE_<target_triple_with_underscores>
# 3. TARGET_CMAKE (or HOST_CMAKE when host == target)
# 4. CMAKE
# We therefore scope the wrapper to Windows MSVC targets by setting the
# target-suffixed variant in the global [env] section. Non-Windows
# hosts (macOS, Linux, iOS, Android) never see CMAKE set and invoke
# `cmake` directly.
#
# NOTE: CMAKE_POLICY_DEFAULT_CMP0091 and CMAKE_MSVC_RUNTIME_LIBRARY cannot
# be set via the process environment because CMake does NOT auto-import
# them into its cache; they must be passed as `-D` definitions, which the
# wrapper does.
#
# iOS deployment target (DEC-003: iOS 13.0 minimum) is NOT set here. It is
# enforced in two places that own the iOS build:
# 1. tools/build-ios.sh — sets IPHONEOS_DEPLOYMENT_TARGET for the cargo
# invocation and bypasses audiopus_sys's CMake build via
# LIBOPUS_STATIC=1 / LIBOPUS_NO_PKG=1 / LIBOPUS_LIB_DIR.
# 2. apps/chanora_flutter/ios/Runner.xcodeproj — sets the Xcode
# IPHONEOS_DEPLOYMENT_TARGET build setting for the final link.
# Setting it globally here would make native macOS `cargo check` runs try
# to link iPhone objects against the macOS SDK.
[env] [env]
CMAKE_POLICY_VERSION_MINIMUM = "3.5" CMAKE_POLICY_VERSION_MINIMUM = "3.5"
# Scope the cmake wrapper to Windows MSVC targets only via the # iOS builds must set IPHONEOS_DEPLOYMENT_TARGET in the invoking script
# target-suffixed env var name that cc/cmake-rs already resolve. # or Xcode build phase. Do not set it globally here: native macOS cargo
# Force = true so a developer's pre-existing CMAKE_x86_64-pc-windows-msvc # checks also compile bundled C/C++ dependencies, and a global iOS
# does not silently bypass the wrapper. Relative = true so the path # deployment target makes clang try to link iPhone objects against the
# resolves from the workspace root regardless of where cargo is invoked. # macOS SDK.
CMAKE_x86_64-pc-windows-msvc = { value = "tools/cmake-msvc-release-crt.cmd", force = true, relative = true }
CMAKE_aarch64-pc-windows-msvc = { value = "tools/cmake-msvc-release-crt.cmd", force = true, relative = true } # iOS target linker flags (DEC-003: minimum deployment target iOS 13.0).
#
# These rustflags pass -miphoneos-version-min=13.0 to the linker, ensuring
# the final binary targets iOS 13.0+. This is defense-in-depth alongside
# the IPHONEOS_DEPLOYMENT_TARGET env var above — the env var affects C
# compilation (cc crate, CMake), while these rustflags affect the final
# link step.
#
# NOTE: The canonical iOS build is done via tools/build-ios.sh, which
# sets LIBOPUS_STATIC=1, LIBOPUS_NO_PKG=1, and LIBOPUS_LIB_DIR to
# bypass audiopus_sys's CMake build entirely.
[target.aarch64-apple-ios]
rustflags = ["-C", "link-arg=-miphoneos-version-min=13.0"]
[target.aarch64-apple-ios-sim]
rustflags = ["-C", "link-arg=-miphonesimulator-version-min=13.0"]
[target.x86_64-apple-ios]
rustflags = ["-C", "link-arg=-miphonesimulator-version-min=13.0"]
+15 -124
View File
@@ -25,10 +25,15 @@ jobs:
run: cargo check --workspace --locked run: cargo check --workspace --locked
- name: cargo test --workspace - name: cargo test --workspace
env: env:
# Storage tests must not hit the real OS keyring on CI:
# there is no D-Bus session available and the call would
# block. The runtime code carries the same toggle for
# headless / sandboxed environments.
CHANORA_DISABLE_KEYRING: "1" CHANORA_DISABLE_KEYRING: "1"
run: cargo test --workspace --locked --no-fail-fast run: cargo test --workspace --locked --no-fail-fast
- name: cargo clippy - name: cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings run: cargo clippy --workspace --all-targets -- -D warnings
continue-on-error: true
supply-chain: supply-chain:
name: cargo deny (licenses + advisories + bans + sources) name: cargo deny (licenses + advisories + bans + sources)
@@ -38,6 +43,9 @@ jobs:
- uses: EmbarkStudios/cargo-deny-action@v2 - uses: EmbarkStudios/cargo-deny-action@v2
with: with:
command: check command: check
# `licenses` enforces the DEC-020 license posture; the
# other three are minimal supply-chain hygiene per
# `docs/governance/legal-review-readiness.md` §5.
arguments: --workspace --all-features arguments: --workspace --all-features
license-inventory: license-inventory:
@@ -50,6 +58,10 @@ jobs:
- name: Install cargo-about - name: Install cargo-about
run: cargo install --locked --features cli cargo-about run: cargo install --locked --features cli cargo-about
- name: Regenerate inventory and compare - name: Regenerate inventory and compare
# Build the inventory in a temp file and diff against the
# committed copy. CI fails when the committed inventory is
# stale, forcing contributors to run the tool locally
# before opening a PR that touches the dependency tree.
run: | run: |
cargo about generate --output-file /tmp/license-inventory.md about-md.hbs cargo about generate --output-file /tmp/license-inventory.md about-md.hbs
diff docs/security/license-inventory.md /tmp/license-inventory.md \ diff docs/security/license-inventory.md /tmp/license-inventory.md \
@@ -68,6 +80,9 @@ jobs:
run: flutter pub get run: flutter pub get
- name: Regenerate Flutter license inventory and compare - name: Regenerate Flutter license inventory and compare
env: env:
# Resolved by the wrapper from $HOME/sdks/flutter when
# not set; CI's subosito/flutter-action puts flutter on
# PATH but exports the SDK root under FLUTTER_ROOT.
FLUTTER_ROOT: ${{ env.FLUTTER_ROOT }} FLUTTER_ROOT: ${{ env.FLUTTER_ROOT }}
run: | run: |
./tools/dump_flutter_licenses.sh ./tools/dump_flutter_licenses.sh
@@ -121,127 +136,3 @@ jobs:
if: steps.silero-coreml.outputs.available == 'true' if: steps.silero-coreml.outputs.available == 'true'
working-directory: apps/chanora_flutter working-directory: apps/chanora_flutter
run: flutter build ios --release --no-codesign run: flutter build ios --release --no-codesign
- name: xcodebuild archive verification
if: steps.silero-coreml.outputs.available == 'true'
working-directory: apps/chanora_flutter
run: |
xcodebuild archive \
-workspace ios/Runner.xcworkspace \
-scheme Runner \
-archive build/Runner.xcarchive \
CODE_SIGNING_ALLOWED=NO \
| xcpretty || { echo "::error::xcodebuild archive failed — see issue-history-analysis.md §4 'Xcode Archive vs build divergence'"; exit 1; }
android-build:
name: Android build (${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-linux-android
abi: arm64-v8a
- target: armv7-linux-androideabi
abi: armeabi-v7a
- target: x86_64-linux-android
abi: x86_64
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cargo-ndk
run: cargo install --locked cargo-ndk
- name: Setup NDK
run: |
ANDROID_ROOT="/usr/local/lib/android/sdk"
SDKMANAGER="$ANDROID_ROOT/cmdline-tools/latest/bin/sdkmanager"
echo "y" | $SDKMANAGER "ndk;27.0.12077973"
echo "ANDROID_NDK_HOME=$ANDROID_ROOT/ndk/27.0.12077973" >> "$GITHUB_ENV"
- name: cargo ndk build
run: cargo ndk -t ${{ matrix.abi }} build --workspace --locked
windows-build:
name: Windows build
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: cargo check --workspace
run: cargo check --workspace --locked
macos-build:
name: macOS build
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: cargo check --workspace
run: cargo check --workspace --locked
linux-multi-distro:
name: Linux build (${{ matrix.distro }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- distro: ubuntu
image: ubuntu:24.04
install: |
apt-get update
apt-get install -y curl build-essential pkg-config \
libasound2-dev libpulse-dev libdbus-1-dev libsdl2-dev libopus-dev
- distro: fedora
image: fedora:latest
install: |
dnf install -y curl gcc pkg-config \
alsa-lib-devel pulseaudio-libs-devel dbus-devel SDL2-devel opus-devel
- distro: arch
image: archlinux:latest
install: |
pacman -Syu --noconfirm
pacman -S --noconfirm curl base-devel pkg-config \
alsa-lib pulseaudio dbus sdl2 opus
container:
image: ${{ matrix.image }}
steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: ${{ matrix.install }}
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: cargo check --workspace
run: cargo check --workspace
coverage:
name: cargo llvm-cov
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: System deps
run: |
sudo apt-get update
sudo apt-get install -y \
libasound2-dev libpulse-dev pkg-config \
libdbus-1-dev libsdl2-dev libopus-dev
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
run: cargo install --locked cargo-llvm-cov
- name: Generate coverage
env:
CHANORA_DISABLE_KEYRING: "1"
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: lcov-report
path: lcov.info
-17
View File
@@ -117,23 +117,6 @@ opencode.json
# iOS framework build artifacts produced by chanora_bridge.podspec # iOS framework build artifacts produced by chanora_bridge.podspec
/apps/chanora_flutter/ios/Frameworks/ /apps/chanora_flutter/ios/Frameworks/
# macOS framework build artifacts produced by chanora_bridge.podspec
# (prepare_command + script_phase rm -rf and regenerate this tree on
# every pod install AND every Xcode build, so tracking it in git is
# pure waste — the committed binary was ~40 MB per commit).
/apps/chanora_flutter/macos/Frameworks/
.opencode/ .opencode/
.omo/
AGENTS.md AGENTS.md
Screenshot 2026-05-17 at 22.23.07.png Screenshot 2026-05-17 at 22.23.07.png
# Xcode archive / export bundles (generated by Product > Archive > Distribute)
**/Chanora */
**/*.xcarchive/
**/*.ipa
**/*.dSYM/
# macOS release zip bundles produced by local release scripts
/chanora-v*.zip
+12 -16
View File
@@ -8,10 +8,8 @@ This project follows a Conventional Commits style workflow.
The v0.3.0 milestone transitions Chanora from an internal-beta voice The v0.3.0 milestone transitions Chanora from an internal-beta voice
prototype to a cross-platform baseline client with event-driven UI, prototype to a cross-platform baseline client with event-driven UI,
visible per-client audio state, non-self client info parity, and per-user audio controls, non-self client info parity, and CI-hardened
documented host Rust workspace plus Flutter validation gates. Android Android / iOS / macOS / Linux builds.
target compile/install/smoke evidence remains blocked locally pending the
required NDK compiler and an authorized ADB target.
### Added ### Added
@@ -19,9 +17,10 @@ required NDK compiler and an authorized ADB target.
deltas (client join/leave/move/update, channel add/remove/update) deltas (client join/leave/move/update, channel add/remove/update)
flow through a typed `ProtocolDelta` enum and update the Flutter UI flow through a typed `ProtocolDelta` enum and update the Flutter UI
in real time. Channel switching is instant. in real time. Channel switching is instant.
- **Per-client audio state visibility.** Client rows surface - **Per-user volume controls.** Each client in the snapshot gets an
muted/deafened state in avatar badges. Per-user volume UI, persistence, independent volume slider persisted in the bridge layer. Avatar
and mixer wiring remain tracked as follow-up work. badges show muted/deafened state. Volume adjustments take effect
immediately on the audio mix.
- **Non-self client info parity with Qint.** The Info tab now populates - **Non-self client info parity with Qint.** The Info tab now populates
connection metadata (name, description, created, last connected, connection metadata (name, description, created, last connected,
connections, transfer, ping deviation) for other clients via an connections, transfer, ping deviation) for other clients via an
@@ -30,10 +29,9 @@ required NDK compiler and an authorized ADB target.
- **Ping deviation in client profiles.** `ping_deviation_milliseconds` - **Ping deviation in client profiles.** `ping_deviation_milliseconds`
propagated from protocol DTO through bridge API to Dart, with a propagated from protocol DTO through bridge API to Dart, with a
conditional l10n row in the client info sheet (en + zh). conditional l10n row in the client info sheet (en + zh).
- **Apple CoreML Silero VAD scaffolding/assets** for iOS / macOS when - **Apple CoreML Silero VAD** as the preferred voice activity detector
the private `silero-coreml` SwiftPM package is available. Product on iOS / macOS when the private `silero-coreml` SwiftPM submodule is
`VoiceActivity` remains reserved/disabled per DEC-030 until a later available. WebRTC VAD remains the runtime fallback.
baseline enables and verifies it.
- **TeamSpeak address resolver** (`chanora_resolver`) for DNS SRV - **TeamSpeak address resolver** (`chanora_resolver`) for DNS SRV
lookups and `ts3server://` URI handling. lookups and `ts3server://` URI handling.
- **Per-ABI Android APK splitting.** `flutter build apk - **Per-ABI Android APK splitting.** `flutter build apk
@@ -52,9 +50,8 @@ required NDK compiler and an authorized ADB target.
- **iOS / macOS audio lifecycle hardened.** Voice unit restart-in-place, - **iOS / macOS audio lifecycle hardened.** Voice unit restart-in-place,
serialized lifecycle events, WebRTC VAD on iOS, unblocked connect-time serialized lifecycle events, WebRTC VAD on iOS, unblocked connect-time
audio startup. audio startup.
- **Linux native audio path promoted** with ONNX Runtime VAD assets - **Linux native audio path promoted** with ONNX Runtime bundled for
bundled for future `VoiceActivity` work. Desktop voice I/O works on VAD. Desktop voice I/O works on PipeWire / PulseAudio.
PipeWire / PulseAudio; product `VoiceActivity` remains disabled.
- **Android audio routing** uses `MODE_IN_COMMUNICATION`, proper - **Android audio routing** uses `MODE_IN_COMMUNICATION`, proper
startup permission flow, and system back-button integration. startup permission flow, and system back-button integration.
- **`SnapshotChanged` event removed.** Replaced by the typed delta - **`SnapshotChanged` event removed.** Replaced by the typed delta
@@ -62,8 +59,7 @@ required NDK compiler and an authorized ADB target.
Flutter). Flutter).
- **Prefetch crate renamed** from the PoC-era name to - **Prefetch crate renamed** from the PoC-era name to
`chanora_prefetch`. All docs, specs, and code updated. `chanora_prefetch`. All docs, specs, and code updated.
- **Flutter app version/build bumped to `0.3.0+100`.** Rust workspace - **Build number bumped to 76.**
packages remain versioned separately at `0.2.0-beta.1`.
- **Flutter bridge regenerated** for `flutter_rust_bridge` 2.12.0. - **Flutter bridge regenerated** for `flutter_rust_bridge` 2.12.0.
### Fixed ### Fixed
Generated
+91 -443
View File
@@ -70,15 +70,6 @@ dependencies = [
"backtrace", "backtrace",
] ]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "alsa" name = "alsa"
version = "0.11.0" version = "0.11.0"
@@ -148,111 +139,12 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [ dependencies = [
"event-listener 5.4.1", "event-listener",
"event-listener-strategy", "event-listener-strategy",
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-global-executor"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
dependencies = [
"async-channel 2.5.0",
"async-executor",
"async-io",
"async-lock",
"blocking",
"futures-lite",
"once_cell",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener 5.4.1",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel 2.5.0",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener 5.4.1",
"futures-lite",
"rustix",
]
[[package]] [[package]]
name = "async-recursion" name = "async-recursion"
version = "1.1.1" version = "1.1.1"
@@ -264,57 +156,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-std"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b"
dependencies = [
"async-channel 1.9.0",
"async-global-executor",
"async-io",
"async-lock",
"async-process",
"crossbeam-utils",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.89" version = "0.1.89"
@@ -407,12 +248,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -464,19 +299,6 @@ dependencies = [
"objc2", "objc2",
] ]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel 2.5.0",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]] [[package]]
name = "build-target" name = "build-target"
version = "0.4.0" version = "0.4.0"
@@ -521,32 +343,6 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cacache"
version = "13.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5063741c7b2e260bbede781cf4679632dd90e2718e99f7715e46824b65670b"
dependencies = [
"async-std",
"digest 0.10.7",
"either",
"futures",
"hex",
"libc",
"memmap2",
"miette",
"reflink-copy",
"serde",
"serde_derive",
"serde_json",
"sha1",
"sha2",
"ssri",
"tempfile",
"thiserror 1.0.69",
"walkdir",
]
[[package]] [[package]]
name = "cast" name = "cast"
version = "0.3.0" version = "0.3.0"
@@ -623,21 +419,21 @@ name = "chanora_audio"
version = "0.2.0-beta.1" version = "0.2.0-beta.1"
dependencies = [ dependencies = [
"audiopus", "audiopus",
"bytemuck",
"chanora_protocol", "chanora_protocol",
"coreaudio-rs", "coreaudio-rs",
"cpal", "cpal",
"criterion", "criterion",
"crossbeam", "crossbeam",
"crossbeam-utils",
"dhat", "dhat",
"dispatch2", "dispatch2",
"futures-util", "futures-util",
"jni 0.22.4", "jni 0.21.1",
"ndarray", "ndarray",
"ndk-context", "ndk-context",
"oboe", "oboe",
"ort", "ort",
"rand 0.10.1", "rand 0.8.6",
"rustfft", "rustfft",
"sdl2", "sdl2",
"serde_json", "serde_json",
@@ -648,7 +444,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"tsclientlib", "tsclientlib",
"webrtc-vad", "webrtc-vad",
"windows", "windows 0.54.0",
"zbus", "zbus",
] ]
@@ -663,7 +459,6 @@ dependencies = [
"log", "log",
"ndk-context", "ndk-context",
"serde", "serde",
"serde_json",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@@ -671,23 +466,11 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "chanora_cache"
version = "0.2.0-beta.1"
dependencies = [
"cacache",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "chanora_core" name = "chanora_core"
version = "0.2.0-beta.1" version = "0.2.0-beta.1"
dependencies = [ dependencies = [
"chanora_audio", "chanora_audio",
"chanora_cache",
"chanora_diagnostics", "chanora_diagnostics",
"chanora_prefetch", "chanora_prefetch",
"chanora_protocol", "chanora_protocol",
@@ -723,12 +506,11 @@ name = "chanora_protocol"
version = "0.2.0-beta.1" version = "0.2.0-beta.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1", "base64",
"chanora_resolver", "chanora_resolver",
"futures", "futures",
"reqwest 0.13.4", "reqwest 0.13.4",
"serde", "serde",
"serde_json",
"thiserror 2.0.18", "thiserror 2.0.18",
"time", "time",
"tokio", "tokio",
@@ -741,7 +523,7 @@ dependencies = [
[[package]] [[package]]
name = "chanora_resolver" name = "chanora_resolver"
version = "0.2.0-beta.1" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hickory-resolver", "hickory-resolver",
@@ -763,7 +545,7 @@ dependencies = [
name = "chanora_storage" name = "chanora_storage"
version = "0.2.0-beta.1" version = "0.2.0-beta.1"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"chacha20poly1305", "chacha20poly1305",
"keyring", "keyring",
"rand 0.8.6", "rand 0.8.6",
@@ -935,15 +717,14 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.18.0" version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9dd2b2151ebb4d5866c804d89fe28244bfb6b74481b9b4d406e4ec4d7f88ce5" checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
dependencies = [ dependencies = [
"alsa", "alsa",
"block2",
"coreaudio-rs", "coreaudio-rs",
"dasp_sample", "dasp_sample",
"jni 0.22.4", "jni 0.21.1",
"js-sys", "js-sys",
"libc", "libc",
"mach2", "mach2",
@@ -958,9 +739,10 @@ dependencies = [
"objc2-core-audio-types", "objc2-core-audio-types",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
"windows", "windows 0.62.2",
"windows-core",
] ]
[[package]] [[package]]
@@ -983,24 +765,25 @@ dependencies = [
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.8.2" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [ dependencies = [
"alloca",
"anes", "anes",
"cast", "cast",
"ciborium", "ciborium",
"clap", "clap",
"criterion-plot", "criterion-plot",
"itertools 0.13.0", "is-terminal",
"itertools 0.10.5",
"num-traits", "num-traits",
"once_cell",
"oorandom", "oorandom",
"page_size",
"plotters", "plotters",
"rayon", "rayon",
"regex", "regex",
"serde", "serde",
"serde_derive",
"serde_json", "serde_json",
"tinytemplate", "tinytemplate",
"walkdir", "walkdir",
@@ -1008,12 +791,12 @@ dependencies = [
[[package]] [[package]]
name = "criterion-plot" name = "criterion-plot"
version = "0.8.2" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [ dependencies = [
"cast", "cast",
"itertools 0.13.0", "itertools 0.10.5",
] ]
[[package]] [[package]]
@@ -1448,12 +1231,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "5.4.1" version = "5.4.1"
@@ -1471,7 +1248,7 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [ dependencies = [
"event-listener 5.4.1", "event-listener",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1774,18 +1551,6 @@ dependencies = [
"time", "time",
] ]
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "group" name = "group"
version = "0.13.0" version = "0.13.0"
@@ -2064,7 +1829,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@@ -2220,7 +1985,7 @@ dependencies = [
"socket2", "socket2",
"widestring", "widestring",
"windows-registry", "windows-registry",
"windows-result", "windows-result 0.4.1",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
@@ -2234,10 +1999,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "itertools" name = "is-terminal"
version = "0.13.0" version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [ dependencies = [
"either", "either",
] ]
@@ -2369,15 +2145,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -2462,9 +2229,6 @@ name = "log"
version = "0.4.31" version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
dependencies = [
"value-bag",
]
[[package]] [[package]]
name = "lru-slab" name = "lru-slab"
@@ -2474,9 +2238,12 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "mach2" name = "mach2"
version = "0.6.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
@@ -2513,15 +2280,6 @@ version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "memmap2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.9.1" version = "0.9.1"
@@ -2531,29 +2289,6 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "miette"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
dependencies = [
"miette-derive",
"once_cell",
"thiserror 1.0.69",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -2805,7 +2540,6 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
dependencies = [ dependencies = [
"bitflags 2.12.1",
"objc2", "objc2",
"objc2-foundation", "objc2-foundation",
] ]
@@ -3018,16 +2752,6 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@@ -3084,17 +2808,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]] [[package]]
name = "pkcs8" name = "pkcs8"
version = "0.10.2" version = "0.10.2"
@@ -3139,20 +2852,6 @@ dependencies = [
"plotters-backend", "plotters-backend",
] ]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "poly1305" name = "poly1305"
version = "0.8.0" version = "0.8.0"
@@ -3479,18 +3178,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "reflink-copy"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13362233b147e57674c37b802d216b7c5e3dcccbed8967c84f0d8d223868ae27"
dependencies = [
"cfg-if",
"libc",
"rustix",
"windows",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.3"
@@ -3526,7 +3213,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bytes", "bytes",
"futures-core", "futures-core",
"http", "http",
@@ -3564,7 +3251,7 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@@ -3811,9 +3498,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "sdl2" name = "sdl2"
version = "0.38.0" version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"lazy_static", "lazy_static",
@@ -3823,9 +3510,9 @@ dependencies = [
[[package]] [[package]]
name = "sdl2-sys" name = "sdl2-sys"
version = "0.38.0" version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -3980,17 +3667,6 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
] ]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@@ -4170,23 +3846,6 @@ dependencies = [
"der", "der",
] ]
[[package]]
name = "ssri"
version = "9.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7a2b3c2bc9693bcb40870c4e9b5bf0d79f9cb46273321bf855ec513e919082"
dependencies = [
"base64 0.21.7",
"digest 0.10.7",
"hex",
"miette",
"serde",
"sha-1",
"sha2",
"thiserror 1.0.69",
"xxhash-rust",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@@ -4685,7 +4344,7 @@ name = "ts-bookkeeping"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"heck", "heck",
"itertools 0.14.0", "itertools 0.14.0",
"num-derive", "num-derive",
@@ -4706,7 +4365,7 @@ version = "0.2.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [ dependencies = [
"audiopus", "audiopus",
"base64 0.22.1", "base64",
"futures", "futures",
"git-testament", "git-testament",
"hickory-net", "hickory-net",
@@ -4734,7 +4393,7 @@ version = "0.2.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [ dependencies = [
"aes", "aes",
"base64 0.22.1", "base64",
"curve25519-dalek-ng", "curve25519-dalek-ng",
"eax", "eax",
"futures", "futures",
@@ -4763,7 +4422,7 @@ name = "tsproto-packets"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bitflags 2.12.1", "bitflags 2.12.1",
"num-derive", "num-derive",
"num-traits", "num-traits",
@@ -4778,7 +4437,7 @@ name = "tsproto-structs"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/EdisonJwa/tsclientlib.git?branch=fix%2Fp256-short-coordinate-pad#8b7a3226c692319b714ea1d32fd5ded05911aa40" source = "git+https://github.com/EdisonJwa/tsclientlib.git?branch=fix%2Fp256-short-coordinate-pad#8b7a3226c692319b714ea1d32fd5ded05911aa40"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"csv", "csv",
"heck", "heck",
"once_cell", "once_cell",
@@ -4791,7 +4450,7 @@ name = "tsproto-structs"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e" source = "git+https://github.com/ReSpeak/tsclientlib.git?rev=04aa2491#04aa24917abbf6a0c8442a79742d6d2d40ecf71e"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"csv", "csv",
"heck", "heck",
"once_cell", "once_cell",
@@ -4804,7 +4463,7 @@ name = "tsproto-types"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/EdisonJwa/tsclientlib.git?branch=fix%2Fp256-short-coordinate-pad#8b7a3226c692319b714ea1d32fd5ded05911aa40" source = "git+https://github.com/EdisonJwa/tsclientlib.git?branch=fix%2Fp256-short-coordinate-pad#8b7a3226c692319b714ea1d32fd5ded05911aa40"
dependencies = [ dependencies = [
"base64 0.22.1", "base64",
"bitflags 2.12.1", "bitflags 2.12.1",
"curve25519-dalek-ng", "curve25519-dalek-ng",
"elliptic-curve", "elliptic-curve",
@@ -4848,12 +4507,6 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@@ -4912,12 +4565,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "value-bag"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@@ -5121,22 +4768,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.11" version = "0.1.11"
@@ -5147,10 +4778,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "windows"
version = "0.4.0" version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core 0.54.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
@@ -5159,7 +4794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [ dependencies = [
"windows-collections", "windows-collections",
"windows-core", "windows-core 0.62.2",
"windows-future", "windows-future",
"windows-numerics", "windows-numerics",
] ]
@@ -5170,7 +4805,17 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result 0.1.2",
"windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -5182,7 +4827,7 @@ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-link", "windows-link",
"windows-result", "windows-result 0.4.1",
"windows-strings", "windows-strings",
] ]
@@ -5192,7 +4837,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.62.2",
"windows-link", "windows-link",
"windows-threading", "windows-threading",
] ]
@@ -5231,7 +4876,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.62.2",
"windows-link", "windows-link",
] ]
@@ -5242,10 +4887,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [ dependencies = [
"windows-link", "windows-link",
"windows-result", "windows-result 0.4.1",
"windows-strings", "windows-strings",
] ]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.4.1" version = "0.4.1"
@@ -5604,12 +5258,6 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.2" version = "0.8.2"
@@ -5643,7 +5291,7 @@ dependencies = [
"async-recursion", "async-recursion",
"async-trait", "async-trait",
"enumflags2", "enumflags2",
"event-listener 5.4.1", "event-listener",
"futures-core", "futures-core",
"futures-lite", "futures-lite",
"hex", "hex",
-18
View File
@@ -10,7 +10,6 @@
# crates/chanora_resolver/ — TeamSpeak address resolution # crates/chanora_resolver/ — TeamSpeak address resolution
# crates/chanora_state/ — snapshot, deltas, reducers # crates/chanora_state/ — snapshot, deltas, reducers
# crates/chanora_audio/ — capture, DSP, Opus, jitter, mixer # crates/chanora_audio/ — capture, DSP, Opus, jitter, mixer
# crates/chanora_cache/ — avatar/icon blob cache (cacache-backed)
# crates/chanora_storage/ — bookmarks, settings, identity refs # crates/chanora_storage/ — bookmarks, settings, identity refs
# crates/chanora_diagnostics/ — logs, redaction, export # crates/chanora_diagnostics/ — logs, redaction, export
# crates/chanora_prefetch — server-resolution prefetch cache/policy # crates/chanora_prefetch — server-resolution prefetch cache/policy
@@ -31,7 +30,6 @@ members = [
"crates/chanora_state", "crates/chanora_state",
"crates/chanora_audio", "crates/chanora_audio",
"crates/chanora_storage", "crates/chanora_storage",
"crates/chanora_cache",
"crates/chanora_diagnostics", "crates/chanora_diagnostics",
"crates/chanora_prefetch", "crates/chanora_prefetch",
"crates/chanora_bridge", "crates/chanora_bridge",
@@ -40,8 +38,6 @@ members = [
exclude = [ exclude = [
"apps/chanora_flutter", "apps/chanora_flutter",
"tools/protocol-probe",
"tools/audio-test",
] ]
[workspace.package] [workspace.package]
@@ -86,17 +82,3 @@ tsproto-types = { git = "https://github.com/EdisonJwa/tsclientlib.git", branch =
[patch.crates-io] [patch.crates-io]
cmake = { git = "https://github.com/pr2502/cmake-rs", rev = "bdad5edc569d82151922c5c6c4685b1563f12aa1" } cmake = { git = "https://github.com/pr2502/cmake-rs", rev = "bdad5edc569d82151922c5c6c4685b1563f12aa1" }
# Apple-only DWARF emission for archive validation lives in the iOS and
# macOS chanora_bridge podspecs (apps/chanora_flutter/{ios,macos}/
# chanora_bridge.podspec) as per-build environment overrides:
#
# CARGO_PROFILE_RELEASE_DEBUG=true
# CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off
# CARGO_PROFILE_RELEASE_STRIP=false
#
# This keeps Android, Linux, and Windows release binaries on the cargo
# default release profile (no DWARF, no extra ~10MB symbol payload).
# Apple builds need the DWARF so dsymutil can emit a usable
# chanora_bridge.framework.dSYM that the archive validator accepts.
-190
View File
@@ -1,190 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024-2026 Chanora Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024-2026 Chanora Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+10 -8
View File
@@ -14,10 +14,10 @@ Flutter UI + Rust Core + tsclientlib
## Status ## Status
Chanora is currently a baseline-candidate Flutter + Rust workspace. It is not production-ready and is not approved for public or store release. Chanora is currently in early planning and baseline-candidate design.
```text ```text
Current documentation baseline: v0.9.x document set Current documentation baseline: v0.9.2
Current status: Baseline Candidate Current status: Baseline Candidate
Implementation status: Not production-ready Implementation status: Not production-ready
``` ```
@@ -25,7 +25,7 @@ Implementation status: Not production-ready
The current engineering focus is: The current engineering focus is:
- defining the system and software architecture; - defining the system and software architecture;
- hardening the Flutter + Rust application structure; - preparing the Flutter + Rust application structure;
- validating TeamSpeak-compatible protocol integration through `tsclientlib`; - validating TeamSpeak-compatible protocol integration through `tsclientlib`;
- defining cross-platform audio behavior; - defining cross-platform audio behavior;
- preparing release, verification, security, privacy, and legal gates. - preparing release, verification, security, privacy, and legal gates.
@@ -49,7 +49,7 @@ Current platform policy:
| iOS / iPadOS runtime target | iOS 16+ while Apple CoreML Silero VAD is linked | | iOS / iPadOS runtime target | iOS 16+ while Apple CoreML Silero VAD is linked |
| macOS runtime target | macOS 13+ while Apple CoreML Silero VAD is linked | | macOS runtime target | macOS 13+ while Apple CoreML Silero VAD is linked |
| App Store Connect upload gate | Xcode 26+ with iOS 26 / iPadOS 26 SDK+ for upload on or after 2026-04-28 | | App Store Connect upload gate | Xcode 26+ with iOS 26 / iPadOS 26 SDK+ for upload on or after 2026-04-28 |
| Android runtime target | Android API 28+ per DEC-004, SysRS-288, SRS-187, and Gradle `minSdk = 28` | | Android runtime target | Android API 24+ unless Flutter, plugin, audio, or product constraints require raising it |
| Google Play target API | Target the Google Play-required API level on upload date | | Google Play target API | Target the Google Play-required API level on upload date |
The App Store / Play Store upload gates are release requirements. They are separate from local development and internal testing requirements. The App Store / Play Store upload gates are release requirements. They are separate from local development and internal testing requirements.
@@ -230,7 +230,7 @@ docs/
aspice-swe2-swe3-integration-note.md aspice-swe2-swe3-integration-note.md
``` ```
Implementation source folders are present in this workspace. The current high-level structure is: Implementation source folders may be added later. A likely structure is:
```text ```text
apps/ apps/
@@ -248,7 +248,7 @@ crates/
chanora_bridge/ chanora_bridge/
``` ```
The exact implementation layout may continue to evolve as maintainability reviews split or merge Modules, but the repository scaffold exists. The exact implementation layout should be finalized when the repository scaffold is created.
--- ---
@@ -395,7 +395,9 @@ docs/governance/git-commit-message-convention.md
## Development ## Development
Common local commands include: Implementation commands will be added after the repository scaffold is finalized.
Expected future commands may include:
```bash ```bash
flutter pub get flutter pub get
@@ -405,7 +407,7 @@ cargo clippy
cargo fmt cargo fmt
``` ```
Android runtime success also requires an available Android NDK toolchain and an authorized device or emulator for build/install/smoke verification. Do not treat these as authoritative until the actual Flutter/Rust workspace has been created.
--- ---
@@ -59,15 +59,12 @@ android {
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlin { kotlinOptions {
compilerOptions { jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
} }
defaultConfig { defaultConfig {
@@ -199,7 +196,6 @@ android {
// armeabi-v7a, x86_64, x86. AGP merges these into the APK/AAB. // armeabi-v7a, x86_64, x86. AGP merges these into the APK/AAB.
dependencies { dependencies {
implementation("com.microsoft.onnxruntime:onnxruntime-android:1.26.0") implementation("com.microsoft.onnxruntime:onnxruntime-android:1.26.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
} }
flutter { flutter {
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3C8.69,3 6,5.69 6,9V13C6,16.31 8.69,19 12,19C15.31,19 18,16.31 18,13V9C18,5.69 15.31,3 12,3ZM12,5C14.21,5 16,6.79 16,9V13C16,15.21 14.21,17 12,17C9.79,17 8,15.21 8,13V9C8,6.79 9.79,5 12,5ZM11,20V22H13V20H11Z" />
</vector>
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/ic_chanora_notification" />
@@ -1,6 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
@@ -19,8 +19,8 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.13.1" apply false id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false id("org.jetbrains.kotlin.android") version "2.2.20" apply false
} }
include(":app") include(":app")
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>ad-hoc</string>
<key>destination</key>
<string>export</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
<key>teamID</key>
<string>ZNVDEVDRX3</string>
<key>provisioningProfiles</key>
<dict>
<key>app.teamspeak.chanora</key>
<string>Chanora_Ad_Hoc</string>
</dict>
</dict>
</plist>
@@ -1,9 +1,2 @@
#include? "Pods/Target Support Files/Pods-Chanora/Pods-Chanora.debug.xcconfig" #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
// Mirror Release.xcconfig (see explanation there). `-u` is the load-bearing
// flag: without it the linker drops Swift @_cdecl symbols (no Swift caller)
// before `-exported_symbol` can re-export them, and the verify_silero_exports
// build phase fails the build.
OTHER_LDFLAGS = $(inherited) -Xlinker -u -Xlinker _chanora_silero_vad_create -Xlinker -u -Xlinker _chanora_silero_vad_destroy -Xlinker -u -Xlinker _chanora_silero_vad_reset -Xlinker -u -Xlinker _chanora_silero_vad_process -Xlinker -u -Xlinker _chanora_silero_vad_last_error -Xlinker -u -Xlinker _chanora_silero_vad_free_string -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string
STRIP_STYLE = non-global
@@ -1,27 +1,2 @@
#include? "Pods/Target Support Files/Pods-Chanora/Pods-Chanora.release.xcconfig" #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
// Force the linker to retain Swift @_cdecl symbols that the chanora_bridge
// Rust framework resolves at runtime via dlsym(RTLD_DEFAULT). Two flags per
// symbol, intentionally redundant:
//
// -u _sym marks the symbol as force-undefined at link time,
// which keeps the object that defines it from being
// dropped and prevents dead-strip from removing the
// definition. This is the load-bearing flag.
// -exported_symbol _sym re-exports the symbol in the final binary's
// dynamic symbol table so dlsym(RTLD_DEFAULT) can
// find it from the Rust framework at runtime.
//
// Without -u, Xcode Archive's -dead_strip (WMO + LTO) can remove the
// symbol before the export list is applied, and CoreML VAD silently falls
// back to WebRTC on TestFlight / App Store. The Swift-side static
// `unsafeBitCast` references in SileroCoreMLBridge.swift are belt-and-
// suspenders defense-in-depth, NOT the primary guarantee.
OTHER_LDFLAGS = $(inherited) -Xlinker -u -Xlinker _chanora_silero_vad_create -Xlinker -u -Xlinker _chanora_silero_vad_destroy -Xlinker -u -Xlinker _chanora_silero_vad_reset -Xlinker -u -Xlinker _chanora_silero_vad_process -Xlinker -u -Xlinker _chanora_silero_vad_last_error -Xlinker -u -Xlinker _chanora_silero_vad_free_string -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string
// `STRIP_STYLE = all` (Xcode default for archive installs) runs `strip` without
// `-x`, which removes even the global @_cdecl symbols the linker exported above
// via -exported_symbol. `non-global` runs `strip -x`, preserving globals so the
// Rust framework's dlsym(RTLD_DEFAULT) can find them. 264-byte cost in the app.
STRIP_STYLE = non-global
+1 -1
View File
@@ -27,7 +27,7 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup flutter_ios_podfile_setup
target 'Chanora' do target 'Runner' do
use_frameworks! use_frameworks!
# Chanora Rust bridge as a vendored framework. The podspec runs # Chanora Rust bridge as a vendored framework. The podspec runs
+39 -2
View File
@@ -1,33 +1,70 @@
PODS: PODS:
- audio_session (0.0.1):
- Flutter
- chanora_bridge (1.0.0) - chanora_bridge (1.0.0)
- connectivity_plus (0.0.1):
- Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_foreground_task (0.0.1): - flutter_foreground_task (0.0.1):
- Flutter - Flutter
- haptic_kit (1.0.0): - haptic_kit (1.0.0):
- Flutter - Flutter
- package_info_plus (0.4.5):
- Flutter
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- chanora_bridge (from `.`) - chanora_bridge (from `.`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`) - flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
- haptic_kit (from `.symlinks/plugins/haptic_kit/ios`) - haptic_kit (from `.symlinks/plugins/haptic_kit/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
chanora_bridge: chanora_bridge:
:path: "." :path: "."
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_foreground_task: flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios" :path: ".symlinks/plugins/flutter_foreground_task/ios"
haptic_kit: haptic_kit:
:path: ".symlinks/plugins/haptic_kit/ios" :path: ".symlinks/plugins/haptic_kit/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
chanora_bridge: 27a03592058709f6f38701343eb51c3a55b02da0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758 haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
PODFILE CHECKSUM: 85b93b53f958f1ff700a147e9da4374c8b1c6970 PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
@@ -8,18 +8,17 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B4754099284EEDCD859A973 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3EF79A791760D95CE0F41CFF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63497078A621E2A73B102C46 /* Pods_RunnerTests.framework */; }; 3EF79A791760D95CE0F41CFF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63497078A621E2A73B102C46 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
8C5000012DD0000000000001 /* SileroCoreMLBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */; }; 8C5000012DD0000000000001 /* SileroCoreMLBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */; };
8C5000042DD0000000000001 /* SileroCoreML in Frameworks */ = {isa = PBXBuildFile; productRef = 8C5000032DD0000000000001 /* SileroCoreML */; }; 8C5000042DD0000000000001 /* SileroCoreML in Frameworks */ = {isa = PBXBuildFile; productRef = 8C5000032DD0000000000001 /* SileroCoreML */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C8BACE02E6EE5F840EE3F174 /* Pods_Chanora.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC4F9695FDCB1E0D04E08974 /* Pods_Chanora.framework */; };
FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; }; FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -48,10 +47,10 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
076D9E9796600FBC91FD7714 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; 076D9E9796600FBC91FD7714 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
0B4754099284EEDCD859A973 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
2EA1142FBFD36E2ED564A5AA /* Pods-Chanora.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chanora.release.xcconfig"; path = "Target Support Files/Pods-Chanora/Pods-Chanora.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
@@ -60,23 +59,19 @@
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; }; 8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SileroCoreMLBridge.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7E043103010958FC2C6CA47F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; 7E043103010958FC2C6CA47F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
89E01DD0E6B92DA93A02E9D6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 89E01DD0E6B92DA93A02E9D6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SileroCoreMLBridge.swift; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Chanora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chanora.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A23C02505CD7E5092CA7958C /* Pods-Chanora.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chanora.profile.xcconfig"; path = "Target Support Files/Pods-Chanora/Pods-Chanora.profile.xcconfig"; sourceTree = "<group>"; };
C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
DC4F9695FDCB1E0D04E08974 /* Pods_Chanora.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chanora.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
EAE6402BFC041304D1D0896D /* Pods-Chanora.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chanora.debug.xcconfig"; path = "Target Support Files/Pods-Chanora/Pods-Chanora.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -84,9 +79,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
8C5000042DD0000000000001 /* SileroCoreML in Frameworks */, 8C5000042DD0000000000001 /* SileroCoreML in Frameworks */,
C8BACE02E6EE5F840EE3F174 /* Pods_Chanora.framework in Frameworks */, 1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -112,8 +106,8 @@
4351F25046559EFA4C03047A /* Frameworks */ = { 4351F25046559EFA4C03047A /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0B4754099284EEDCD859A973 /* Pods_Runner.framework */,
63497078A621E2A73B102C46 /* Pods_RunnerTests.framework */, 63497078A621E2A73B102C46 /* Pods_RunnerTests.framework */,
DC4F9695FDCB1E0D04E08974 /* Pods_Chanora.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -127,9 +121,6 @@
73171B86DD76CC3E5A58E160 /* Pods-RunnerTests.debug.xcconfig */, 73171B86DD76CC3E5A58E160 /* Pods-RunnerTests.debug.xcconfig */,
E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */, E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */,
076D9E9796600FBC91FD7714 /* Pods-RunnerTests.profile.xcconfig */, 076D9E9796600FBC91FD7714 /* Pods-RunnerTests.profile.xcconfig */,
EAE6402BFC041304D1D0896D /* Pods-Chanora.debug.xcconfig */,
2EA1142FBFD36E2ED564A5AA /* Pods-Chanora.release.xcconfig */,
A23C02505CD7E5092CA7958C /* Pods-Chanora.profile.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -137,7 +128,6 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -161,7 +151,7 @@
97C146EF1CF9000F007C117D /* Products */ = { 97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
97C146EE1CF9000F007C117D /* Chanora.app */, 97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
); );
name = Products; name = Products;
@@ -207,15 +197,14 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
97C146ED1CF9000F007C117D /* Chanora */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Chanora" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
7FE733EE83086540AF5D21CB /* [CP] Check Pods Manifest.lock */, 7FE733EE83086540AF5D21CB /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
CA110001000000000000A100 /* Verify Silero Exports */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
@@ -225,13 +214,12 @@
); );
dependencies = ( dependencies = (
); );
name = Chanora; name = Runner;
packageProductDependencies = ( packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
8C5000032DD0000000000001 /* SileroCoreML */, 8C5000032DD0000000000001 /* SileroCoreML */,
); );
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Chanora.app */; productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@@ -264,14 +252,13 @@
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = ( packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */, 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
); );
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
97C146ED1CF9000F007C117D /* Chanora */, 97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */, 331C8080294A63A400263BE5 /* RunnerTests */,
); );
}; };
@@ -344,15 +331,15 @@
files = ( files = (
); );
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Chanora/Pods-Chanora-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Chanora/Pods-Chanora-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Chanora/Pods-Chanora-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
7FE733EE83086540AF5D21CB /* [CP] Check Pods Manifest.lock */ = { 7FE733EE83086540AF5D21CB /* [CP] Check Pods Manifest.lock */ = {
@@ -370,7 +357,7 @@
outputFileListPaths = ( outputFileListPaths = (
); );
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Chanora-checkManifestLockResult.txt", "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
@@ -392,21 +379,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
}; };
CA110001000000000000A100 /* Verify Silero Exports */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Verify Silero Exports";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/../scripts/verify_silero_exports.sh\"\n";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -434,7 +406,7 @@
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = { 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Chanora */; target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
}; };
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
@@ -518,23 +490,18 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = 349G7M4TQQ;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = ZNVDEVDRX3;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Chanora;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = app.teamspeak.chanora; PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Chanora_iOS_Ad Hoc"; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Chanora_iOS_Ad Hoc";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -555,7 +522,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Chanora.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Chanora"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
}; };
name = Debug; name = Debug;
}; };
@@ -571,7 +538,7 @@
PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Chanora.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Chanora"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
}; };
name = Release; name = Release;
}; };
@@ -587,7 +554,7 @@
PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Chanora.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Chanora"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
}; };
name = Profile; name = Profile;
}; };
@@ -709,23 +676,18 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = 349G7M4TQQ;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = ZNVDEVDRX3;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Chanora;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = app.teamspeak.chanora; PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = Chanora_ios_Development; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Chanora_ios_Development;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -740,23 +702,18 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Automatic;
CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CURRENT_PROJECT_VERSION = 101; DEVELOPMENT_TEAM = 349G7M4TQQ;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = ZNVDEVDRX3;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Chanora;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = app.teamspeak.chanora; PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Chanora_App Store"; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Chanora_App Store";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -786,7 +743,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Chanora" */ = { 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
97C147061CF9000F007C117D /* Debug */, 97C147061CF9000F007C117D /* Debug */,
@@ -799,21 +756,13 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */ = { 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = "../../../silero-coreml"; relativePath = ../../../silero-coreml;
}; };
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
8C5000032DD0000000000001 /* SileroCoreML */ = { 8C5000032DD0000000000001 /* SileroCoreML */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */; package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
@@ -1,28 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1510" LastUpgradeVersion = "1510"
version = "1.7"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Chanora.app"
BlueprintName = "Chanora"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -33,8 +15,8 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Chanora.app" BuildableName = "Runner.app"
BlueprintName = "Chanora" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </BuildActionEntry>
@@ -50,8 +32,8 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Chanora.app" BuildableName = "Runner.app"
BlueprintName = "Chanora" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
@@ -86,8 +68,8 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Chanora.app" BuildableName = "Runner.app"
BlueprintName = "Chanora" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
@@ -103,8 +85,8 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Chanora.app" BuildableName = "Runner.app"
BlueprintName = "Chanora" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
+113 -120
View File
@@ -6,54 +6,95 @@ import AVFoundation
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
private var iosAudioLifecycleChannel: FlutterMethodChannel? private var iosAudioLifecycleChannel: FlutterMethodChannel?
private var iosPlatformChannel: FlutterMethodChannel? private var iosPlatformChannel: FlutterMethodChannel?
private var iosAudioSessionChannel: FlutterMethodChannel?
/// Tracks whether a voice channel is currently active.
///
/// The AVAudioSession is intentionally not configured for VoIP at
/// app launch that would interrupt other apps' audio (Spotify,
/// Apple Music, podcasts) the moment the user opens Chanora, even
/// when they're just reading chat. Production VoIP apps (Telegram
/// group calls, Signal, Discord, Element) only switch the session
/// to `.playAndRecord` + `.voiceChat` when the user actually joins
/// a voice channel. See `docs/architecture/sad.md` and the
/// `chanora/ios_audio_session` MethodChannel contract.
///
/// This flag gates lifecycle handlers (interruption-ended,
/// media-services-reset) so we only rebuild the VoIP session if a
/// call is actually in progress. When false, those handlers leave
/// the session in the inactive `.ambient` baseline.
private var voiceSessionActive: Bool = false
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
DispatchQueue.global(qos: .utility).async { // Configure the iOS AVAudioSession **category + mode** at
ChanoraSileroSelfTest.run() // app-launch time, but DEFER setActive(true) until the scene
// is foregrounded. Calling setActive in didFinishLaunching is
// racy on iOS 17+ devices: if the user launches the app from a
// cold state, the UIApplication isn't yet `.active` and
// setActive returns `AVAudioSessionErrorCodeCannotStartPlaying`
// (561017449) the iOS audio policy server refuses to grant
// the audio session because the app is not yet considered the
// foreground priority owner. Symptom in production builds:
// 'AVAudioSession setup failed: Error 561017449 "Session
// activation failed"' in NSLog, after which the audio engine
// is unusable until the user backgrounds + foregrounds the
// app.
//
// The category itself can be set whenever; only the active
// state needs to be deferred. We listen for
// didBecomeActiveNotification and activate then. Most
// production iOS voice apps (Discord, Zoom, FaceTime) follow
// this same shape.
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(
.playAndRecord,
mode: .voiceChat,
// Mode rationale (May 2026, .voiceChat reinstated):
//
// We previously used .default mode after discovering that
// .voiceChat routed output through iOS's in-call audio
// channel, which made speaker output barely audible. That
// bug was caused by cpal's RemoteIO unit binding to a stale
// physical transducer after migrating to coreaudio-rs +
// kAudioUnitSubType_VoiceProcessingIO (see
// crates/chanora_audio/src/ios_voice_unit.rs) the route
// binding is correct under either mode because VPIO re-binds
// on overrideOutputAudioPort.
//
// .voiceChat advantages over .default:
// * Tells iOS this is a VoIP session other apps' audio
// is properly ducked/paused instead of competing.
// * Enables correct Bluetooth HFP negotiation without
// manual workarounds.
// * iOS treats the audio session as a "call" for priority
// purposes (won't be interrupted by notification sounds).
// * System-level CallKit integration (lock-screen controls).
//
// .defaultToSpeaker ensures output goes to the main speaker
// (not the earpiece) by default when no headphones are
// connected, compensating for the in-call channel's tendency
// to route to the earpiece.
//
// References:
// * https://github.com/twilio/video-quickstart-ios/issues/522
// * https://stackoverflow.com/questions/79834998 (Daily.co)
//
// Options:
// .defaultToSpeaker : route output to the main speaker
// (not the earpiece) by default
// when no headphones are connected.
// .allowBluetoothHFP : permit Bluetooth Hands-Free
// Profile headsets as both input
// and output.
// .allowBluetoothA2DP : permit higher-quality A2DP
// output-only Bluetooth devices.
options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
)
// Match VPIO / Opus frame cadence to reduce callback pressure.
try session.setPreferredIOBufferDuration(0.02)
try session.setPreferredSampleRate(48000.0)
logAudioSessionState(context: "setCategory")
} catch {
NSLog("chanora_flutter: AVAudioSession setCategory failed: \(error)")
} }
// AVAudioSession lifecycle policy (DEC-2026-06-08, supersedes // Activate the session once the app is actually foreground. The
// the launch-time .playAndRecord setup): // notification fires immediately after the cold-launch settles,
// // and again on every resume-from-background both safe
// At launch we set the category to .ambient and leave the // moments to call setActive(true). Repeated activation while
// session INACTIVE matching the Telegram / Signal / Discord / // already-active is a no-op per the docs.
// Element / Jitsi pattern and Apple's guidance that "a VoIP NotificationCenter.default.addObserver(
// app's audio session should not be active" while idle. self,
// Configuring .playAndRecord + .voiceChat at launch stops other selector: #selector(activateAudioSession),
// apps' music (Spotify, Apple Music, podcasts) the moment the name: UIApplication.didBecomeActiveNotification,
// user opens Chanora, even when they are just reading text chat. object: nil
// )
// VoIP configuration is engaged on voice-channel join via the
// `chanora/ios_audio_session` MethodChannel, driven from Dart
// before `voiceJoin` starts VoiceProcessingIO and again as an
// idempotent guard on the AudioStarted lifecycle.
do {
try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
logAudioSessionState(context: "launch-ambient")
} catch {
NSLog("chanora_flutter: AVAudioSession .ambient baseline failed: \(error)")
}
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
@@ -79,60 +120,37 @@ import AVFoundation
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
/// Activate the VoIP audio session. Called from Dart via the /// Called by `didBecomeActiveNotification` (cold-launch settle +
/// `chanora/ios_audio_session` channel before a voice channel join /// every resume-from-background). Activates the AVAudioSession.
/// starts VoiceProcessingIO. Configures /// Repeated activation is a no-op when the session is already
/// .playAndRecord + .voiceChat with .mixWithOthers so other apps /// active so this is safe to call on every foreground.
/// (Spotify, podcasts) can keep playing alongside the voice @objc private func activateAudioSession() {
/// channel matching the Telegram group-call UX. Idempotent:
/// repeated calls while already active are a no-op.
private func activateVoiceSession() {
do { do {
let session = AVAudioSession.sharedInstance() try AVAudioSession.sharedInstance().setActive(true, options: [])
try session.setCategory( NSLog("chanora_flutter: AVAudioSession activated on foreground")
.playAndRecord, // Read back the ACTUAL session state. preferredSampleRate /
mode: .voiceChat, // preferredIOBufferDuration are hints; iOS may pick something
options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP, .mixWithOthers] // else depending on hardware + currently-engaged effects.
) // Without these we can't tell whether VPIO is running at
try session.setPreferredIOBufferDuration(0.02) // 48 kHz mono (what our render callback assumes) or at e.g.
try session.setPreferredSampleRate(48000.0) // 44.1 kHz (which would explain the user's broken playback
try session.setActive(true, options: []) // \u2014 our render callback would be writing samples at the
voiceSessionActive = true // wrong rate, causing pitch + timing artifacts).
logAudioSessionState(context: "activateVoiceSession") logAudioSessionState(context: "setActive")
let ins = session.currentRoute.inputs.map { $0.portType.rawValue }.joined(separator: ",") let s = AVAudioSession.sharedInstance()
let ins = s.currentRoute.inputs.map { $0.portType.rawValue }.joined(separator: ",")
NSLog( NSLog(
"chanora_flutter: voice session active: " + "chanora_flutter: AVAudioSession actual: " +
"sampleRate=\(session.sampleRate) " + "sampleRate=\(s.sampleRate) " +
"ioBufferDuration=\(String(format: "%.4f", session.ioBufferDuration)) " + "ioBufferDuration=\(String(format: "%.4f", s.ioBufferDuration)) " +
"inputs=[\(ins)] outputVolume=\(session.outputVolume)" "inputs=[\(ins)] " +
"outputVolume=\(s.outputVolume)"
) )
} catch { } catch {
NSLog("chanora_flutter: activateVoiceSession failed: \(error)") NSLog("chanora_flutter: AVAudioSession setActive failed: \(error)")
} }
} }
/// Deactivate the VoIP audio session and return to the idle
/// .ambient baseline. Called from Dart on `BridgeEvent::AudioStopped`
/// (intentional leave, disconnect, or connection lost).
/// `.notifyOthersOnDeactivation` lets other audio apps know they
/// can resume best-effort: Apple Music / Podcasts resume
/// reliably, Spotify is not guaranteed.
private func deactivateVoiceSession() {
let session = AVAudioSession.sharedInstance()
do {
try session.setActive(false, options: [.notifyOthersOnDeactivation])
} catch {
NSLog("chanora_flutter: deactivateVoiceSession setActive(false) failed: \(error)")
}
do {
try session.setCategory(.ambient, mode: .default)
} catch {
NSLog("chanora_flutter: deactivateVoiceSession setCategory(.ambient) failed: \(error)")
}
voiceSessionActive = false
logAudioSessionState(context: "deactivateVoiceSession")
}
/// Reads back the actual AVAudioSession state and logs it for /// Reads back the actual AVAudioSession state and logs it for
/// SDD-098 compliance. Called after both setCategory and setActive /// SDD-098 compliance. Called after both setCategory and setActive
/// to verify that the session accepted the requested configuration. /// to verify that the session accepted the requested configuration.
@@ -197,30 +215,25 @@ import AVFoundation
} }
@objc private func handleMediaServicesReset(_ notification: Notification) { @objc private func handleMediaServicesReset(_ notification: Notification) {
NSLog("chanora_flutter: media services reset voiceActive=\(voiceSessionActive)") NSLog("chanora_flutter: media services reset")
if voiceSessionActive {
do { do {
let session = AVAudioSession.sharedInstance() let session = AVAudioSession.sharedInstance()
try session.setCategory( try session.setCategory(
.playAndRecord, .playAndRecord,
mode: .voiceChat, mode: .voiceChat,
options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP, .mixWithOthers] options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
) )
try session.setPreferredIOBufferDuration(0.02) try session.setPreferredIOBufferDuration(0.02)
try session.setPreferredSampleRate(48000.0) try session.setPreferredSampleRate(48000.0)
try session.setActive(true, options: []) try session.setActive(true, options: [])
logAudioSessionState(context: "mediaServicesWereReset-voip") logAudioSessionState(context: "mediaServicesWereReset")
} catch { } catch {
NSLog("chanora_flutter: AVAudioSession media-services reset rebuild failed: \(error)") NSLog("chanora_flutter: AVAudioSession media-services reset rebuild failed: \(error)")
} }
} else { // P1: After rebuilding the session, send the current route class to
do { // Rust so it can recompute the processing policy and reset the
try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default) // AudioUnit. The Rust side handles this via ios_handle_media_services_reset
logAudioSessionState(context: "mediaServicesWereReset-ambient") // which calls ios_restart_voice_unit.
} catch {
NSLog("chanora_flutter: AVAudioSession media-services reset ambient restore failed: \(error)")
}
}
let routeClass = classifyAudioRoute(AVAudioSession.sharedInstance().currentRoute) let routeClass = classifyAudioRoute(AVAudioSession.sharedInstance().currentRoute)
NSLog("chanora_flutter: media services reset complete, route=\(routeClass)") NSLog("chanora_flutter: media services reset complete, route=\(routeClass)")
iosAudioLifecycleChannel?.invokeMethod("handleMediaServicesReset", arguments: routeClass) iosAudioLifecycleChannel?.invokeMethod("handleMediaServicesReset", arguments: routeClass)
@@ -252,26 +265,6 @@ import AVFoundation
name: "chanora/ios_platform", name: "chanora/ios_platform",
binaryMessenger: engineBridge.applicationRegistrar.messenger() binaryMessenger: engineBridge.applicationRegistrar.messenger()
) )
iosAudioSessionChannel = FlutterMethodChannel(
name: "chanora/ios_audio_session",
binaryMessenger: engineBridge.applicationRegistrar.messenger()
)
iosAudioSessionChannel?.setMethodCallHandler { [weak self] call, result in
guard let self = self else {
result(FlutterError(code: "delegate_gone", message: "AppDelegate deallocated", details: nil))
return
}
switch call.method {
case "activateVoiceSession":
self.activateVoiceSession()
result(nil)
case "deactivateVoiceSession":
self.deactivateVoiceSession()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
iosPlatformChannel?.setMethodCallHandler { call, result in iosPlatformChannel?.setMethodCallHandler { call, result in
switch call.method { switch call.method {
case "getMicrophonePermissionState": case "getMicrophonePermissionState":
+10 -11
View File
@@ -4,6 +4,9 @@
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<!-- Opt into ProMotion / high-refresh-rate CADisplayLink ranges on
supported iPhones. Flutter's iOS embedder reads this key; no
additional Flutter package is required for dynamic refresh. -->
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -24,18 +27,10 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>Chanora needs local network access to connect to your voice servers.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Chanora needs microphone access so you can talk on your voice server.</string> <string>Chanora needs microphone access so you can talk on your TeamSpeak-compatible voice server.</string>
<key>NSUserNotificationsUsageDescription</key>
<string>Chanora sends you a notification when another user pokes you.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
@@ -63,8 +58,8 @@
<array> <array>
<string>audio</string> <string>audio</string>
</array> </array>
<key>UIFileSharingEnabled</key> <key>NSLocalNetworkUsageDescription</key>
<true/> <string>Chanora needs local network access to connect to TeamSpeak-compatible voice servers.</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -82,5 +77,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict> </dict>
</plist> </plist>
@@ -1,5 +1,4 @@
import CoreML import CoreML
import Darwin
import Foundation import Foundation
import SileroCoreML import SileroCoreML
@@ -97,136 +96,3 @@ public func chanoraSileroVadFreeString(_ string: UnsafeMutablePointer<CChar>?) {
guard let string else { return } guard let string else { return }
free(string) free(string)
} }
@objc public final class ChanoraSileroSelfTest: NSObject {
// Validates the same code path the Rust framework uses: dlsym(RTLD_DEFAULT) for all
// six @_cdecl symbols, then exercises create -> reset -> process -> destroy. Catches
// the dead-strip / linker-export class of bug that broke TestFlight; calling the Swift
// functions directly would mask it because direct calls bypass the dynamic symbol table.
@objc public static func run() {
let started = DispatchTime.now()
// Static linker references: keep the Swift compiler / linker from
// dead-stripping the @_cdecl symbols under Whole-Module-Optimization
// + LTO in Archive builds. dlsym(RTLD_DEFAULT) below does NOT count
// as a static reference for the dead-stripper these `_ = ` lines
// do. Without them, TestFlight builds shipped without the symbols
// even though Debug builds (no LTO) worked.
//
// The `withoutActuallyEscaping` dance prevents the optimizer from
// proving the references are unused: assigning the function value
// to a `@convention(c)` typealias forces address-taken semantics.
_ = unsafeBitCast(
chanoraSileroVadCreate as @convention(c) () -> UnsafeMutableRawPointer?,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadDestroy as @convention(c) (UnsafeMutableRawPointer?) -> Void,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadReset as @convention(c) (UnsafeMutableRawPointer?) -> Int32,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadProcess
as @convention(c) (
UnsafeMutableRawPointer?, UnsafePointer<Float>?, Int,
UnsafeMutablePointer<Float>?
) -> Int32,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadLastError as @convention(c) () -> UnsafeMutablePointer<CChar>?,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadFreeString as @convention(c) (UnsafeMutablePointer<CChar>?) -> Void,
to: UnsafeRawPointer.self,
)
typealias CreateFn = @convention(c) () -> UnsafeMutableRawPointer?
typealias DestroyFn = @convention(c) (UnsafeMutableRawPointer?) -> Void
typealias ResetFn = @convention(c) (UnsafeMutableRawPointer?) -> Int32
typealias ProcessFn = @convention(c) (
UnsafeMutableRawPointer?, UnsafePointer<Float>?, Int, UnsafeMutablePointer<Float>?
) -> Int32
typealias LastErrorFn = @convention(c) () -> UnsafeMutablePointer<CChar>?
typealias FreeStringFn = @convention(c) (UnsafeMutablePointer<CChar>?) -> Void
func resolve<T>(_ name: String, as type: T.Type) -> T? {
guard let raw = dlsym(UnsafeMutableRawPointer(bitPattern: -2), name) else {
return nil
}
return unsafeBitCast(raw, to: type)
}
let names = [
"chanora_silero_vad_create",
"chanora_silero_vad_destroy",
"chanora_silero_vad_reset",
"chanora_silero_vad_process",
"chanora_silero_vad_last_error",
"chanora_silero_vad_free_string",
]
let missing = names.filter { dlsym(UnsafeMutableRawPointer(bitPattern: -2), $0) == nil }
if !missing.isEmpty {
NSLog("chanora_flutter: SileroCoreML self-test FAILED dlsym missing=\(missing.joined(separator: ","))")
return
}
guard
let create = resolve("chanora_silero_vad_create", as: CreateFn.self),
let destroy = resolve("chanora_silero_vad_destroy", as: DestroyFn.self),
let reset = resolve("chanora_silero_vad_reset", as: ResetFn.self),
let process = resolve("chanora_silero_vad_process", as: ProcessFn.self),
let lastError = resolve("chanora_silero_vad_last_error", as: LastErrorFn.self),
let freeString = resolve("chanora_silero_vad_free_string", as: FreeStringFn.self)
else {
NSLog("chanora_flutter: SileroCoreML self-test FAILED unsafeBitCast resolution")
return
}
func readError() -> String {
guard let ptr = lastError() else { return "unknown" }
let msg = String(cString: ptr)
freeString(ptr)
return msg
}
guard let handle = create() else {
let elapsedMs = elapsedMs(since: started)
NSLog("chanora_flutter: SileroCoreML self-test FAILED at create err=\(readError()) elapsed_ms=\(elapsedMs)")
return
}
let resetRc = reset(handle)
if resetRc != 0 {
destroy(handle)
let elapsedMs = elapsedMs(since: started)
NSLog("chanora_flutter: SileroCoreML self-test FAILED at reset rc=\(resetRc) err=\(readError()) elapsed_ms=\(elapsedMs)")
return
}
let chunkSize = SileroVADRunner.chunkSize
var probability: Float = 0
let samples = [Float](repeating: 0, count: chunkSize)
let processRc = samples.withUnsafeBufferPointer { buf -> Int32 in
process(handle, buf.baseAddress, chunkSize, &probability)
}
destroy(handle)
let elapsedMs = elapsedMs(since: started)
if processRc == 0 {
NSLog("chanora_flutter: SileroCoreML self-test OK probability=\(probability) elapsed_ms=\(elapsedMs)")
} else {
NSLog("chanora_flutter: SileroCoreML self-test FAILED at process rc=\(processRc) err=\(readError()) elapsed_ms=\(elapsedMs)")
}
}
private static func elapsedMs(since start: DispatchTime) -> String {
let ns = DispatchTime.now().uptimeNanoseconds &- start.uptimeNanoseconds
return String(format: "%.1f", Double(ns) / 1_000_000.0)
}
}
@@ -85,13 +85,6 @@ Pod::Spec.new do |s|
} }
CARGO_BIN="$(find_cargo)" CARGO_BIN="$(find_cargo)"
RUSTC_BIN="$(find_rustc)" RUSTC_BIN="$(find_rustc)"
# Prepend Homebrew's bin dir to PATH so `cmake` (used by
# audiopus_sys's libopus source build) is found. Xcode's
# script_phase PATH sanitisation strips /opt/homebrew/bin,
# which on Apple Silicon hosts is where Homebrew tools live.
export PATH="/opt/homebrew/bin:$PATH"
echo "[chanora_bridge.podspec] cargo build aarch64-apple-ios" echo "[chanora_bridge.podspec] cargo build aarch64-apple-ios"
cd "$REPO_ROOT" cd "$REPO_ROOT"
HOME="$USER_HOME" \\ HOME="$USER_HOME" \\
@@ -102,9 +95,6 @@ Pod::Spec.new do |s|
IPHONEOS_DEPLOYMENT_TARGET=16.0 \\ IPHONEOS_DEPLOYMENT_TARGET=16.0 \\
CMAKE_POLICY_VERSION_MINIMUM=3.5 \\ CMAKE_POLICY_VERSION_MINIMUM=3.5 \\
CMAKE_OSX_DEPLOYMENT_TARGET=16.0 \\ CMAKE_OSX_DEPLOYMENT_TARGET=16.0 \\
CARGO_PROFILE_RELEASE_DEBUG=true \\
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \\
CARGO_PROFILE_RELEASE_STRIP=false \\
"$CARGO_BIN" build --release --target aarch64-apple-ios -p chanora_bridge "$CARGO_BIN" build --release --target aarch64-apple-ios -p chanora_bridge
if [ ! -f "$BRIDGE" ]; then if [ ! -f "$BRIDGE" ]; then
@@ -139,21 +129,7 @@ PLIST
install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\ install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\
"$FW/chanora_bridge" "$FW/chanora_bridge"
echo "[chanora_bridge.podspec] framework ready at $FW"
# Generate the framework's dSYM bundle. Apple's archive validator
# rejects uploads when an embedded framework has no matching dSYM
# (UUID lookup miss in the archive's dSYMs/ folder), which is the
# failure mode that produced this prepare_command in the first
# place. dsymutil reads the DWARF that cargo emitted (enabled by
# [profile.release] debug = true at the workspace root) and writes
# chanora_bridge.framework.dSYM next to the framework. We then
# strip the in-framework binary so the shipped app stays slim —
# the symbols live exclusively in the dSYM bundle, which is the
# layout xcodebuild -exportArchive and App Store Connect expect.
rm -rf "$FW.dSYM"
xcrun dsymutil "$FW/chanora_bridge" -o "$FW.dSYM"
xcrun strip -S -x "$FW/chanora_bridge"
echo "[chanora_bridge.podspec] framework + dSYM ready at $FW"
SCRIPT SCRIPT
# Pod CocoaPods picks this up; the framework gets embedded into # Pod CocoaPods picks this up; the framework gets embedded into
@@ -209,13 +185,6 @@ PLIST
} }
CARGO_BIN="$(find_cargo)" CARGO_BIN="$(find_cargo)"
RUSTC_BIN="$(find_rustc)" RUSTC_BIN="$(find_rustc)"
# Prepend Homebrew's bin dir to PATH so `cmake` (used by
# audiopus_sys's libopus source build) is found. Xcode's
# script_phase PATH sanitisation strips /opt/homebrew/bin,
# which on Apple Silicon hosts is where Homebrew tools live.
export PATH="/opt/homebrew/bin:$PATH"
if [ "${PLATFORM_NAME:-iphoneos}" = "iphonesimulator" ]; then if [ "${PLATFORM_NAME:-iphoneos}" = "iphonesimulator" ]; then
RUST_TARGET="aarch64-apple-ios-sim" RUST_TARGET="aarch64-apple-ios-sim"
SUPPORTED_PLATFORM="iPhoneSimulator" SUPPORTED_PLATFORM="iPhoneSimulator"
@@ -234,9 +203,6 @@ PLIST
IPHONEOS_DEPLOYMENT_TARGET=16.0 \\ IPHONEOS_DEPLOYMENT_TARGET=16.0 \\
CMAKE_POLICY_VERSION_MINIMUM=3.5 \\ CMAKE_POLICY_VERSION_MINIMUM=3.5 \\
CMAKE_OSX_DEPLOYMENT_TARGET=16.0 \\ CMAKE_OSX_DEPLOYMENT_TARGET=16.0 \\
CARGO_PROFILE_RELEASE_DEBUG=true \\
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \\
CARGO_PROFILE_RELEASE_STRIP=false \\
"$CARGO_BIN" build --release --target "$RUST_TARGET" -p chanora_bridge "$CARGO_BIN" build --release --target "$RUST_TARGET" -p chanora_bridge
cd "$REPO_ROOT/apps/chanora_flutter/ios" cd "$REPO_ROOT/apps/chanora_flutter/ios"
@@ -244,16 +210,12 @@ PLIST
# Skip the wrap step if the framework's binary is already # Skip the wrap step if the framework's binary is already
# up-to-date with the cargo output (fast no-op on incremental # up-to-date with the cargo output (fast no-op on incremental
# builds where Rust didn't change). We still publish the dSYM # builds where Rust didn't change).
# into DWARF_DSYM_FOLDER_PATH below so archive builds always
# have the symbols, even when the framework itself is cached.
FW_UP_TO_DATE=0
if [ -f "$FW/chanora_bridge" ] && [ "$FW/chanora_bridge" -nt "$BRIDGE" ]; then if [ -f "$FW/chanora_bridge" ] && [ "$FW/chanora_bridge" -nt "$BRIDGE" ]; then
echo "[chanora_bridge script_phase] framework already up-to-date" echo "[chanora_bridge script_phase] framework already up-to-date"
FW_UP_TO_DATE=1 exit 0
fi fi
if [ "$FW_UP_TO_DATE" = 0 ]; then
mkdir -p "$FW" mkdir -p "$FW"
cp "$BRIDGE" "$FW/chanora_bridge" cp "$BRIDGE" "$FW/chanora_bridge"
cat > "$FW/Info.plist" <<PLIST cat > "$FW/Info.plist" <<PLIST
@@ -274,26 +236,7 @@ PLIST
PLIST PLIST
install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\ install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\
"$FW/chanora_bridge" "$FW/chanora_bridge"
rm -rf "$FW.dSYM" echo "[chanora_bridge script_phase] framework refreshed"
xcrun dsymutil "$FW/chanora_bridge" -o "$FW.dSYM"
xcrun strip -S -x "$FW/chanora_bridge"
echo "[chanora_bridge script_phase] framework refreshed (with dSYM)"
fi
# Publish the dSYM into Xcode's archive dSYM folder on every
# build (cached or not). Without this the archive validator
# fails with "archive did not include a dSYM for the
# chanora_bridge.framework with the UUIDs [<uuid>]" and the
# IPA cannot be uploaded to App Store Connect / TestFlight.
# ${DWARF_DSYM_FOLDER_PATH} resolves to <ARCHIVE>/dSYMs for
# archive builds and <BUILT_PRODUCTS_DIR> otherwise; both paths
# are the ones xcodebuild scans when collecting symbols.
if [ -n "${DWARF_DSYM_FOLDER_PATH:-}" ] && [ -d "$FW.dSYM" ]; then
mkdir -p "$DWARF_DSYM_FOLDER_PATH"
rm -rf "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM"
cp -R "$FW.dSYM" "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM"
echo "[chanora_bridge script_phase] dSYM published to $DWARF_DSYM_FOLDER_PATH"
fi
SCRIPT SCRIPT
:execution_position => :before_compile, :execution_position => :before_compile,
} }
@@ -1,69 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Canonical layout breakpoints for Chanora.
//
// Aligned with Material 3 adaptive layout guidance:
// compact < 600dp — phone, narrow tablet
// medium 6001023 — tablet portrait, small desktop window
// expanded ≥ 1024dp — desktop, tablet landscape
//
// 1024dp was chosen as the expanded threshold based on production app
// research: Discord (member list at 1024px), Mattermost (RHS docked at
// ≥ 1024px), and Rocket.Chat (contextual bar persistent at lg/1024px).
/// Canonical breakpoint thresholds in logical pixels.
///
/// Use these instead of hardcoded pixel values in layout decisions.
/// Migrate existing `_wideBreakpoint` / `_chatMobileBreakpoint` references
/// to these named constants.
class ChanoraBreakpoints {
ChanoraBreakpoints._();
/// Width at which the layout switches from compact to medium.
/// Below this: single-column mobile layout.
/// At/above: two-panel side-by-side layout.
static const double medium = 600;
/// Width at which the layout switches from medium to expanded.
/// Below this: chat opens as a pushed route.
/// At/above: three-panel layout with inline chat panel.
static const double expanded = 1024;
// Panel sizing constants.
/// Fixed width of the left voice/control panel.
static const double voicePanelWidth = 320;
/// Fixed width of the right chat panel (expanded layout only).
static const double chatPanelWidth = 380;
/// Horizontal gap between panels.
static const double panelGap = 12;
/// Desktop snackbar width cap (used when width ≥ [medium]).
static const double snackBarDesktopCap = 560;
/// Connect form action buttons switch from row to column below this width.
static const double connectActionsStackMaxWidth = 400;
/// Modal bottom sheet max height as fraction of screen height.
static const double modalSheetHeightFraction = 0.72;
}
/// Semantic layout class derived from viewport width.
enum LayoutClass {
/// < 600dp — single-column mobile layout.
compact,
/// 6001023dp — two-panel side-by-side layout.
medium,
/// ≥ 1024dp — three-panel layout with inline chat.
expanded,
}
/// Computes the current [LayoutClass] from viewport [width].
LayoutClass layoutClassFromWidth(double width) {
if (width >= ChanoraBreakpoints.expanded) return LayoutClass.expanded;
if (width >= ChanoraBreakpoints.medium) return LayoutClass.medium;
return LayoutClass.compact;
}
@@ -1,67 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Viewport info inherited widget for Chanora.
//
// Computes [LayoutClass] once per frame from the current [MediaQuery] size
// and provides it to the entire widget subtree. Downstream widgets read
// `ViewportInfo.of(context)` instead of calling `LayoutBuilder` or
// `MediaQuery.sizeOf` directly for layout-class decisions.
import 'package:flutter/widgets.dart';
import 'breakpoints.dart';
/// Inherited widget that exposes the current layout class and viewport
/// dimensions to the entire subtree.
///
/// Insert this once near the top of the widget tree (inside the Scaffold
/// body or equivalent). All descendants can then read
/// `ViewportInfo.of(context)` to determine their layout behaviour.
class ViewportInfo extends InheritedWidget {
/// Creates a [ViewportInfo].
const ViewportInfo({
super.key,
required this.layoutClass,
required this.width,
required this.height,
required super.child,
});
/// Current layout class derived from viewport width.
final LayoutClass layoutClass;
/// Current viewport width in logical pixels.
final double width;
/// Current viewport height in logical pixels.
final double height;
/// Returns the nearest [ViewportInfo] in the widget tree.
///
/// Asserts that a [ViewportInfo] ancestor exists.
static ViewportInfo of(BuildContext context) {
final info = context.dependOnInheritedWidgetOfExactType<ViewportInfo>();
assert(info != null, 'No ViewportInfo found in widget tree');
return info!;
}
/// Whether the current layout is compact (< 600dp).
bool get isCompact => layoutClass == LayoutClass.compact;
/// Whether the current layout is medium (6001023dp).
bool get isMedium => layoutClass == LayoutClass.medium;
/// Whether the current layout is expanded (≥ 1024dp).
bool get isExpanded => layoutClass == LayoutClass.expanded;
/// Whether the layout has room for at least two panels (medium or expanded).
bool get isWide => !isCompact;
@override
bool updateShouldNotify(ViewportInfo old) => layoutClass != old.layoutClass;
// NOTE: width/height changes within the same layout class do NOT trigger
// notification. Dependents who genuinely need pixel-level dimensions
// (rare — most layouts should switch on layoutClass) must use a local
// LayoutBuilder. Notifying on every pixel would rebuild every dependent
// on every resize frame, which is the exact pessimisation this
// InheritedWidget exists to avoid.
}
+9 -22
View File
@@ -46,7 +46,6 @@
"retryAction": "Retry", "retryAction": "Retry",
"chatAction": "Chat", "chatAction": "Chat",
"chatCloseAction": "Close chat", "chatCloseAction": "Close chat",
"chatPanelCollapsedHint": "Tap the chat button to continue your conversation",
"chatNewPrivateAction": "New private chat", "chatNewPrivateAction": "New private chat",
"chatSearchClientsHint": "Search clients...", "chatSearchClientsHint": "Search clients...",
"chatDirectMessageAction": "Private message", "chatDirectMessageAction": "Private message",
@@ -242,30 +241,19 @@
"clientInfoUnknown": "Unknown", "clientInfoUnknown": "Unknown",
"clientInfoHidden": "Hidden", "clientInfoHidden": "Hidden",
"clientInfoNone": "None", "clientInfoNone": "None",
"pokeSettingsAction": "Poke notifications", "pokeSnackBarClearAction": "Clear",
"pokeSettingsTitle": "Poke notifications", "pokeSnackBarMoreIndicator": "...",
"pokeSettingsEnableLabel": "Notify me about pokes", "pokeSnackBarIncomingNoMessage": "{sender} pokes you",
"pokeSettingsEnableDescription": "Show local notifications for incoming pokes when this is on.", "@pokeSnackBarIncomingNoMessage": {
"pokeSettingsMutedSendersHeader": "Muted senders",
"pokeSettingsMutedSendersEmpty": "No muted poke senders.",
"pokeSettingsMutedSenderLabel": "Client ID {senderId}",
"@pokeSettingsMutedSenderLabel": {
"placeholders": {
"senderId": { "type": "String" }
}
},
"pokeSettingsUnmuteSenderAction": "Unmute",
"pokeOverflowMutePrompt": "Repeated pokes from {sender} were suppressed. Mute this sender?",
"@pokeOverflowMutePrompt": {
"placeholders": { "placeholders": {
"sender": { "type": "String" } "sender": { "type": "String" }
} }
}, },
"pokeOverflowMuteAction": "Mute", "pokeSnackBarIncomingWithMessage": "{sender} pokes you: {message}",
"pokeMutedSenderConfirmation": "Muted pokes from {sender}", "@pokeSnackBarIncomingWithMessage": {
"@pokeMutedSenderConfirmation": {
"placeholders": { "placeholders": {
"sender": { "type": "String" } "sender": { "type": "String" },
"message": { "type": "String" }
} }
}, },
"pokeHistorySelfNoMessage": "<{time}> You poked \"{target}\".", "pokeHistorySelfNoMessage": "<{time}> You poked \"{target}\".",
@@ -310,6 +298,5 @@
"clientVolumeMuteAction": "Mute user", "clientVolumeMuteAction": "Mute user",
"clientVolumeUnmuteAction": "Unmute user", "clientVolumeUnmuteAction": "Unmute user",
"clientVolumeResetAction": "Reset to default", "clientVolumeResetAction": "Reset to default",
"permissionDenied": "Permission Denied", "permissionDenied": "Permission Denied"
"voiceTalkPowerBlocked": "Insufficient talk power to speak in this channel"
} }
+9 -22
View File
@@ -39,7 +39,6 @@
"retryAction": "重试", "retryAction": "重试",
"chatAction": "聊天", "chatAction": "聊天",
"chatCloseAction": "关闭聊天", "chatCloseAction": "关闭聊天",
"chatPanelCollapsedHint": "点击聊天按钮以继续对话",
"chatNewPrivateAction": "新建私聊", "chatNewPrivateAction": "新建私聊",
"chatSearchClientsHint": "搜索用户...", "chatSearchClientsHint": "搜索用户...",
"chatDirectMessageAction": "私聊", "chatDirectMessageAction": "私聊",
@@ -191,30 +190,19 @@
"clientInfoUnknown": "未知", "clientInfoUnknown": "未知",
"clientInfoHidden": "隐藏", "clientInfoHidden": "隐藏",
"clientInfoNone": "无", "clientInfoNone": "无",
"pokeSettingsAction": "戳一戳通知", "pokeSnackBarClearAction": "清除",
"pokeSettingsTitle": "戳一戳通知", "pokeSnackBarMoreIndicator": "...",
"pokeSettingsEnableLabel": "接收戳一戳通知", "pokeSnackBarIncomingNoMessage": "{sender} 戳了你一下",
"pokeSettingsEnableDescription": "开启后,收到戳一戳时会显示本地通知。", "@pokeSnackBarIncomingNoMessage": {
"pokeSettingsMutedSendersHeader": "已静音的发送者",
"pokeSettingsMutedSendersEmpty": "没有已静音的戳一戳发送者。",
"pokeSettingsMutedSenderLabel": "用户 ID {senderId}",
"@pokeSettingsMutedSenderLabel": {
"placeholders": {
"senderId": { "type": "String" }
}
},
"pokeSettingsUnmuteSenderAction": "取消静音",
"pokeOverflowMutePrompt": "来自 {sender} 的重复戳一戳已被抑制。要静音此发送者吗?",
"@pokeOverflowMutePrompt": {
"placeholders": { "placeholders": {
"sender": { "type": "String" } "sender": { "type": "String" }
} }
}, },
"pokeOverflowMuteAction": "静音", "pokeSnackBarIncomingWithMessage": "{sender} 戳了你一下:{message}",
"pokeMutedSenderConfirmation": "已静音来自 {sender} 的戳一戳", "@pokeSnackBarIncomingWithMessage": {
"@pokeMutedSenderConfirmation": {
"placeholders": { "placeholders": {
"sender": { "type": "String" } "sender": { "type": "String" },
"message": { "type": "String" }
} }
}, },
"pokeHistorySelfNoMessage": "<{time}> 你戳了“{target}”一下。", "pokeHistorySelfNoMessage": "<{time}> 你戳了“{target}”一下。",
@@ -253,6 +241,5 @@
"clientVolumeMuteAction": "静音该用户", "clientVolumeMuteAction": "静音该用户",
"clientVolumeUnmuteAction": "取消静音", "clientVolumeUnmuteAction": "取消静音",
"clientVolumeResetAction": "恢复默认", "clientVolumeResetAction": "恢复默认",
"permissionDenied": "权限被拒绝", "permissionDenied": "权限被拒绝"
"voiceTalkPowerBlocked": "发言权限不足,无法在此频道发言"
} }
@@ -307,12 +307,6 @@ abstract class AppL10n {
/// **'Close chat'** /// **'Close chat'**
String get chatCloseAction; String get chatCloseAction;
/// No description provided for @chatPanelCollapsedHint.
///
/// In en, this message translates to:
/// **'Tap the chat button to continue your conversation'**
String get chatPanelCollapsedHint;
/// No description provided for @chatNewPrivateAction. /// No description provided for @chatNewPrivateAction.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1159,71 +1153,29 @@ abstract class AppL10n {
/// **'None'** /// **'None'**
String get clientInfoNone; String get clientInfoNone;
/// No description provided for @pokeSettingsAction. /// No description provided for @pokeSnackBarClearAction.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Poke notifications'** /// **'Clear'**
String get pokeSettingsAction; String get pokeSnackBarClearAction;
/// No description provided for @pokeSettingsTitle. /// No description provided for @pokeSnackBarMoreIndicator.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Poke notifications'** /// **'...'**
String get pokeSettingsTitle; String get pokeSnackBarMoreIndicator;
/// No description provided for @pokeSettingsEnableLabel. /// No description provided for @pokeSnackBarIncomingNoMessage.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Notify me about pokes'** /// **'{sender} pokes you'**
String get pokeSettingsEnableLabel; String pokeSnackBarIncomingNoMessage(String sender);
/// No description provided for @pokeSettingsEnableDescription. /// No description provided for @pokeSnackBarIncomingWithMessage.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Show local notifications for incoming pokes when this is on.'** /// **'{sender} pokes you: {message}'**
String get pokeSettingsEnableDescription; String pokeSnackBarIncomingWithMessage(String sender, String message);
/// No description provided for @pokeSettingsMutedSendersHeader.
///
/// In en, this message translates to:
/// **'Muted senders'**
String get pokeSettingsMutedSendersHeader;
/// No description provided for @pokeSettingsMutedSendersEmpty.
///
/// In en, this message translates to:
/// **'No muted poke senders.'**
String get pokeSettingsMutedSendersEmpty;
/// No description provided for @pokeSettingsMutedSenderLabel.
///
/// In en, this message translates to:
/// **'Client ID {senderId}'**
String pokeSettingsMutedSenderLabel(String senderId);
/// No description provided for @pokeSettingsUnmuteSenderAction.
///
/// In en, this message translates to:
/// **'Unmute'**
String get pokeSettingsUnmuteSenderAction;
/// No description provided for @pokeOverflowMutePrompt.
///
/// In en, this message translates to:
/// **'Repeated pokes from {sender} were suppressed. Mute this sender?'**
String pokeOverflowMutePrompt(String sender);
/// No description provided for @pokeOverflowMuteAction.
///
/// In en, this message translates to:
/// **'Mute'**
String get pokeOverflowMuteAction;
/// No description provided for @pokeMutedSenderConfirmation.
///
/// In en, this message translates to:
/// **'Muted pokes from {sender}'**
String pokeMutedSenderConfirmation(String sender);
/// No description provided for @pokeHistorySelfNoMessage. /// No description provided for @pokeHistorySelfNoMessage.
/// ///
@@ -1294,12 +1246,6 @@ abstract class AppL10n {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Permission Denied'** /// **'Permission Denied'**
String get permissionDenied; String get permissionDenied;
/// No description provided for @voiceTalkPowerBlocked.
///
/// In en, this message translates to:
/// **'Insufficient talk power to speak in this channel'**
String get voiceTalkPowerBlocked;
} }
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> { class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
@@ -123,10 +123,6 @@ class AppL10nEn extends AppL10n {
@override @override
String get chatCloseAction => 'Close chat'; String get chatCloseAction => 'Close chat';
@override
String get chatPanelCollapsedHint =>
'Tap the chat button to continue your conversation';
@override @override
String get chatNewPrivateAction => 'New private chat'; String get chatNewPrivateAction => 'New private chat';
@@ -590,43 +586,19 @@ class AppL10nEn extends AppL10n {
String get clientInfoNone => 'None'; String get clientInfoNone => 'None';
@override @override
String get pokeSettingsAction => 'Poke notifications'; String get pokeSnackBarClearAction => 'Clear';
@override @override
String get pokeSettingsTitle => 'Poke notifications'; String get pokeSnackBarMoreIndicator => '...';
@override @override
String get pokeSettingsEnableLabel => 'Notify me about pokes'; String pokeSnackBarIncomingNoMessage(String sender) {
return '$sender pokes you';
@override
String get pokeSettingsEnableDescription =>
'Show local notifications for incoming pokes when this is on.';
@override
String get pokeSettingsMutedSendersHeader => 'Muted senders';
@override
String get pokeSettingsMutedSendersEmpty => 'No muted poke senders.';
@override
String pokeSettingsMutedSenderLabel(String senderId) {
return 'Client ID $senderId';
} }
@override @override
String get pokeSettingsUnmuteSenderAction => 'Unmute'; String pokeSnackBarIncomingWithMessage(String sender, String message) {
return '$sender pokes you: $message';
@override
String pokeOverflowMutePrompt(String sender) {
return 'Repeated pokes from $sender were suppressed. Mute this sender?';
}
@override
String get pokeOverflowMuteAction => 'Mute';
@override
String pokeMutedSenderConfirmation(String sender) {
return 'Muted pokes from $sender';
} }
@override @override
@@ -681,8 +653,4 @@ class AppL10nEn extends AppL10n {
@override @override
String get permissionDenied => 'Permission Denied'; String get permissionDenied => 'Permission Denied';
@override
String get voiceTalkPowerBlocked =>
'Insufficient talk power to speak in this channel';
} }
@@ -120,9 +120,6 @@ class AppL10nZh extends AppL10n {
@override @override
String get chatCloseAction => '关闭聊天'; String get chatCloseAction => '关闭聊天';
@override
String get chatPanelCollapsedHint => '点击聊天按钮以继续对话';
@override @override
String get chatNewPrivateAction => '新建私聊'; String get chatNewPrivateAction => '新建私聊';
@@ -577,42 +574,19 @@ class AppL10nZh extends AppL10n {
String get clientInfoNone => ''; String get clientInfoNone => '';
@override @override
String get pokeSettingsAction => '戳一戳通知'; String get pokeSnackBarClearAction => '清除';
@override @override
String get pokeSettingsTitle => '戳一戳通知'; String get pokeSnackBarMoreIndicator => '...';
@override @override
String get pokeSettingsEnableLabel => '接收戳一戳通知'; String pokeSnackBarIncomingNoMessage(String sender) {
return '$sender 戳了你一下';
@override
String get pokeSettingsEnableDescription => '开启后,收到戳一戳时会显示本地通知。';
@override
String get pokeSettingsMutedSendersHeader => '已静音的发送者';
@override
String get pokeSettingsMutedSendersEmpty => '没有已静音的戳一戳发送者。';
@override
String pokeSettingsMutedSenderLabel(String senderId) {
return '用户 ID $senderId';
} }
@override @override
String get pokeSettingsUnmuteSenderAction => '取消静音'; String pokeSnackBarIncomingWithMessage(String sender, String message) {
return '$sender 戳了你一下:$message';
@override
String pokeOverflowMutePrompt(String sender) {
return '来自 $sender 的重复戳一戳已被抑制。要静音此发送者吗?';
}
@override
String get pokeOverflowMuteAction => '静音';
@override
String pokeMutedSenderConfirmation(String sender) {
return '已静音来自 $sender 的戳一戳';
} }
@override @override
@@ -667,7 +641,4 @@ class AppL10nZh extends AppL10n {
@override @override
String get permissionDenied => '权限被拒绝'; String get permissionDenied => '权限被拒绝';
@override
String get voiceTalkPowerBlocked => '发言权限不足,无法在此频道发言';
} }
File diff suppressed because it is too large Load Diff
@@ -1,97 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import '../l10n/generated/app_localizations.dart';
class LiveDiagnosticsDialog extends StatefulWidget {
const LiveDiagnosticsDialog({super.key, required this.diagnosticsTextBuilder});
final String Function() diagnosticsTextBuilder;
@override
State<LiveDiagnosticsDialog> createState() => _LiveDiagnosticsDialogState();
}
class _LiveDiagnosticsDialogState extends State<LiveDiagnosticsDialog> {
static const _refreshInterval = Duration(seconds: 1);
Timer? _refreshTimer;
String _text = '';
@override
void initState() {
super.initState();
_refresh();
_refreshTimer = Timer.periodic(_refreshInterval, (_) => _refresh());
}
@override
void dispose() {
_refreshTimer?.cancel();
super.dispose();
}
void _refresh() {
final next = widget.diagnosticsTextBuilder();
if (!mounted || next == _text) return;
setState(() => _text = next);
}
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final size = MediaQuery.sizeOf(context);
return AlertDialog(
title: Row(
children: [
Expanded(child: Text(l10n.diagnosticsAction)),
const SizedBox(width: 12),
Tooltip(
message: l10n.diagnosticsLiveUpdating,
child: Icon(
Icons.sync,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 720,
maxHeight: size.height * 0.65,
),
child: SingleChildScrollView(
child: SelectableText(
_text,
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
),
),
),
actions: [
TextButton(
onPressed: () async {
await SharePlus.instance.share(ShareParams(text: _text));
},
child: Text(l10n.shareAction),
),
TextButton(
onPressed: () async {
await Clipboard.setData(ClipboardData(text: _text));
if (!context.mounted) return;
Navigator.of(context).pop();
},
child: Text(l10n.copyAction),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.closeAction),
),
],
);
}
}
File diff suppressed because it is too large Load Diff
@@ -15,22 +15,15 @@ typedef StorageDirectoryProvider = Future<Directory> Function();
typedef StorageInitializer = Future<void> Function(String dir); typedef StorageInitializer = Future<void> Function(String dir);
Future<void>? _storageInitFuture; Future<void>? _storageInitFuture;
Future<void>? _cacheInitFuture;
Future<void>? _vadBootstrapFuture; Future<void>? _vadBootstrapFuture;
StorageDirectoryProvider _storageDirectoryProvider = StorageDirectoryProvider _storageDirectoryProvider =
getApplicationSupportDirectory; getApplicationSupportDirectory;
StorageDirectoryProvider _cacheDirectoryProvider = getApplicationCacheDirectory;
StorageInitializer _storageInitializer = _defaultStorageInitializer; StorageInitializer _storageInitializer = _defaultStorageInitializer;
StorageInitializer _cacheInitializer = _defaultCacheInitializer;
Future<void> _defaultStorageInitializer(String dir) { Future<void> _defaultStorageInitializer(String dir) {
return rust.initStorage(dir: dir); return rust.initStorage(dir: dir);
} }
Future<void> _defaultCacheInitializer(String dir) {
return rust.initCache(dir: dir);
}
Future<File> _copyBundledAssetToDocuments({ Future<File> _copyBundledAssetToDocuments({
required String assetPath, required String assetPath,
required String fileName, required String fileName,
@@ -128,50 +121,16 @@ Future<void> _wireStorageImpl() async {
} }
} }
Future<void> wireCache() async {
final existing = _cacheInitFuture;
if (existing != null) {
await existing;
return;
}
final initFuture = _wireCacheImpl();
_cacheInitFuture = initFuture;
await initFuture;
}
Future<void> _wireCacheImpl() async {
var initialized = false;
try {
final dir = await _cacheDirectoryProvider();
await _cacheInitializer(dir.path);
initialized = true;
} catch (_) {
// Best-effort; missing cache just means protocol-owned assets are
// re-downloaded this session.
} finally {
if (!initialized) {
_cacheInitFuture = null;
}
}
}
@visibleForTesting @visibleForTesting
void debugResetStorageBootstrap({ void debugResetStorageBootstrap({
StorageDirectoryProvider? storageDirectoryProvider, StorageDirectoryProvider? storageDirectoryProvider,
StorageDirectoryProvider? cacheDirectoryProvider,
StorageInitializer? storageInitializer, StorageInitializer? storageInitializer,
StorageInitializer? cacheInitializer,
}) { }) {
_storageInitFuture = null; _storageInitFuture = null;
_cacheInitFuture = null;
_vadBootstrapFuture = null; _vadBootstrapFuture = null;
_storageDirectoryProvider = _storageDirectoryProvider =
storageDirectoryProvider ?? getApplicationSupportDirectory; storageDirectoryProvider ?? getApplicationSupportDirectory;
_cacheDirectoryProvider =
cacheDirectoryProvider ?? getApplicationCacheDirectory;
_storageInitializer = storageInitializer ?? _defaultStorageInitializer; _storageInitializer = storageInitializer ?? _defaultStorageInitializer;
_cacheInitializer = cacheInitializer ?? _defaultCacheInitializer;
} }
rust.BridgeNetworkState _mapConnectivity(List<ConnectivityResult> results) { rust.BridgeNetworkState _mapConnectivity(List<ConnectivityResult> results) {
@@ -6,22 +6,7 @@ import '../src/rust/api.dart' as rust;
const iosAudioLifecycleChannelName = 'chanora/ios_audio_lifecycle'; const iosAudioLifecycleChannelName = 'chanora/ios_audio_lifecycle';
const androidAudioLifecycleChannelName = 'chanora/android_audio_lifecycle'; const androidAudioLifecycleChannelName = 'chanora/android_audio_lifecycle';
const macosAudioLifecycleChannelName = 'chanora/macos_audio_lifecycle';
/// Parses a platform-channel route string into a [rust.BridgeAudioRoute].
///
/// The producer contract is:
/// - iOS: `AppDelegate.classifyAudioRoute(_:)` emits one of
/// `Earpiece`, `Speaker`, `WiredHeadset`, `BluetoothHfp`, `BluetoothA2dp`,
/// `Unknown`.
/// - Android: `AndroidAudioLifecycleController.classifyDevice` emits one of
/// `Earpiece`, `Speaker`, `WiredHeadset`, `UsbHeadset`, `BluetoothHfp`,
/// `BluetoothA2dp`, `Hdmi`, `Unknown`.
///
/// Both producers emit exact PascalCase strings. Case variants (`USB_HEADSET`,
/// `usb_headset`, `UsbHeadphone`) are NOT handled and will fall through to
/// `unknown`. If either platform classifier changes its string contract,
/// update both producers and this parser together.
rust.BridgeAudioRoute parseBridgeAudioRoute(String value) { rust.BridgeAudioRoute parseBridgeAudioRoute(String value) {
switch (value) { switch (value) {
case 'Earpiece': case 'Earpiece':
@@ -29,15 +14,11 @@ rust.BridgeAudioRoute parseBridgeAudioRoute(String value) {
case 'Speaker': case 'Speaker':
return rust.BridgeAudioRoute.speaker; return rust.BridgeAudioRoute.speaker;
case 'WiredHeadset': case 'WiredHeadset':
case 'UsbHeadset':
return rust.BridgeAudioRoute.wiredHeadset; return rust.BridgeAudioRoute.wiredHeadset;
case 'BluetoothHfp': case 'BluetoothHfp':
return rust.BridgeAudioRoute.bluetoothHfp; return rust.BridgeAudioRoute.bluetoothHfp;
case 'BluetoothA2dp': case 'BluetoothA2dp':
return rust.BridgeAudioRoute.bluetoothA2Dp; return rust.BridgeAudioRoute.bluetoothA2Dp;
case 'Hdmi':
case 'Unknown':
return rust.BridgeAudioRoute.unknown;
default: default:
return rust.BridgeAudioRoute.unknown; return rust.BridgeAudioRoute.unknown;
} }
@@ -46,7 +27,6 @@ rust.BridgeAudioRoute parseBridgeAudioRoute(String value) {
void wireAudioLifecycle() { void wireAudioLifecycle() {
wireIosAudioLifecycle(); wireIosAudioLifecycle();
wireAndroidAudioLifecycle(); wireAndroidAudioLifecycle();
wireMacosAudioLifecycle();
} }
/// Wire the iOS AVAudioSession lifecycle MethodChannel. /// Wire the iOS AVAudioSession lifecycle MethodChannel.
@@ -125,43 +105,3 @@ void wireAndroidAudioLifecycle({
} }
}); });
} }
/// Wire the macOS audio lifecycle MethodChannel.
///
/// Swift side (`MacOSAudioLifecycle`) posts `handleDefaultDeviceChange`
/// (with `role: 'input' | 'output'`) when Core Audio HAL default-input /
/// default-output device changes, and `handleConfigurationChange` when
/// the VPIO AudioUnit reports a stream-format change. Closes the
/// iOS/macOS asymmetry noted in SysRS-051.
///
/// Current scope: events are received and logged. The FRB
/// `macosDefaultDeviceChanged` function that triggers a VPIO
/// re-bind on the engine is a follow-up; until it's exposed, the
/// macOS path mirrors the iOS `chanora/ios_audio_lifecycle` event
/// surface but does not yet trigger an engine-side restart.
void wireMacosAudioLifecycle({
bool isMacos = false,
MethodChannel channel = const MethodChannel(macosAudioLifecycleChannelName),
}) {
if (!isMacos && !Platform.isMacOS) return;
channel.setMethodCallHandler((call) async {
try {
switch (call.method) {
case 'handleDefaultDeviceChange':
// TRACKED(macos-device-change): call rust.macosDefaultDeviceChanged()
// once exposed via flutter_rust_bridge; until then the event is
// captured here for observability.
break;
case 'handleConfigurationChange':
// TRACKED(macos-config-change): currently captured, no engine action
// yet — depends on Rust-side device-change API exposure.
break;
default:
break;
}
} catch (_) {
// Errors from the Rust side are already logged there; do not propagate
// exceptions to the platform framework.
}
});
}
@@ -1,31 +0,0 @@
class HardMuteOwners {
const HardMuteOwners({
this.manual = false,
this.permission = false,
this.talkPower = false,
});
final bool manual;
final bool permission;
final bool talkPower;
bool get effective => manual || permission || talkPower;
HardMuteOwners withBridgeManualMute(bool muted) {
return copyWith(
manual: muted && (manual || !permission && !talkPower),
);
}
HardMuteOwners copyWith({
bool? manual,
bool? permission,
bool? talkPower,
}) {
return HardMuteOwners(
manual: manual ?? this.manual,
permission: permission ?? this.permission,
talkPower: talkPower ?? this.talkPower,
);
}
}
@@ -1,66 +0,0 @@
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
const iosAudioSessionChannelName = 'chanora/ios_audio_session';
/// Controls the iOS AVAudioSession VoIP lifecycle from Dart.
///
/// The Swift `AppDelegate` configures the session to `.ambient` at
/// launch and leaves it inactive. The session is only switched to
/// `.playAndRecord` + `.voiceChat` (with `.mixWithOthers`) while a
/// voice channel is actually active. This controller is the Dart
/// side of that contract — call [activate] before the Rust engine
/// starts VoiceProcessingIO and [deactivate] on
/// `BridgeEvent::AudioStopped` or failed joins.
///
/// On non-iOS platforms both methods are no-ops; the platforms
/// handle their own session lifecycle elsewhere (Android via
/// `AndroidAudioLifecycleController`, macOS via
/// `MacOSAudioLifecycle`, desktop has no exclusive session).
class IosAudioSessionController {
IosAudioSessionController({
MethodChannel? channel,
bool? isIos,
}) : _channel = channel ?? const MethodChannel(iosAudioSessionChannelName),
_isIos = isIos ?? Platform.isIOS;
final MethodChannel _channel;
final bool _isIos;
Future<void> activate() async {
if (!_isIos) return;
try {
await _channel.invokeMethod<void>('activateVoiceSession');
} on PlatformException {
// Swift side logs the failure via NSLog; surfacing the
// exception to the event handler would be noise. The Rust
// engine remains alive and will produce silence until the
// next route change or a manual leave/rejoin.
} on MissingPluginException {
// Test hosts and mispackaged builds may not have registered
// the iOS channel. Keep event dispatch alive rather than
// surfacing an unhandled async error.
}
}
Future<void> deactivate() async {
if (!_isIos) return;
try {
await _channel.invokeMethod<void>('deactivateVoiceSession');
} on PlatformException {
// Same rationale as activate(): the Swift side logs.
// Worst case the session stays in .playAndRecord until the
// app is backgrounded — at which point iOS reclaims the
// session automatically.
} on MissingPluginException {
// Same rationale as activate(): missing channel should not
// break bridge event handling.
}
}
}
/// Default singleton used by [main.dart] event dispatch. Tests
/// should construct their own [IosAudioSessionController] with a
/// mocked channel rather than mutating this instance.
final iosAudioSessionController = IosAudioSessionController();
@@ -3,19 +3,11 @@ import 'package:flutter/material.dart';
import '../l10n/generated/app_localizations.dart'; import '../l10n/generated/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// Manages user-trusted link domains to suppress external-link warnings.
///
/// Trusted domains are persisted in [SharedPreferences] under
/// `'trusted_domains'`. Supports wildcard patterns (e.g. `'*.example.com'`)
/// that match any subdomain of the base host.
///
/// This is a singleton; use [LinkTrustService.instance] to obtain it.
class LinkTrustService extends ChangeNotifier { class LinkTrustService extends ChangeNotifier {
static LinkTrustService? _instance; static LinkTrustService? _instance;
final Set<String> _trusted = {}; final Set<String> _trusted = {};
bool _loaded = false; bool _loaded = false;
/// Returns the singleton [LinkTrustService] instance.
static LinkTrustService get instance { static LinkTrustService get instance {
_instance ??= LinkTrustService._(); _instance ??= LinkTrustService._();
return _instance!; return _instance!;
@@ -34,10 +26,6 @@ class LinkTrustService extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Returns `true` if [host] matches any trusted domain pattern.
///
/// Matching is case-insensitive. Wildcard patterns like `'*.example.com'`
/// match both `example.com` and any `*.example.com` subdomain.
bool isTrusted(String host) { bool isTrusted(String host) {
host = host.toLowerCase(); host = host.toLowerCase();
for (final pattern in _trusted) { for (final pattern in _trusted) {
@@ -46,7 +34,6 @@ class LinkTrustService extends ChangeNotifier {
return false; return false;
} }
/// Persists [host] as a trusted domain and notifies listeners.
Future<void> addTrustedDomain(String host) async { Future<void> addTrustedDomain(String host) async {
host = host.toLowerCase(); host = host.toLowerCase();
_trusted.add(host); _trusted.add(host);
@@ -64,10 +51,6 @@ class LinkTrustService extends ChangeNotifier {
} }
} }
/// Shows a dialog asking the user whether to open an external link.
///
/// Returns `true` if the user chose to open and checked "remember this domain",
/// `false` if the user chose to open without remembering, or `null` if cancelled.
Future<bool?> showLinkTrustDialog(BuildContext context, String domain) async { Future<bool?> showLinkTrustDialog(BuildContext context, String domain) async {
bool remember = false; bool remember = false;
return showDialog<bool>( return showDialog<bool>(
@@ -63,8 +63,6 @@ const String methodTriggerLocalNetworkPrompt = 'triggerLocalNetworkPrompt';
@visibleForTesting @visibleForTesting
const String methodCheckLocalNetwork = 'checkLocalNetwork'; const String methodCheckLocalNetwork = 'checkLocalNetwork';
@visibleForTesting @visibleForTesting
const String methodCheckLocalNetworkAccess = 'checkLocalNetworkAccess';
@visibleForTesting
const String methodRequestNotifications = 'requestNotifications'; const String methodRequestNotifications = 'requestNotifications';
@visibleForTesting @visibleForTesting
const String methodCheckNotifications = 'checkNotifications'; const String methodCheckNotifications = 'checkNotifications';
@@ -311,14 +309,12 @@ class MacOSPermissionsService {
_inputMonitoringState.value = state; _inputMonitoringState.value = state;
_pttCapabilityState.value = _pttCapabilityLevel(state); _pttCapabilityState.value = _pttCapabilityLevel(state);
} }
break;
case methodLocalNetworkStateChanged: case methodLocalNetworkStateChanged:
final args = call.arguments; final args = call.arguments;
if (args is Map) { if (args is Map) {
_localNetworkState.value = _localNetworkState.value =
_parseLocalNetworkState(args['state'] as String?); _parseLocalNetworkState(args['state'] as String?);
} }
break;
default: default:
break; break;
} }
@@ -442,37 +438,6 @@ class MacOSPermissionsService {
} }
} }
/// Probe whether Local Network access is currently denied for [host]:[port]
/// by creating a short-lived NWConnection and checking
/// `unsatisfiedReason == .localNetworkDenied`.
///
/// This does NOT trigger a new system prompt — it is a read-only check.
/// Returns [MacOSLocalNetworkState.unsupported] on non-macOS platforms.
Future<MacOSLocalNetworkState> checkLocalNetworkAccess({
required String host,
required int port,
}) async {
final ch = _channel;
if (ch == null) return MacOSLocalNetworkState.unsupported;
try {
final raw = await ch.invokeMethod<String>(
methodCheckLocalNetworkAccess,
<String, dynamic>{'host': host, 'port': port},
);
final state = _parseLocalNetworkState(raw);
_localNetworkState.value = state;
return state;
} catch (e, st) {
developer.log(
'checkLocalNetworkAccess failed',
name: 'MacOSPermissionsService',
error: e,
stackTrace: st,
);
return _localNetworkState.value;
}
}
// -- Outbound: Notifications ---------------------------------------------- // -- Outbound: Notifications ----------------------------------------------
Future<MacOSPermissionState> _checkNotifications() async { Future<MacOSPermissionState> _checkNotifications() async {
@@ -1,198 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../src/rust/api.dart' as rust;
/// Manages local push notifications for TeamSpeak poke events.
///
/// Handles platform-specific notification configuration across Android,
/// iOS, macOS, Linux, and Windows. Notification sound is intentionally
/// delegated to [EventSoundService] (tracked: TODO-event-sounds); this
/// service only manages the visual notification surface.
///
/// Poke strength maps to platform-appropriate urgency levels:
/// - [BridgePokeStrength.strong] → high-priority / time-sensitive
/// - [BridgePokeStrength.suppressed] → normal priority
/// - [BridgePokeStrength.suppressedOverflow] → passive / low priority
class PokeNotificationService {
/// Creates a [PokeNotificationService] with an optional
/// [FlutterLocalNotificationsPlugin] for testing.
PokeNotificationService({FlutterLocalNotificationsPlugin? notifications})
: _notifications = notifications ?? FlutterLocalNotificationsPlugin();
static const _strongAndroidChannelId = 'chanora_pokes_strong_v1';
static const _defaultAndroidChannelId = 'chanora_pokes_default_v1';
static const _groupKey = 'chanora.pokes';
static const _darwinThreadId = 'chanora.pokes';
static const _windowsHeader = WindowsHeader(
id: 'chanora.pokes',
title: 'Pokes',
arguments: 'pokes',
);
static const _windowsAppUserModelId = 'Chanora.Client';
static const _windowsGuid = '6B7F3DCB-4418-4E0A-8CC7-02B7C95B675E';
final FlutterLocalNotificationsPlugin _notifications;
bool _initialized = false;
/// Initializes the notification plugin with platform-specific settings.
///
/// Safe to call multiple times; subsequent calls are no-ops.
Future<void> init() async {
if (_initialized) return;
await _notifications.initialize(
settings: const InitializationSettings(
android: AndroidInitializationSettings('ic_chanora_notification'),
iOS: DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
requestSoundPermission: false,
defaultPresentSound: false,
),
macOS: DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
requestSoundPermission: false,
defaultPresentSound: false,
),
linux: LinuxInitializationSettings(
defaultActionName: 'Open',
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
defaultSuppressSound: true,
),
windows: WindowsInitializationSettings(
appName: 'Chanora',
appUserModelId: _windowsAppUserModelId,
guid: _windowsGuid,
),
),
);
_initialized = true;
}
/// Requests notification permission from the user on the current platform.
///
/// Returns `true` on platforms where permission is not required (web,
/// Linux, Windows) or when the user grants permission.
Future<bool> requestPermission() async {
await init();
if (kIsWeb) return true;
final android = _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
if (android != null) {
return await android.requestNotificationsPermission() ?? true;
}
final ios = _notifications
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin
>();
if (ios != null) {
return await ios.requestPermissions(alert: true, badge: true) ?? false;
}
final macOS = _notifications
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin
>();
if (macOS != null) {
return await macOS.requestPermissions(alert: true, badge: true) ?? false;
}
return true;
}
/// Displays a poke notification from [senderName] with [strength]-based
/// urgency. Silently returns if the user has denied notification permission.
Future<void> show({
required String senderName,
required String message,
required BigInt senderId,
required rust.BridgePokeStrength strength,
}) async {
await init();
final permitted = await requestPermission();
if (!permitted) return;
final trimmedMessage = message.trim();
final body = trimmedMessage.isEmpty
? '$senderName pokes you'
: trimmedMessage;
await _notifications.show(
id: senderId.toUnsigned(31).toInt(),
title: 'Poke from $senderName',
body: body,
notificationDetails: NotificationDetails(
android: _androidDetails(strength),
iOS: _darwinDetails(strength),
macOS: _darwinDetails(strength),
linux: _linuxDetails(strength),
windows: _windowsDetails(strength),
),
payload: 'poke:$senderId',
);
}
AndroidNotificationDetails _androidDetails(rust.BridgePokeStrength strength) {
final isStrong = strength == rust.BridgePokeStrength.strong;
return AndroidNotificationDetails(
isStrong ? _strongAndroidChannelId : _defaultAndroidChannelId,
isStrong ? 'Pokes' : 'Pokes (quiet)',
channelDescription: 'TeamSpeak poke notifications',
importance: isStrong ? Importance.max : Importance.defaultImportance,
priority: isStrong ? Priority.high : Priority.defaultPriority,
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
playSound: false,
silent: true,
groupKey: _groupKey,
category: AndroidNotificationCategory.message,
visibility: NotificationVisibility.private,
);
}
DarwinNotificationDetails _darwinDetails(rust.BridgePokeStrength strength) {
return DarwinNotificationDetails(
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
presentSound: false,
threadIdentifier: _darwinThreadId,
interruptionLevel: switch (strength) {
rust.BridgePokeStrength.strong => InterruptionLevel.timeSensitive,
rust.BridgePokeStrength.suppressed => InterruptionLevel.active,
rust.BridgePokeStrength.suppressedOverflow => InterruptionLevel.passive,
},
);
}
LinuxNotificationDetails _linuxDetails(rust.BridgePokeStrength strength) {
return LinuxNotificationDetails(
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
suppressSound: true,
urgency: switch (strength) {
rust.BridgePokeStrength.strong => LinuxNotificationUrgency.critical,
rust.BridgePokeStrength.suppressed => LinuxNotificationUrgency.normal,
rust.BridgePokeStrength.suppressedOverflow =>
LinuxNotificationUrgency.low,
},
);
}
WindowsNotificationDetails _windowsDetails(rust.BridgePokeStrength strength) {
return WindowsNotificationDetails(
// Sound handled by EventSoundService (tracked: TODO-event-sounds).
audio: WindowsNotificationAudio.silent(),
header: _windowsHeader,
scenario: strength == rust.BridgePokeStrength.strong
? WindowsNotificationScenario.urgent
: null,
duration: strength == rust.BridgePokeStrength.strong
? WindowsNotificationDuration.long
: WindowsNotificationDuration.short,
);
}
}
@@ -1,75 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Persists user preferences for TeamSpeak poke notifications.
///
/// Stores two settings:
/// - Whether pokes are globally enabled
/// - A set of muted sender client IDs
///
/// Preferences are written to [SharedPreferences] and observable
/// through [ValueListenable] so UI widgets can rebuild reactively.
class PokePreferencesService {
static const _enabledKey = 'pokes.enabled';
static const _mutedSendersKey = 'pokes.muted_senders';
final ValueNotifier<bool> _pokesEnabled = ValueNotifier<bool>(true);
final ValueNotifier<Set<BigInt>> _mutedSenders = ValueNotifier<Set<BigInt>>(
const <BigInt>{},
);
/// Whether poke notifications are globally enabled.
ValueListenable<bool> get pokesEnabled => _pokesEnabled;
/// Set of client IDs whose pokes are muted.
ValueListenable<Set<BigInt>> get mutedSenders => _mutedSenders;
/// Loads persisted preferences from [SharedPreferences].
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
_pokesEnabled.value = prefs.getBool(_enabledKey) ?? true;
_mutedSenders.value = (prefs.getStringList(_mutedSendersKey) ?? const [])
.map(BigInt.parse)
.toSet();
}
/// Enables or disables poke notifications globally.
Future<void> setPokesEnabled(bool enabled) async {
_pokesEnabled.value = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_enabledKey, enabled);
}
/// Adds [senderId] to the muted senders set.
Future<void> muteSender(BigInt senderId) async {
if (_mutedSenders.value.contains(senderId)) return;
_mutedSenders.value = {..._mutedSenders.value, senderId};
await _saveMutedSenders();
}
/// Removes [senderId] from the muted senders set.
Future<void> unmuteSender(BigInt senderId) async {
if (!_mutedSenders.value.contains(senderId)) return;
_mutedSenders.value = _mutedSenders.value
.where((mutedSender) => mutedSender != senderId)
.toSet();
await _saveMutedSenders();
}
/// Returns `true` if [senderId] is in the muted senders set.
bool isMuted(BigInt senderId) => _mutedSenders.value.contains(senderId);
Future<void> _saveMutedSenders() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
_mutedSendersKey,
_mutedSenders.value.map((senderId) => senderId.toString()).toList(),
);
}
/// Releases [ValueNotifier] resources.
void dispose() {
_pokesEnabled.dispose();
_mutedSenders.dispose();
}
}
@@ -18,19 +18,11 @@ class UiSettings {
this.host = '', this.host = '',
this.nickname = '', this.nickname = '',
this.themeMode = UiThemeMode.system, this.themeMode = UiThemeMode.system,
this.transmitModeIndex,
this.releaseTailMs,
this.inputDeviceId,
this.outputDeviceId,
}); });
final String host; final String host;
final String nickname; final String nickname;
final UiThemeMode themeMode; final UiThemeMode themeMode;
final int? transmitModeIndex;
final int? releaseTailMs;
final String? inputDeviceId;
final String? outputDeviceId;
} }
class UiPreferencesService { class UiPreferencesService {
@@ -38,10 +30,6 @@ class UiPreferencesService {
static const _nicknameKey = 'ui.nickname'; static const _nicknameKey = 'ui.nickname';
static const _themeModeKey = 'ui.theme_mode'; static const _themeModeKey = 'ui.theme_mode';
static const _permissionsExplainedKey = 'perms_explained'; static const _permissionsExplainedKey = 'perms_explained';
static const _transmitModeIndexKey = 'voice.transmit_mode_index';
static const _releaseTailMsKey = 'voice.release_tail_ms';
static const _inputDeviceIdKey = 'audio.input_device_id';
static const _outputDeviceIdKey = 'audio.output_device_id';
const UiPreferencesService(); const UiPreferencesService();
@@ -51,10 +39,6 @@ class UiPreferencesService {
host: prefs.getString(_hostKey) ?? '', host: prefs.getString(_hostKey) ?? '',
nickname: prefs.getString(_nicknameKey) ?? '', nickname: prefs.getString(_nicknameKey) ?? '',
themeMode: UiThemeMode.fromStorage(prefs.getString(_themeModeKey)), themeMode: UiThemeMode.fromStorage(prefs.getString(_themeModeKey)),
transmitModeIndex: prefs.getInt(_transmitModeIndexKey),
releaseTailMs: prefs.getInt(_releaseTailMsKey),
inputDeviceId: prefs.getString(_inputDeviceIdKey),
outputDeviceId: prefs.getString(_outputDeviceIdKey),
); );
} }
@@ -69,34 +53,6 @@ class UiPreferencesService {
await prefs.setString(_themeModeKey, themeMode.name); await prefs.setString(_themeModeKey, themeMode.name);
} }
Future<void> saveTransmitModeIndex(int index) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_transmitModeIndexKey, index);
}
Future<void> saveReleaseTailMs(int ms) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_releaseTailMsKey, ms);
}
Future<void> saveInputDeviceId(String? id) async {
final prefs = await SharedPreferences.getInstance();
if (id == null) {
await prefs.remove(_inputDeviceIdKey);
} else {
await prefs.setString(_inputDeviceIdKey, id);
}
}
Future<void> saveOutputDeviceId(String? id) async {
final prefs = await SharedPreferences.getInstance();
if (id == null) {
await prefs.remove(_outputDeviceIdKey);
} else {
await prefs.setString(_outputDeviceIdKey, id);
}
}
Future<bool> hasExplainedPermissions() async { Future<bool> hasExplainedPermissions() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_permissionsExplainedKey) ?? false; return prefs.getBool(_permissionsExplainedKey) ?? false;
@@ -1,36 +0,0 @@
typedef VoiceJoinCallback = Future<void> Function({
required BigInt channelId,
required String password,
});
typedef IosVoiceSessionActivation = Future<void> Function();
typedef IosVoiceSessionDeactivation = Future<void> Function();
/// Predicate used to recognise `voiceJoin` errors that the caller treats as a
/// successful join outcome (e.g. the server replied "already in channel").
///
/// When this returns `true` for a thrown error, the iOS audio session is kept
/// active because the user is still considered joined to the channel. The
/// error is still rethrown so the caller can run its success-on-already-joined
/// branch and update local state.
typedef VoiceJoinSuccessPredicate = bool Function(Object error);
Future<void> joinVoiceChannelWithIosAudioSession({
required BigInt channelId,
required String password,
required VoiceJoinCallback voiceJoin,
required IosVoiceSessionActivation activateIosAudioSession,
required IosVoiceSessionDeactivation deactivateIosAudioSession,
VoiceJoinSuccessPredicate? isJoinSuccess,
}) async {
await activateIosAudioSession();
try {
await voiceJoin(channelId: channelId, password: password);
} catch (e) {
if (isJoinSuccess != null && isJoinSuccess(e)) {
rethrow;
}
await deactivateIosAudioSession();
rethrow;
}
}
+9 -127
View File
@@ -11,7 +11,7 @@ part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `dispatch_platform_audio_event`, `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `platform_audio_events`, `process`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8` // These functions are ignored because they are not marked as `pub`: `dispatch_platform_audio_event`, `install_panic_diagnostic_hook`, `log_file_path`, `log_sink`, `map_join_error_code`, `map_join_sync_state`, `open_log_file`, `permission_events`, `platform_audio_events`, `process`, `publish_permission_state`, `runtime`, `session`, `task_join_error`, `transmit_mode_from_u8`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `PlatformAudioEvent` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `PlatformAudioEvent`
// 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`, `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`, `clone`, `clone`, `eq`, `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`, `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`, `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`, `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`, `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`, `from`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate` // These functions are ignored (category: IgnoreBecauseExplicitAttribute): `from_kotlin_str`, `to_permission_gate`
/// Return the platform-conventional log-file path as a string, or /// Return the platform-conventional log-file path as a string, or
@@ -226,29 +226,6 @@ String exportDiagnostics() => RustLib.instance.api.crateApiExportDiagnostics();
Future<void> initStorage({required String dir}) => Future<void> initStorage({required String dir}) =>
RustLib.instance.api.crateApiInitStorage(dir: dir); RustLib.instance.api.crateApiInitStorage(dir: dir);
/// Configure the bridge blob cache root.
Future<void> initCache({required String dir}) =>
RustLib.instance.api.crateApiInitCache(dir: dir);
/// Resolve avatar bytes through the bridge.
Future<Uint8List?> downloadAvatar({
required String avatarHash,
required String clientUid,
}) => RustLib.instance.api.crateApiDownloadAvatar(
avatarHash: avatarHash,
clientUid: clientUid,
);
/// Resolve icon bytes through the bridge.
Future<Uint8List?> downloadIcon({required BigInt iconId}) =>
RustLib.instance.api.crateApiDownloadIcon(iconId: iconId);
/// Purge cached protocol-owned assets.
Future<void> clearFileCache() => RustLib.instance.api.crateApiClearFileCache();
/// Report the configured file-cache size.
Future<BigInt> fileCacheSize() => RustLib.instance.api.crateApiFileCacheSize();
/// List persisted bookmarks. /// List persisted bookmarks.
Future<List<BridgeBookmark>> listBookmarks() => Future<List<BridgeBookmark>> listBookmarks() =>
RustLib.instance.api.crateApiListBookmarks(); RustLib.instance.api.crateApiListBookmarks();
@@ -284,13 +261,6 @@ Stream<BridgeEvent> eventsStream() =>
Future<BridgeAudioStats> audioStats() => Future<BridgeAudioStats> audioStats() =>
RustLib.instance.api.crateApiAudioStats(); RustLib.instance.api.crateApiAudioStats();
/// Subscribe to real-time microphone input level at ~30 Hz.
/// Values are dBFS (-120 = silence, 0 = clipping). The stream ends
/// when the Dart subscriber cancels, the session is dropped, or
/// the session becomes persistently unavailable.
Stream<double> inputLevelStream() =>
RustLib.instance.api.crateApiInputLevelStream();
/// Apply the P1 audio-processing config. /// Apply the P1 audio-processing config.
Future<void> setAudioProcessingConfig({ Future<void> setAudioProcessingConfig({
required BridgeAudioProcessingConfig config, required BridgeAudioProcessingConfig config,
@@ -711,22 +681,15 @@ class BridgeAudioStats {
/// Current push-to-talk state. /// Current push-to-talk state.
final bool pttActive; final bool pttActive;
/// Current microphone input level in dBFS (-120.0 = silence, 0.0 = clipping).
final double inputLevel;
const BridgeAudioStats({ const BridgeAudioStats({
required this.framesSent, required this.framesSent,
required this.framesReceived, required this.framesReceived,
required this.pttActive, required this.pttActive,
required this.inputLevel,
}); });
@override @override
int get hashCode => int get hashCode =>
framesSent.hashCode ^ framesSent.hashCode ^ framesReceived.hashCode ^ pttActive.hashCode;
framesReceived.hashCode ^
pttActive.hashCode ^
inputLevel.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -735,8 +698,7 @@ class BridgeAudioStats {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
framesSent == other.framesSent && framesSent == other.framesSent &&
framesReceived == other.framesReceived && framesReceived == other.framesReceived &&
pttActive == other.pttActive && pttActive == other.pttActive;
inputLevel == other.inputLevel;
} }
/// Bookmark DTO mirroring [`chanora_core::Bookmark`]. /// Bookmark DTO mirroring [`chanora_core::Bookmark`].
@@ -1233,9 +1195,6 @@ sealed class BridgeEvent with _$BridgeEvent {
/// Target scope (server/channel/private/poke). /// Target scope (server/channel/private/poke).
required BridgeMessageTarget target, required BridgeMessageTarget target,
/// Poke notification strength, present only for poke messages.
BridgePokeStrength? pokeStrength,
}) = BridgeEvent_ChatMessage; }) = BridgeEvent_ChatMessage;
/// Human-readable server activity surfaced from protocol bookkeeping events. /// Human-readable server activity surfaced from protocol bookkeeping events.
@@ -1246,124 +1205,59 @@ sealed class BridgeEvent with _$BridgeEvent {
/// Audio route changed (speaker/earpiece/BT/wired). /// Audio route changed (speaker/earpiece/BT/wired).
const factory BridgeEvent.audioRouteChanged({ const factory BridgeEvent.audioRouteChanged({
/// New audio output route.
required BridgeAudioRoute route, required BridgeAudioRoute route,
}) = BridgeEvent_AudioRouteChanged; }) = BridgeEvent_AudioRouteChanged;
/// A client moved to a different channel.
const factory BridgeEvent.clientMoved({ const factory BridgeEvent.clientMoved({
/// Unique client identifier.
required BigInt clientId, required BigInt clientId,
/// Destination channel.
required BigInt newChannelId, required BigInt newChannelId,
}) = BridgeEvent_ClientMoved; }) = BridgeEvent_ClientMoved;
/// A new client connected.
const factory BridgeEvent.clientJoined({ const factory BridgeEvent.clientJoined({
/// Unique client identifier.
required BigInt clientId, required BigInt clientId,
/// Channel the client joined.
required BigInt channelId, required BigInt channelId,
/// Display nickname.
required String name, required String name,
/// Microphone muted state.
required bool inputMuted, required bool inputMuted,
/// Speaker muted state.
required bool outputMuted, required bool outputMuted,
/// True for server query (bot) clients.
required bool isServerQuery, required bool isServerQuery,
/// Client's talk power value.
required int talkPower, required int talkPower,
/// Whether the server granted temporary talk power.
required bool talkPowerGranted, required bool talkPowerGranted,
}) = BridgeEvent_ClientJoined; }) = BridgeEvent_ClientJoined;
/// A client disconnected.
const factory BridgeEvent.clientLeft({ const factory BridgeEvent.clientLeft({
/// Unique client identifier.
required BigInt clientId, required BigInt clientId,
/// Display nickname at time of disconnect.
required String name, required String name,
}) = BridgeEvent_ClientLeft; }) = BridgeEvent_ClientLeft;
/// Client properties changed.
const factory BridgeEvent.clientUpdated({ const factory BridgeEvent.clientUpdated({
/// Unique client identifier.
required BigInt clientId, required BigInt clientId,
/// Microphone muted state.
required bool inputMuted, required bool inputMuted,
/// Speaker muted state.
required bool outputMuted, required bool outputMuted,
/// True for server query (bot) clients.
required bool isServerQuery, required bool isServerQuery,
/// Client's talk power value.
required int talkPower, required int talkPower,
/// Whether the server granted temporary talk power.
required bool talkPowerGranted, required bool talkPowerGranted,
}) = BridgeEvent_ClientUpdated; }) = BridgeEvent_ClientUpdated;
/// A new channel appeared.
const factory BridgeEvent.channelAdded({ const factory BridgeEvent.channelAdded({
/// Unique channel identifier.
required BigInt id, required BigInt id,
/// Parent channel ID.
required BigInt parent, required BigInt parent,
/// Channel name.
required String name, required String name,
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
required PlatformInt64 order, required PlatformInt64 order,
/// Whether the channel requires a password.
required bool hasPassword, required bool hasPassword,
/// Talk power required to speak; `None` means no restriction.
int? neededTalkPower, int? neededTalkPower,
}) = BridgeEvent_ChannelAdded; }) = BridgeEvent_ChannelAdded;
const factory BridgeEvent.channelRemoved({required BigInt id}) =
/// A channel was deleted. BridgeEvent_ChannelRemoved;
const factory BridgeEvent.channelRemoved({
/// Channel identifier.
required BigInt id,
}) = BridgeEvent_ChannelRemoved;
/// Channel properties changed.
const factory BridgeEvent.channelUpdated({ const factory BridgeEvent.channelUpdated({
/// Unique channel identifier.
required BigInt id, required BigInt id,
/// Channel name.
required String name, required String name,
/// Whether the channel requires a password.
required bool hasPassword, required bool hasPassword,
/// Talk power required to speak; `None` means no restriction.
int? neededTalkPower, int? neededTalkPower,
}) = BridgeEvent_ChannelUpdated; }) = BridgeEvent_ChannelUpdated;
} }
/// Bridge iOS voice-processing mode. /// Bridge iOS voice-processing mode.
enum BridgeIosVoiceProcessingMode { enum BridgeIosVoiceProcessingMode {
/// Apple VoiceProcessingIO path. /// Shipping VPIO path.
platformVoiceProcessing, platformVoiceProcessing,
/// Experimental Sonora path.
sonoraExperimental,
} }
@freezed @freezed
@@ -1398,18 +1292,6 @@ enum BridgeNetworkState {
offline, offline,
} }
/// Bridge poke notification strength.
enum BridgePokeStrength {
/// Poke should be surfaced at full strength.
strong,
/// Poke is rate-limited but below overflow severity.
suppressed,
/// Poke remains suppressed after repeated suppressed pokes.
suppressedOverflow,
}
/// Persisted PTT binding display state for the UI. /// Persisted PTT binding display state for the UI.
class BridgePttBinding { class BridgePttBinding {
/// Stable input category string (`""`, `"keyboard"`, or /// Stable input category string (`""`, `"keyboard"`, or
@@ -173,7 +173,7 @@ return channelUpdated(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength)? chatMessage,TResult Function( String message)? serverActivity,TResult Function( BridgeAudioRoute route)? audioRouteChanged,TResult Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult Function( BigInt clientId, String name)? clientLeft,TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult Function( BigInt id)? channelRemoved,TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,TResult Function( String permission, PermissionStateKind state)? permissionState,TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult Function( String message)? serverActivity,TResult Function( BridgeAudioRoute route)? audioRouteChanged,TResult Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult Function( BigInt clientId, String name)? clientLeft,TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult Function( BigInt id)? channelRemoved,TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case BridgeEvent_Connected() when connected != null: case BridgeEvent_Connected() when connected != null:
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null: return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
@@ -186,7 +186,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null: return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null:
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null: return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null:
return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null: return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null:
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target,_that.pokeStrength);case BridgeEvent_ServerActivity() when serverActivity != null: return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity() when serverActivity != null:
return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null:
return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved() when clientMoved != null: return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved() when clientMoved != null:
return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined() when clientJoined != null: return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined() when clientJoined != null:
@@ -213,7 +213,7 @@ return channelUpdated(_that.id,_that.name,_that.hasPassword,_that.neededTalkPowe
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,required TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength) chatMessage,required TResult Function( String message) serverActivity,required TResult Function( BridgeAudioRoute route) audioRouteChanged,required TResult Function( BigInt clientId, BigInt newChannelId) clientMoved,required TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientJoined,required TResult Function( BigInt clientId, String name) clientLeft,required TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientUpdated,required TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower) channelAdded,required TResult Function( BigInt id) channelRemoved,required TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower) channelUpdated,}) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,required TResult Function( String permission, PermissionStateKind state) permissionState,required TResult Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target) chatMessage,required TResult Function( String message) serverActivity,required TResult Function( BridgeAudioRoute route) audioRouteChanged,required TResult Function( BigInt clientId, BigInt newChannelId) clientMoved,required TResult Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientJoined,required TResult Function( BigInt clientId, String name) clientLeft,required TResult Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted) clientUpdated,required TResult Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower) channelAdded,required TResult Function( BigInt id) channelRemoved,required TResult Function( BigInt id, String name, bool hasPassword, int? neededTalkPower) channelUpdated,}) {final _that = this;
switch (_that) { switch (_that) {
case BridgeEvent_Connected(): case BridgeEvent_Connected():
return connected(_that.serverName);case BridgeEvent_Lost(): return connected(_that.serverName);case BridgeEvent_Lost():
@@ -226,7 +226,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState(): return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState():
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState(): return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState():
return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage(): return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage():
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target,_that.pokeStrength);case BridgeEvent_ServerActivity(): return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity():
return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged(): return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged():
return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved(): return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved():
return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined(): return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined():
@@ -249,7 +249,7 @@ return channelUpdated(_that.id,_that.name,_that.hasPassword,_that.neededTalkPowe
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,TResult? Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength)? chatMessage,TResult? Function( String message)? serverActivity,TResult? Function( BridgeAudioRoute route)? audioRouteChanged,TResult? Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult? Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult? Function( BigInt clientId, String name)? clientLeft,TResult? Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult? Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult? Function( BigInt id)? channelRemoved,TResult? Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,}) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs, BigInt? currentChannelId, BigInt? pendingTargetChannelId, bool canJoin, bool canLeave, BridgeVoiceJoinSyncState joinSyncState, BridgeVoiceJoinErrorCode? joinErrorCode)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,TResult? Function( String permission, PermissionStateKind state)? permissionState,TResult? Function( BigInt senderId, String senderName, String message, BridgeMessageTarget target)? chatMessage,TResult? Function( String message)? serverActivity,TResult? Function( BridgeAudioRoute route)? audioRouteChanged,TResult? Function( BigInt clientId, BigInt newChannelId)? clientMoved,TResult? Function( BigInt clientId, BigInt channelId, String name, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientJoined,TResult? Function( BigInt clientId, String name)? clientLeft,TResult? Function( BigInt clientId, bool inputMuted, bool outputMuted, bool isServerQuery, int talkPower, bool talkPowerGranted)? clientUpdated,TResult? Function( BigInt id, BigInt parent, String name, PlatformInt64 order, bool hasPassword, int? neededTalkPower)? channelAdded,TResult? Function( BigInt id)? channelRemoved,TResult? Function( BigInt id, String name, bool hasPassword, int? neededTalkPower)? channelUpdated,}) {final _that = this;
switch (_that) { switch (_that) {
case BridgeEvent_Connected() when connected != null: case BridgeEvent_Connected() when connected != null:
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null: return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
@@ -262,7 +262,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case Bri
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null: return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs,_that.currentChannelId,_that.pendingTargetChannelId,_that.canJoin,_that.canLeave,_that.joinSyncState,_that.joinErrorCode);case BridgeEvent_InterruptionState() when interruptionState != null:
return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null: return interruptionState(_that.began,_that.shouldResume);case BridgeEvent_PermissionState() when permissionState != null:
return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null: return permissionState(_that.permission,_that.state);case BridgeEvent_ChatMessage() when chatMessage != null:
return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target,_that.pokeStrength);case BridgeEvent_ServerActivity() when serverActivity != null: return chatMessage(_that.senderId,_that.senderName,_that.message,_that.target);case BridgeEvent_ServerActivity() when serverActivity != null:
return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null: return serverActivity(_that.message);case BridgeEvent_AudioRouteChanged() when audioRouteChanged != null:
return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved() when clientMoved != null: return audioRouteChanged(_that.route);case BridgeEvent_ClientMoved() when clientMoved != null:
return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined() when clientJoined != null: return clientMoved(_that.clientId,_that.newChannelId);case BridgeEvent_ClientJoined() when clientJoined != null:
@@ -929,7 +929,7 @@ as PermissionStateKind,
class BridgeEvent_ChatMessage extends BridgeEvent { class BridgeEvent_ChatMessage extends BridgeEvent {
const BridgeEvent_ChatMessage({required this.senderId, required this.senderName, required this.message, required this.target, this.pokeStrength}): super._(); const BridgeEvent_ChatMessage({required this.senderId, required this.senderName, required this.message, required this.target}): super._();
/// Client id of the sender. /// Client id of the sender.
@@ -940,8 +940,6 @@ class BridgeEvent_ChatMessage extends BridgeEvent {
final String message; final String message;
/// Target scope (server/channel/private/poke). /// Target scope (server/channel/private/poke).
final BridgeMessageTarget target; final BridgeMessageTarget target;
/// Poke notification strength, present only for poke messages.
final BridgePokeStrength? pokeStrength;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -953,16 +951,16 @@ $BridgeEvent_ChatMessageCopyWith<BridgeEvent_ChatMessage> get copyWith => _$Brid
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_ChatMessage&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.senderName, senderName) || other.senderName == senderName)&&(identical(other.message, message) || other.message == message)&&(identical(other.target, target) || other.target == target)&&(identical(other.pokeStrength, pokeStrength) || other.pokeStrength == pokeStrength)); return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_ChatMessage&&(identical(other.senderId, senderId) || other.senderId == senderId)&&(identical(other.senderName, senderName) || other.senderName == senderName)&&(identical(other.message, message) || other.message == message)&&(identical(other.target, target) || other.target == target));
} }
@override @override
int get hashCode => Object.hash(runtimeType,senderId,senderName,message,target,pokeStrength); int get hashCode => Object.hash(runtimeType,senderId,senderName,message,target);
@override @override
String toString() { String toString() {
return 'BridgeEvent.chatMessage(senderId: $senderId, senderName: $senderName, message: $message, target: $target, pokeStrength: $pokeStrength)'; return 'BridgeEvent.chatMessage(senderId: $senderId, senderName: $senderName, message: $message, target: $target)';
} }
@@ -973,7 +971,7 @@ abstract mixin class $BridgeEvent_ChatMessageCopyWith<$Res> implements $BridgeEv
factory $BridgeEvent_ChatMessageCopyWith(BridgeEvent_ChatMessage value, $Res Function(BridgeEvent_ChatMessage) _then) = _$BridgeEvent_ChatMessageCopyWithImpl; factory $BridgeEvent_ChatMessageCopyWith(BridgeEvent_ChatMessage value, $Res Function(BridgeEvent_ChatMessage) _then) = _$BridgeEvent_ChatMessageCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
BigInt senderId, String senderName, String message, BridgeMessageTarget target, BridgePokeStrength? pokeStrength BigInt senderId, String senderName, String message, BridgeMessageTarget target
}); });
@@ -990,14 +988,13 @@ class _$BridgeEvent_ChatMessageCopyWithImpl<$Res>
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? senderId = null,Object? senderName = null,Object? message = null,Object? target = null,Object? pokeStrength = freezed,}) { @pragma('vm:prefer-inline') $Res call({Object? senderId = null,Object? senderName = null,Object? message = null,Object? target = null,}) {
return _then(BridgeEvent_ChatMessage( return _then(BridgeEvent_ChatMessage(
senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable senderId: null == senderId ? _self.senderId : senderId // ignore: cast_nullable_to_non_nullable
as BigInt,senderName: null == senderName ? _self.senderName : senderName // ignore: cast_nullable_to_non_nullable as BigInt,senderName: null == senderName ? _self.senderName : senderName // ignore: cast_nullable_to_non_nullable
as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,target: null == target ? _self.target : target // ignore: cast_nullable_to_non_nullable as String,target: null == target ? _self.target : target // ignore: cast_nullable_to_non_nullable
as BridgeMessageTarget,pokeStrength: freezed == pokeStrength ? _self.pokeStrength : pokeStrength // ignore: cast_nullable_to_non_nullable as BridgeMessageTarget,
as BridgePokeStrength?,
)); ));
} }
@@ -1087,7 +1084,6 @@ class BridgeEvent_AudioRouteChanged extends BridgeEvent {
const BridgeEvent_AudioRouteChanged({required this.route}): super._(); const BridgeEvent_AudioRouteChanged({required this.route}): super._();
/// New audio output route.
final BridgeAudioRoute route; final BridgeAudioRoute route;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1154,9 +1150,7 @@ class BridgeEvent_ClientMoved extends BridgeEvent {
const BridgeEvent_ClientMoved({required this.clientId, required this.newChannelId}): super._(); const BridgeEvent_ClientMoved({required this.clientId, required this.newChannelId}): super._();
/// Unique client identifier.
final BigInt clientId; final BigInt clientId;
/// Destination channel.
final BigInt newChannelId; final BigInt newChannelId;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1224,21 +1218,13 @@ class BridgeEvent_ClientJoined extends BridgeEvent {
const BridgeEvent_ClientJoined({required this.clientId, required this.channelId, required this.name, required this.inputMuted, required this.outputMuted, required this.isServerQuery, required this.talkPower, required this.talkPowerGranted}): super._(); const BridgeEvent_ClientJoined({required this.clientId, required this.channelId, required this.name, required this.inputMuted, required this.outputMuted, required this.isServerQuery, required this.talkPower, required this.talkPowerGranted}): super._();
/// Unique client identifier.
final BigInt clientId; final BigInt clientId;
/// Channel the client joined.
final BigInt channelId; final BigInt channelId;
/// Display nickname.
final String name; final String name;
/// Microphone muted state.
final bool inputMuted; final bool inputMuted;
/// Speaker muted state.
final bool outputMuted; final bool outputMuted;
/// True for server query (bot) clients.
final bool isServerQuery; final bool isServerQuery;
/// Client's talk power value.
final int talkPower; final int talkPower;
/// Whether the server granted temporary talk power.
final bool talkPowerGranted; final bool talkPowerGranted;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1312,9 +1298,7 @@ class BridgeEvent_ClientLeft extends BridgeEvent {
const BridgeEvent_ClientLeft({required this.clientId, required this.name}): super._(); const BridgeEvent_ClientLeft({required this.clientId, required this.name}): super._();
/// Unique client identifier.
final BigInt clientId; final BigInt clientId;
/// Display nickname at time of disconnect.
final String name; final String name;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1382,17 +1366,11 @@ class BridgeEvent_ClientUpdated extends BridgeEvent {
const BridgeEvent_ClientUpdated({required this.clientId, required this.inputMuted, required this.outputMuted, required this.isServerQuery, required this.talkPower, required this.talkPowerGranted}): super._(); const BridgeEvent_ClientUpdated({required this.clientId, required this.inputMuted, required this.outputMuted, required this.isServerQuery, required this.talkPower, required this.talkPowerGranted}): super._();
/// Unique client identifier.
final BigInt clientId; final BigInt clientId;
/// Microphone muted state.
final bool inputMuted; final bool inputMuted;
/// Speaker muted state.
final bool outputMuted; final bool outputMuted;
/// True for server query (bot) clients.
final bool isServerQuery; final bool isServerQuery;
/// Client's talk power value.
final int talkPower; final int talkPower;
/// Whether the server granted temporary talk power.
final bool talkPowerGranted; final bool talkPowerGranted;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1464,18 +1442,11 @@ class BridgeEvent_ChannelAdded extends BridgeEvent {
const BridgeEvent_ChannelAdded({required this.id, required this.parent, required this.name, required this.order, required this.hasPassword, this.neededTalkPower}): super._(); const BridgeEvent_ChannelAdded({required this.id, required this.parent, required this.name, required this.order, required this.hasPassword, this.neededTalkPower}): super._();
/// Unique channel identifier.
final BigInt id; final BigInt id;
/// Parent channel ID.
final BigInt parent; final BigInt parent;
/// Channel name.
final String name; final String name;
/// Predecessor channel ID within the same parent (TeamSpeak
/// linked-list ordering hint). Zero means first child.
final PlatformInt64 order; final PlatformInt64 order;
/// Whether the channel requires a password.
final bool hasPassword; final bool hasPassword;
/// Talk power required to speak; `None` means no restriction.
final int? neededTalkPower; final int? neededTalkPower;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1547,7 +1518,6 @@ class BridgeEvent_ChannelRemoved extends BridgeEvent {
const BridgeEvent_ChannelRemoved({required this.id}): super._(); const BridgeEvent_ChannelRemoved({required this.id}): super._();
/// Channel identifier.
final BigInt id; final BigInt id;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -1614,13 +1584,9 @@ class BridgeEvent_ChannelUpdated extends BridgeEvent {
const BridgeEvent_ChannelUpdated({required this.id, required this.name, required this.hasPassword, this.neededTalkPower}): super._(); const BridgeEvent_ChannelUpdated({required this.id, required this.name, required this.hasPassword, this.neededTalkPower}): super._();
/// Unique channel identifier.
final BigInt id; final BigInt id;
/// Channel name.
final String name; final String name;
/// Whether the channel requires a password.
final bool hasPassword; final bool hasPassword;
/// Talk power required to speak; `None` means no restriction.
final int? neededTalkPower; final int? neededTalkPower;
/// Create a copy of BridgeEvent /// Create a copy of BridgeEvent
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0'; String get codegenVersion => '2.12.0';
@override @override
int get rustContentHash => 635684021; int get rustContentHash => 281698435;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@@ -87,8 +87,6 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiBridgeInit(); Future<void> crateApiBridgeInit();
Future<void> crateApiClearFileCache();
Future<BridgeClientProfile> crateApiClientProfile({required BigInt clientId}); Future<BridgeClientProfile> crateApiClientProfile({required BigInt clientId});
Future<BridgeSnapshot> crateApiConnect({ Future<BridgeSnapshot> crateApiConnect({
@@ -101,21 +99,12 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateApiDisconnect(); Future<void> crateApiDisconnect();
Future<Uint8List?> crateApiDownloadAvatar({
required String avatarHash,
required String clientUid,
});
Future<Uint8List?> crateApiDownloadIcon({required BigInt iconId});
Future<void> crateApiEnableAudioDebugWavDump({required bool enabled}); Future<void> crateApiEnableAudioDebugWavDump({required bool enabled});
Stream<BridgeEvent> crateApiEventsStream(); Stream<BridgeEvent> crateApiEventsStream();
String crateApiExportDiagnostics(); String crateApiExportDiagnostics();
Future<BigInt> crateApiFileCacheSize();
Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig(); Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig();
Future<BridgePttBinding> crateApiGetPttBinding(); Future<BridgePttBinding> crateApiGetPttBinding();
@@ -132,12 +121,8 @@ abstract class RustLibApi extends BaseApi {
void crateApiHandleRouteChange({required BridgeAudioRoute route}); void crateApiHandleRouteChange({required BridgeAudioRoute route});
Future<void> crateApiInitCache({required String dir});
Future<void> crateApiInitStorage({required String dir}); Future<void> crateApiInitStorage({required String dir});
Stream<double> crateApiInputLevelStream();
Future<bool> crateApiIsConnected(); Future<bool> crateApiIsConnected();
Future<BridgeAudioDeviceList> crateApiListAudioDevices(); Future<BridgeAudioDeviceList> crateApiListAudioDevices();
@@ -333,33 +318,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiBridgeInitConstMeta => TaskConstMeta get kCrateApiBridgeInitConstMeta =>
const TaskConstMeta(debugName: "bridge_init", argNames: []); const TaskConstMeta(debugName: "bridge_init", argNames: []);
@override
Future<void> crateApiClearFileCache() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 5,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiClearFileCacheConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiClearFileCacheConstMeta =>
const TaskConstMeta(debugName: "clear_file_cache", argNames: []);
@override @override
Future<BridgeClientProfile> crateApiClientProfile({ Future<BridgeClientProfile> crateApiClientProfile({
required BigInt clientId, required BigInt clientId,
@@ -372,7 +330,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 6, funcId: 5,
port: port_, port: port_,
); );
}, },
@@ -406,7 +364,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 7, funcId: 6,
port: port_, port: port_,
); );
}, },
@@ -436,7 +394,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 8, funcId: 7,
port: port_, port: port_,
); );
}, },
@@ -463,7 +421,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 9, funcId: 8,
port: port_, port: port_,
); );
}, },
@@ -481,68 +439,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiDisconnectConstMeta => TaskConstMeta get kCrateApiDisconnectConstMeta =>
const TaskConstMeta(debugName: "disconnect", argNames: []); const TaskConstMeta(debugName: "disconnect", argNames: []);
@override
Future<Uint8List?> crateApiDownloadAvatar({
required String avatarHash,
required String clientUid,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(avatarHash, serializer);
sse_encode_String(clientUid, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 10,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_opt_list_prim_u_8_strict,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiDownloadAvatarConstMeta,
argValues: [avatarHash, clientUid],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiDownloadAvatarConstMeta => const TaskConstMeta(
debugName: "download_avatar",
argNames: ["avatarHash", "clientUid"],
);
@override
Future<Uint8List?> crateApiDownloadIcon({required BigInt iconId}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_u_64(iconId, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 11,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_opt_list_prim_u_8_strict,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiDownloadIconConstMeta,
argValues: [iconId],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiDownloadIconConstMeta =>
const TaskConstMeta(debugName: "download_icon", argNames: ["iconId"]);
@override @override
Future<void> crateApiEnableAudioDebugWavDump({required bool enabled}) { Future<void> crateApiEnableAudioDebugWavDump({required bool enabled}) {
return handler.executeNormal( return handler.executeNormal(
@@ -553,7 +449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 12, funcId: 9,
port: port_, port: port_,
); );
}, },
@@ -586,7 +482,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 13, funcId: 10,
port: port_, port: port_,
); );
}, },
@@ -612,7 +508,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask( SyncTask(
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 11)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_String, decodeSuccessData: sse_decode_String,
@@ -628,33 +524,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiExportDiagnosticsConstMeta => TaskConstMeta get kCrateApiExportDiagnosticsConstMeta =>
const TaskConstMeta(debugName: "export_diagnostics", argNames: []); const TaskConstMeta(debugName: "export_diagnostics", argNames: []);
@override
Future<BigInt> crateApiFileCacheSize() {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 15,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_u_64,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiFileCacheSizeConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiFileCacheSizeConstMeta =>
const TaskConstMeta(debugName: "file_cache_size", argNames: []);
@override @override
Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig() { Future<BridgeAudioProcessingConfig> crateApiGetAudioProcessingConfig() {
return handler.executeNormal( return handler.executeNormal(
@@ -664,7 +533,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 16, funcId: 12,
port: port_, port: port_,
); );
}, },
@@ -694,7 +563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 17, funcId: 13,
port: port_, port: port_,
); );
}, },
@@ -721,7 +590,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 18, funcId: 14,
port: port_, port: port_,
); );
}, },
@@ -748,7 +617,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 19, funcId: 15,
port: port_, port: port_,
); );
}, },
@@ -772,7 +641,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask( SyncTask(
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -795,7 +664,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bool(shouldResume, serializer); sse_encode_bool(shouldResume, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 17)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -821,7 +690,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(routeClass, serializer); sse_encode_String(routeClass, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 22)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -847,7 +716,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_audio_route(route, serializer); sse_encode_bridge_audio_route(route, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -865,34 +734,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["route"], argNames: ["route"],
); );
@override
Future<void> crateApiInitCache({required String dir}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(dir, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 24,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiInitCacheConstMeta,
argValues: [dir],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiInitCacheConstMeta =>
const TaskConstMeta(debugName: "init_cache", argNames: ["dir"]);
@override @override
Future<void> crateApiInitStorage({required String dir}) { Future<void> crateApiInitStorage({required String dir}) {
return handler.executeNormal( return handler.executeNormal(
@@ -903,7 +744,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 25, funcId: 20,
port: port_, port: port_,
); );
}, },
@@ -921,38 +762,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiInitStorageConstMeta => TaskConstMeta get kCrateApiInitStorageConstMeta =>
const TaskConstMeta(debugName: "init_storage", argNames: ["dir"]); const TaskConstMeta(debugName: "init_storage", argNames: ["dir"]);
@override
Stream<double> crateApiInputLevelStream() {
final sink = RustStreamSink<double>();
unawaited(
handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_StreamSink_f_32_Sse(sink, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 26,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_unit,
decodeErrorData: sse_decode_bridge_error,
),
constMeta: kCrateApiInputLevelStreamConstMeta,
argValues: [sink],
apiImpl: this,
),
),
);
return sink.stream;
}
TaskConstMeta get kCrateApiInputLevelStreamConstMeta =>
const TaskConstMeta(debugName: "input_level_stream", argNames: ["sink"]);
@override @override
Future<bool> crateApiIsConnected() { Future<bool> crateApiIsConnected() {
return handler.executeNormal( return handler.executeNormal(
@@ -962,7 +771,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 27, funcId: 21,
port: port_, port: port_,
); );
}, },
@@ -989,7 +798,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 28, funcId: 22,
port: port_, port: port_,
); );
}, },
@@ -1016,7 +825,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 29, funcId: 23,
port: port_, port: port_,
); );
}, },
@@ -1040,7 +849,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
SyncTask( SyncTask(
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 24)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_String, decodeSuccessData: sse_decode_String,
@@ -1070,7 +879,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 31, funcId: 25,
port: port_, port: port_,
); );
}, },
@@ -1100,7 +909,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 32, funcId: 26,
port: port_, port: port_,
); );
}, },
@@ -1127,7 +936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 33, funcId: 27,
port: port_, port: port_,
); );
}, },
@@ -1152,7 +961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_String(state, serializer); sse_encode_String(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 34)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -1185,7 +994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 35, funcId: 29,
port: port_, port: port_,
); );
}, },
@@ -1212,7 +1021,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_audio_route(route, serializer); sse_encode_bridge_audio_route(route, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 36)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -1246,7 +1055,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 37, funcId: 31,
port: port_, port: port_,
); );
}, },
@@ -1281,7 +1090,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 38, funcId: 32,
port: port_, port: port_,
); );
}, },
@@ -1311,7 +1120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 39, funcId: 33,
port: port_, port: port_,
); );
}, },
@@ -1339,7 +1148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 40, funcId: 34,
port: port_, port: port_,
); );
}, },
@@ -1367,7 +1176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 41, funcId: 35,
port: port_, port: port_,
); );
}, },
@@ -1397,7 +1206,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 42, funcId: 36,
port: port_, port: port_,
); );
}, },
@@ -1425,7 +1234,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
callFfi: () { callFfi: () {
final serializer = SseSerializer(generalizedFrbRustBinding); final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_bridge_network_state(state, serializer); sse_encode_bridge_network_state(state, serializer);
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 43)!; return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!;
}, },
codec: SseCodec( codec: SseCodec(
decodeSuccessData: sse_decode_unit, decodeSuccessData: sse_decode_unit,
@@ -1451,7 +1260,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 44, funcId: 38,
port: port_, port: port_,
); );
}, },
@@ -1479,7 +1288,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 45, funcId: 39,
port: port_, port: port_,
); );
}, },
@@ -1507,7 +1316,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 46, funcId: 40,
port: port_, port: port_,
); );
}, },
@@ -1535,7 +1344,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 47, funcId: 41,
port: port_, port: port_,
); );
}, },
@@ -1567,7 +1376,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 48, funcId: 42,
port: port_, port: port_,
); );
}, },
@@ -1597,7 +1406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 49, funcId: 43,
port: port_, port: port_,
); );
}, },
@@ -1625,7 +1434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 50, funcId: 44,
port: port_, port: port_,
); );
}, },
@@ -1653,7 +1462,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 51, funcId: 45,
port: port_, port: port_,
); );
}, },
@@ -1680,7 +1489,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 52, funcId: 46,
port: port_, port: port_,
); );
}, },
@@ -1708,7 +1517,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 53, funcId: 47,
port: port_, port: port_,
); );
}, },
@@ -1740,7 +1549,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 54, funcId: 48,
port: port_, port: port_,
); );
}, },
@@ -1769,7 +1578,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 55, funcId: 49,
port: port_, port: port_,
); );
}, },
@@ -1801,12 +1610,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError(); throw UnimplementedError();
} }
@protected
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
throw UnimplementedError();
}
@protected @protected
String dco_decode_String(dynamic raw) { String dco_decode_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1840,12 +1643,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_bridge_message_target(raw); return dco_decode_bridge_message_target(raw);
} }
@protected
BridgePokeStrength dco_decode_box_autoadd_bridge_poke_strength(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_bridge_poke_strength(raw);
}
@protected @protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code( BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw, dynamic raw,
@@ -1984,13 +1781,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) { BridgeAudioStats dco_decode_bridge_audio_stats(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 4) if (arr.length != 3)
throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
return BridgeAudioStats( return BridgeAudioStats(
framesSent: dco_decode_u_32(arr[0]), framesSent: dco_decode_u_32(arr[0]),
framesReceived: dco_decode_u_32(arr[1]), framesReceived: dco_decode_u_32(arr[1]),
pttActive: dco_decode_bool(arr[2]), pttActive: dco_decode_bool(arr[2]),
inputLevel: dco_decode_f_32(arr[3]),
); );
} }
@@ -2170,7 +1966,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
senderName: dco_decode_String(raw[2]), senderName: dco_decode_String(raw[2]),
message: dco_decode_String(raw[3]), message: dco_decode_String(raw[3]),
target: dco_decode_box_autoadd_bridge_message_target(raw[4]), target: dco_decode_box_autoadd_bridge_message_target(raw[4]),
pokeStrength: dco_decode_opt_box_autoadd_bridge_poke_strength(raw[5]),
); );
case 11: case 11:
return BridgeEvent_ServerActivity(message: dco_decode_String(raw[1])); return BridgeEvent_ServerActivity(message: dco_decode_String(raw[1]));
@@ -2262,12 +2057,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return BridgeNetworkState.values[raw as int]; return BridgeNetworkState.values[raw as int];
} }
@protected
BridgePokeStrength dco_decode_bridge_poke_strength(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return BridgePokeStrength.values[raw as int];
}
@protected @protected
BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw) { BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2404,16 +2193,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_String(raw); return raw == null ? null : dco_decode_String(raw);
} }
@protected
BridgePokeStrength? dco_decode_opt_box_autoadd_bridge_poke_strength(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null
? null
: dco_decode_box_autoadd_bridge_poke_strength(raw);
}
@protected @protected
BridgeVoiceJoinErrorCode? BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw) { dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw) {
@@ -2447,12 +2226,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw == null ? null : dco_decode_box_autoadd_u_64(raw); return raw == null ? null : dco_decode_box_autoadd_u_64(raw);
} }
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null ? null : dco_decode_list_prim_u_8_strict(raw);
}
@protected @protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw) { PermissionStateKind dco_decode_permission_state_kind(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2498,14 +2271,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError('Unreachable ()'); throw UnimplementedError('Unreachable ()');
} }
@protected
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
throw UnimplementedError('Unreachable ()');
}
@protected @protected
String sse_decode_String(SseDeserializer deserializer) { String sse_decode_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -2544,14 +2309,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_bridge_message_target(deserializer)); return (sse_decode_bridge_message_target(deserializer));
} }
@protected
BridgePokeStrength sse_decode_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_bridge_poke_strength(deserializer));
}
@protected @protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code( BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer, SseDeserializer deserializer,
@@ -2731,12 +2488,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_framesSent = sse_decode_u_32(deserializer); var var_framesSent = sse_decode_u_32(deserializer);
var var_framesReceived = sse_decode_u_32(deserializer); var var_framesReceived = sse_decode_u_32(deserializer);
var var_pttActive = sse_decode_bool(deserializer); var var_pttActive = sse_decode_bool(deserializer);
var var_inputLevel = sse_decode_f_32(deserializer);
return BridgeAudioStats( return BridgeAudioStats(
framesSent: var_framesSent, framesSent: var_framesSent,
framesReceived: var_framesReceived, framesReceived: var_framesReceived,
pttActive: var_pttActive, pttActive: var_pttActive,
inputLevel: var_inputLevel,
); );
} }
@@ -3003,15 +2758,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_target = sse_decode_box_autoadd_bridge_message_target( var var_target = sse_decode_box_autoadd_bridge_message_target(
deserializer, deserializer,
); );
var var_pokeStrength = sse_decode_opt_box_autoadd_bridge_poke_strength(
deserializer,
);
return BridgeEvent_ChatMessage( return BridgeEvent_ChatMessage(
senderId: var_senderId, senderId: var_senderId,
senderName: var_senderName, senderName: var_senderName,
message: var_message, message: var_message,
target: var_target, target: var_target,
pokeStrength: var_pokeStrength,
); );
case 11: case 11:
var var_message = sse_decode_String(deserializer); var var_message = sse_decode_String(deserializer);
@@ -3139,15 +2890,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return BridgeNetworkState.values[inner]; return BridgeNetworkState.values[inner];
} }
@protected
BridgePokeStrength sse_decode_bridge_poke_strength(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_i_32(deserializer);
return BridgePokeStrength.values[inner];
}
@protected @protected
BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer) { BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -3339,19 +3081,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
BridgePokeStrength? sse_decode_opt_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_box_autoadd_bridge_poke_strength(deserializer));
} else {
return null;
}
}
@protected @protected
BridgeVoiceJoinErrorCode? BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code( sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
@@ -3412,17 +3141,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_list_prim_u_8_strict(deserializer));
} else {
return null;
}
}
@protected @protected
PermissionStateKind sse_decode_permission_state_kind( PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer, SseDeserializer deserializer,
@@ -3481,23 +3199,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
} }
@protected
void sse_encode_StreamSink_f_32_Sse(
RustStreamSink<double> self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(
self.setupAndSerialize(
codec: SseCodec(
decodeSuccessData: sse_decode_f_32,
decodeErrorData: sse_decode_AnyhowException,
),
),
serializer,
);
}
@protected @protected
void sse_encode_String(String self, SseSerializer serializer) { void sse_encode_String(String self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -3537,15 +3238,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_bridge_message_target(self, serializer); sse_encode_bridge_message_target(self, serializer);
} }
@protected
void sse_encode_box_autoadd_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bridge_poke_strength(self, serializer);
}
@protected @protected
void sse_encode_box_autoadd_bridge_voice_join_error_code( void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self, BridgeVoiceJoinErrorCode self,
@@ -3688,7 +3380,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_u_32(self.framesSent, serializer); sse_encode_u_32(self.framesSent, serializer);
sse_encode_u_32(self.framesReceived, serializer); sse_encode_u_32(self.framesReceived, serializer);
sse_encode_bool(self.pttActive, serializer); sse_encode_bool(self.pttActive, serializer);
sse_encode_f_32(self.inputLevel, serializer);
} }
@protected @protected
@@ -3884,17 +3575,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
senderName: final senderName, senderName: final senderName,
message: final message, message: final message,
target: final target, target: final target,
pokeStrength: final pokeStrength,
): ):
sse_encode_i_32(10, serializer); sse_encode_i_32(10, serializer);
sse_encode_u_64(senderId, serializer); sse_encode_u_64(senderId, serializer);
sse_encode_String(senderName, serializer); sse_encode_String(senderName, serializer);
sse_encode_String(message, serializer); sse_encode_String(message, serializer);
sse_encode_box_autoadd_bridge_message_target(target, serializer); sse_encode_box_autoadd_bridge_message_target(target, serializer);
sse_encode_opt_box_autoadd_bridge_poke_strength(
pokeStrength,
serializer,
);
case BridgeEvent_ServerActivity(message: final message): case BridgeEvent_ServerActivity(message: final message):
sse_encode_i_32(11, serializer); sse_encode_i_32(11, serializer);
sse_encode_String(message, serializer); sse_encode_String(message, serializer);
@@ -4016,15 +3702,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(self.index, serializer); sse_encode_i_32(self.index, serializer);
} }
@protected
void sse_encode_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.index, serializer);
}
@protected @protected
void sse_encode_bridge_ptt_binding( void sse_encode_bridge_ptt_binding(
BridgePttBinding self, BridgePttBinding self,
@@ -4201,19 +3878,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
void sse_encode_opt_box_autoadd_bridge_poke_strength(
BridgePokeStrength? self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_box_autoadd_bridge_poke_strength(self, serializer);
}
}
@protected @protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code( void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self, BridgeVoiceJoinErrorCode? self,
@@ -4270,19 +3934,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_list_prim_u_8_strict(self, serializer);
}
}
@protected @protected
void sse_encode_permission_state_kind( void sse_encode_permission_state_kind(
PermissionStateKind self, PermissionStateKind self,
@@ -27,9 +27,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@@ -46,9 +43,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BridgeMessageTarget dco_decode_box_autoadd_bridge_message_target(dynamic raw); BridgeMessageTarget dco_decode_box_autoadd_bridge_message_target(dynamic raw);
@protected
BridgePokeStrength dco_decode_box_autoadd_bridge_poke_strength(dynamic raw);
@protected @protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code( BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw, dynamic raw,
@@ -123,9 +117,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw); BridgeNetworkState dco_decode_bridge_network_state(dynamic raw);
@protected
BridgePokeStrength dco_decode_bridge_poke_strength(dynamic raw);
@protected @protected
BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw); BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw);
@@ -180,11 +171,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String? dco_decode_opt_String(dynamic raw); String? dco_decode_opt_String(dynamic raw);
@protected
BridgePokeStrength? dco_decode_opt_box_autoadd_bridge_poke_strength(
dynamic raw,
);
@protected @protected
BridgeVoiceJoinErrorCode? BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw); dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw);
@@ -201,9 +187,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected @protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw); PermissionStateKind dco_decode_permission_state_kind(dynamic raw);
@@ -227,11 +210,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
SseDeserializer deserializer,
);
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@@ -254,11 +232,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
BridgePokeStrength sse_decode_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected @protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code( BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer, SseDeserializer deserializer,
@@ -347,11 +320,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
BridgePokeStrength sse_decode_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected @protected
BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer); BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer);
@@ -424,11 +392,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String? sse_decode_opt_String(SseDeserializer deserializer); String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
BridgePokeStrength? sse_decode_opt_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected @protected
BridgeVoiceJoinErrorCode? BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code( sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
@@ -447,9 +410,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected @protected
PermissionStateKind sse_decode_permission_state_kind( PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer, SseDeserializer deserializer,
@@ -479,12 +439,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_StreamSink_f_32_Sse(
RustStreamSink<double> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_String(String self, SseSerializer serializer); void sse_encode_String(String self, SseSerializer serializer);
@@ -509,12 +463,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_bridge_voice_join_error_code( void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self, BridgeVoiceJoinErrorCode self,
@@ -626,12 +574,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_bridge_ptt_binding( void sse_encode_bridge_ptt_binding(
BridgePttBinding self, BridgePttBinding self,
@@ -725,12 +667,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_opt_String(String? self, SseSerializer serializer); void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_bridge_poke_strength(
BridgePokeStrength? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code( void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self, BridgeVoiceJoinErrorCode? self,
@@ -752,12 +688,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_permission_state_kind( void sse_encode_permission_state_kind(
PermissionStateKind self, PermissionStateKind self,
@@ -29,9 +29,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
RustStreamSink<double> dco_decode_StreamSink_f_32_Sse(dynamic raw);
@protected @protected
String dco_decode_String(dynamic raw); String dco_decode_String(dynamic raw);
@@ -48,9 +45,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BridgeMessageTarget dco_decode_box_autoadd_bridge_message_target(dynamic raw); BridgeMessageTarget dco_decode_box_autoadd_bridge_message_target(dynamic raw);
@protected
BridgePokeStrength dco_decode_box_autoadd_bridge_poke_strength(dynamic raw);
@protected @protected
BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code( BridgeVoiceJoinErrorCode dco_decode_box_autoadd_bridge_voice_join_error_code(
dynamic raw, dynamic raw,
@@ -125,9 +119,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BridgeNetworkState dco_decode_bridge_network_state(dynamic raw); BridgeNetworkState dco_decode_bridge_network_state(dynamic raw);
@protected
BridgePokeStrength dco_decode_bridge_poke_strength(dynamic raw);
@protected @protected
BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw); BridgePttBinding dco_decode_bridge_ptt_binding(dynamic raw);
@@ -182,11 +173,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String? dco_decode_opt_String(dynamic raw); String? dco_decode_opt_String(dynamic raw);
@protected
BridgePokeStrength? dco_decode_opt_box_autoadd_bridge_poke_strength(
dynamic raw,
);
@protected @protected
BridgeVoiceJoinErrorCode? BridgeVoiceJoinErrorCode?
dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw); dco_decode_opt_box_autoadd_bridge_voice_join_error_code(dynamic raw);
@@ -203,9 +189,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected @protected
PermissionStateKind dco_decode_permission_state_kind(dynamic raw); PermissionStateKind dco_decode_permission_state_kind(dynamic raw);
@@ -229,11 +212,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
RustStreamSink<double> sse_decode_StreamSink_f_32_Sse(
SseDeserializer deserializer,
);
@protected @protected
String sse_decode_String(SseDeserializer deserializer); String sse_decode_String(SseDeserializer deserializer);
@@ -256,11 +234,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
BridgePokeStrength sse_decode_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected @protected
BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code( BridgeVoiceJoinErrorCode sse_decode_box_autoadd_bridge_voice_join_error_code(
SseDeserializer deserializer, SseDeserializer deserializer,
@@ -349,11 +322,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
BridgePokeStrength sse_decode_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected @protected
BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer); BridgePttBinding sse_decode_bridge_ptt_binding(SseDeserializer deserializer);
@@ -426,11 +394,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
String? sse_decode_opt_String(SseDeserializer deserializer); String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
BridgePokeStrength? sse_decode_opt_box_autoadd_bridge_poke_strength(
SseDeserializer deserializer,
);
@protected @protected
BridgeVoiceJoinErrorCode? BridgeVoiceJoinErrorCode?
sse_decode_opt_box_autoadd_bridge_voice_join_error_code( sse_decode_opt_box_autoadd_bridge_voice_join_error_code(
@@ -449,9 +412,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected @protected
PermissionStateKind sse_decode_permission_state_kind( PermissionStateKind sse_decode_permission_state_kind(
SseDeserializer deserializer, SseDeserializer deserializer,
@@ -481,12 +441,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_StreamSink_f_32_Sse(
RustStreamSink<double> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_String(String self, SseSerializer serializer); void sse_encode_String(String self, SseSerializer serializer);
@@ -511,12 +465,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_bridge_voice_join_error_code( void sse_encode_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode self, BridgeVoiceJoinErrorCode self,
@@ -628,12 +576,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_bridge_poke_strength(
BridgePokeStrength self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_bridge_ptt_binding( void sse_encode_bridge_ptt_binding(
BridgePttBinding self, BridgePttBinding self,
@@ -727,12 +669,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_opt_String(String? self, SseSerializer serializer); void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_bridge_poke_strength(
BridgePokeStrength? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_bridge_voice_join_error_code( void sse_encode_opt_box_autoadd_bridge_voice_join_error_code(
BridgeVoiceJoinErrorCode? self, BridgeVoiceJoinErrorCode? self,
@@ -754,12 +690,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_permission_state_kind( void sse_encode_permission_state_kind(
PermissionStateKind self, PermissionStateKind self,
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../design/breakpoints.dart';
/// Semantic tones for lightweight, Material 3 SnackBars. /// Semantic tones for lightweight, Material 3 SnackBars.
enum AppSnackBarVariant { neutral, success, warning, error } enum AppSnackBarVariant { neutral, success, warning, error }
@@ -9,6 +7,7 @@ enum AppSnackBarVariant { neutral, success, warning, error }
class AppSnackBar { class AppSnackBar {
const AppSnackBar._(); const AppSnackBar._();
static const double _desktopMaxWidth = 560;
static const double _radius = 16; static const double _radius = 16;
static const double _elevation = 3; static const double _elevation = 3;
@@ -40,9 +39,9 @@ class AppSnackBar {
}) { }) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final viewWidth = MediaQuery.sizeOf(context).width; final viewWidth = MediaQuery.sizeOf(context).width;
final useDesktopCap = viewWidth >= ChanoraBreakpoints.medium; final useDesktopCap = viewWidth >= 600;
final snackBarWidth = useDesktopCap && margin == null final snackBarWidth = useDesktopCap && margin == null
? ChanoraBreakpoints.snackBarDesktopCap ? _desktopMaxWidth
: null; : null;
final effectiveMargin = useDesktopCap && margin != null final effectiveMargin = useDesktopCap && margin != null
? _desktopCappedMargin(context, margin) ? _desktopCappedMargin(context, margin)
@@ -77,11 +76,7 @@ class AppSnackBar {
final viewWidth = MediaQuery.sizeOf(context).width; final viewWidth = MediaQuery.sizeOf(context).width;
final resolved = margin.resolve(Directionality.of(context)); final resolved = margin.resolve(Directionality.of(context));
final extraHorizontal = final extraHorizontal =
(viewWidth - ChanoraBreakpoints.snackBarDesktopCap).clamp( (viewWidth - _desktopMaxWidth).clamp(0.0, viewWidth) / 2;
0.0,
viewWidth,
) /
2;
return EdgeInsets.fromLTRB( return EdgeInsets.fromLTRB(
resolved.left + extraHorizontal, resolved.left + extraHorizontal,
resolved.top, resolved.top,
@@ -27,7 +27,6 @@ class AudioDeviceListTile extends StatefulWidget {
AudioDeviceListLoader? loadDevices, AudioDeviceListLoader? loadDevices,
AudioDeviceSetter? setInputDevice, AudioDeviceSetter? setInputDevice,
AudioDeviceSetter? setOutputDevice, AudioDeviceSetter? setOutputDevice,
this.onDeviceChanged,
}) : loadDevices = loadDevices ?? rust.listAudioDevices, }) : loadDevices = loadDevices ?? rust.listAudioDevices,
setInputDevice = setInputDevice ?? rust.setInputDevice, setInputDevice = setInputDevice ?? rust.setInputDevice,
setOutputDevice = setOutputDevice ?? rust.setOutputDevice; setOutputDevice = setOutputDevice ?? rust.setOutputDevice;
@@ -47,10 +46,6 @@ class AudioDeviceListTile extends StatefulWidget {
/// Selects an output device. /// Selects an output device.
final AudioDeviceSetter setOutputDevice; final AudioDeviceSetter setOutputDevice;
/// Called after a device selection succeeds. Receives the device id
/// (null for system default).
final ValueChanged<String?>? onDeviceChanged;
@override @override
State<AudioDeviceListTile> createState() => _AudioDeviceListTileState(); State<AudioDeviceListTile> createState() => _AudioDeviceListTileState();
} }
@@ -93,7 +88,6 @@ class _AudioDeviceListTileState extends State<AudioDeviceListTile> {
setState(() { setState(() {
_selectedDeviceId = deviceId; _selectedDeviceId = deviceId;
}); });
widget.onDeviceChanged?.call(deviceId);
final selectedName = _selectedDevice?.name ?? 'System default'; final selectedName = _selectedDevice?.name ?? 'System default';
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -93,51 +93,78 @@ class AudioProcessingConfigState {
isLinux: linux, isLinux: linux,
); );
rust.BridgeAudioBackend processingBackend;
rust.BridgeEffectOwner aec;
rust.BridgeEffectOwner ns;
rust.BridgeEffectOwner agc;
if (android) { if (android) {
final owner = preferHardware final owner = preferHardware
? rust.BridgeEffectOwner.platform ? rust.BridgeEffectOwner.platform
: rust.BridgeEffectOwner.webrtcApm; : rust.BridgeEffectOwner.webrtcApm;
processingBackend = preferHardware return rust.BridgeAudioProcessingConfig(
route: base.route,
iosMode: normalizedIosProcessingMode(iosMode),
processingBackend: preferHardware
? rust.BridgeAudioBackend.platformVoiceProcessing ? rust.BridgeAudioBackend.platformVoiceProcessing
: rust.BridgeAudioBackend.webrtcApm; : rust.BridgeAudioBackend.webrtcApm,
aec = aecEnabled ? owner : rust.BridgeEffectOwner.off; vadBackend: vad,
ns = nsEnabled ? owner : rust.BridgeEffectOwner.off; aec: aecEnabled ? owner : rust.BridgeEffectOwner.off,
agc = agcEnabled ? owner : rust.BridgeEffectOwner.off; ns: nsEnabled ? owner : rust.BridgeEffectOwner.off,
} else if (appleVoiceProcessing) { agc: agcEnabled ? owner : rust.BridgeEffectOwner.off,
processingBackend = rust.BridgeAudioBackend.platformVoiceProcessing; hpfEnabled: hpfEnabled,
aec = rust.BridgeEffectOwner.platform; limiterEnabled: limiterEnabled,
ns = rust.BridgeEffectOwner.platform; vadHangoverMs: base.vadHangoverMs,
agc = rust.BridgeEffectOwner.platform; vadPreRollMs: base.vadPreRollMs,
} else if (desktopWebrtcApm) { vadMinTxMs: base.vadMinTxMs,
debugWavDumpEnabled: debugWavDump,
);
}
if (appleVoiceProcessing) {
return rust.BridgeAudioProcessingConfig(
route: base.route,
iosMode: normalizedIosProcessingMode(iosMode),
processingBackend: rust.BridgeAudioBackend.platformVoiceProcessing,
vadBackend: vad,
aec: rust.BridgeEffectOwner.platform,
ns: rust.BridgeEffectOwner.platform,
agc: rust.BridgeEffectOwner.platform,
hpfEnabled: hpfEnabled,
limiterEnabled: limiterEnabled,
vadHangoverMs: base.vadHangoverMs,
vadPreRollMs: base.vadPreRollMs,
vadMinTxMs: base.vadMinTxMs,
debugWavDumpEnabled: debugWavDump,
);
}
if (desktopWebrtcApm) {
final owner = rust.BridgeEffectOwner.webrtcApm; final owner = rust.BridgeEffectOwner.webrtcApm;
processingBackend = rust.BridgeAudioBackend.webrtcApm; return rust.BridgeAudioProcessingConfig(
aec = aecEnabled ? owner : rust.BridgeEffectOwner.off; route: base.route,
ns = nsEnabled ? owner : rust.BridgeEffectOwner.off; iosMode: normalizedIosProcessingMode(iosMode),
agc = agcEnabled ? owner : rust.BridgeEffectOwner.off; processingBackend: rust.BridgeAudioBackend.webrtcApm,
} else { vadBackend: vad,
processingBackend = rust.BridgeAudioBackend.platformVoiceProcessing; aec: aecEnabled ? owner : rust.BridgeEffectOwner.off,
aec = rust.BridgeEffectOwner.platform; ns: nsEnabled ? owner : rust.BridgeEffectOwner.off,
ns = nsEnabled agc: agcEnabled ? owner : rust.BridgeEffectOwner.off,
? rust.BridgeEffectOwner.platform hpfEnabled: hpfEnabled,
: rust.BridgeEffectOwner.off; limiterEnabled: limiterEnabled,
agc = agcEnabled vadHangoverMs: base.vadHangoverMs,
? rust.BridgeEffectOwner.platform vadPreRollMs: base.vadPreRollMs,
: rust.BridgeEffectOwner.off; vadMinTxMs: base.vadMinTxMs,
debugWavDumpEnabled: debugWavDump,
);
} }
return rust.BridgeAudioProcessingConfig( return rust.BridgeAudioProcessingConfig(
route: base.route, route: base.route,
iosMode: normalizedIosProcessingMode(iosMode), iosMode: normalizedIosProcessingMode(iosMode),
processingBackend: processingBackend, processingBackend: rust.BridgeAudioBackend.platformVoiceProcessing,
vadBackend: vad, vadBackend: vad,
aec: aec, aec: rust.BridgeEffectOwner.platform,
ns: ns, ns: nsEnabled
agc: agc, ? rust.BridgeEffectOwner.platform
: rust.BridgeEffectOwner.off,
agc: agcEnabled
? rust.BridgeEffectOwner.platform
: rust.BridgeEffectOwner.off,
hpfEnabled: hpfEnabled, hpfEnabled: hpfEnabled,
limiterEnabled: limiterEnabled, limiterEnabled: limiterEnabled,
vadHangoverMs: base.vadHangoverMs, vadHangoverMs: base.vadHangoverMs,
@@ -1,88 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
import 'package:flutter/material.dart';
import '../design/breakpoints.dart';
import '../l10n/generated/app_localizations.dart';
import '../services/snapshot_state_mapper.dart';
import '../services/ts3_server_link.dart';
import '../src/rust/api.dart' as rust;
import 'chat_views.dart';
/// Fixed-width inline chat panel for expanded desktop layouts.
class ChatPanel extends StatelessWidget {
/// Construct an inline chat panel.
const ChatPanel({
super.key,
required this.messages,
required this.snapshot,
required this.target,
required this.clientName,
required this.onClose,
this.restoredDraft,
this.onDraftChanged,
this.onTs3ServerLink,
});
/// Backing chat messages shared with the chat route.
final List<ChatEntry> messages;
/// Latest TeamSpeak snapshot.
final rust.BridgeSnapshot snapshot;
/// Chat target shown in the panel.
final rust.BridgeMessageTarget target;
/// Client display name for direct-message and poke targets.
final String clientName;
/// Handle TeamSpeak server links embedded in chat messages.
final Ts3ServerLinkHandler? onTs3ServerLink;
/// Called when the user closes the inline panel.
final VoidCallback onClose;
/// External draft text to restore in the chat detail view.
final String? restoredDraft;
/// Called when the draft text changes.
final ValueChanged<String>? onDraftChanged;
@override
Widget build(BuildContext context) {
final currentChannelId = ownClientSnapshotState(snapshot)?.channelId;
final channelName = snapshotChannelName(snapshot, currentChannelId);
final l10n = AppL10n.of(context);
return SizedBox(
width: ChanoraBreakpoints.chatPanelWidth,
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: BorderDirectional(
start: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
),
child: ChatDetailView(
messages: messages,
snapshot: snapshot,
target: target,
clientName: clientName,
currentChannelId: currentChannelId,
channelName: channelName,
onTs3ServerLink: onTs3ServerLink,
restoredDraft: restoredDraft,
onDraftChanged: onDraftChanged,
messageMaxWidth: 500,
headerTrailing: IconButton(
tooltip: l10n.chatCloseAction,
icon: const Icon(Icons.close),
onPressed: onClose,
),
),
),
);
}
}
+14 -111
View File
@@ -2,7 +2,6 @@ import 'dart:async' show unawaited;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../design/breakpoints.dart';
import '../l10n/generated/app_localizations.dart'; import '../l10n/generated/app_localizations.dart';
import '../services/channel_spacer.dart'; import '../services/channel_spacer.dart';
import '../services/link_trust_service.dart'; import '../services/link_trust_service.dart';
@@ -14,12 +13,7 @@ import 'bbcode_text.dart';
const double _chatSidebarTileExtent = 92; const double _chatSidebarTileExtent = 92;
const double _chatSidebarCompactTileExtent = 76; const double _chatSidebarCompactTileExtent = 76;
const double _chatSidebarCompactHeight = 84; const double _chatSidebarCompactHeight = 84;
const double _chatMobileBreakpoint = 600;
typedef ChatMessageSender =
Future<void> Function({
required String message,
required rust.BridgeMessageTarget target,
});
/// One chat/activity message shown in the chat hub. /// One chat/activity message shown in the chat hub.
class ChatEntry { class ChatEntry {
@@ -503,7 +497,7 @@ String chatInputPlaceholder(
case rust.BridgeMessageTarget_Client(): case rust.BridgeMessageTarget_Client():
return 'Message $clientName...'; return 'Message $clientName...';
case rust.BridgeMessageTarget_Poke(): case rust.BridgeMessageTarget_Poke():
return 'Poke message optional...'; return 'Poke message...';
} }
} }
@@ -519,16 +513,6 @@ bool canSendToChatTarget(
} }
} }
bool canSendChatMessage(
rust.BridgeMessageTarget target,
BigInt? currentChannelId,
String text,
) {
if (!canSendToChatTarget(target, currentChannelId)) return false;
if (target is rust.BridgeMessageTarget_Poke) return true;
return text.trim().isNotEmpty;
}
String? chatSendBlockedReason( String? chatSendBlockedReason(
rust.BridgeMessageTarget target, rust.BridgeMessageTarget target,
BigInt? currentChannelId, BigInt? currentChannelId,
@@ -632,7 +616,7 @@ class _ChatPageState extends State<ChatPage> {
final currentChannelId = _currentChannelId; final currentChannelId = _currentChannelId;
final channelName = snapshotChannelName(snapshot, currentChannelId); final channelName = snapshotChannelName(snapshot, currentChannelId);
final l10n = AppL10n.of(context); final l10n = AppL10n.of(context);
final detail = ChatDetailView( final detail = _ChatDetailView(
target: _selectedTarget, target: _selectedTarget,
clientName: _selectedClientName, clientName: _selectedClientName,
snapshot: snapshot, snapshot: snapshot,
@@ -657,7 +641,7 @@ class _ChatPageState extends State<ChatPage> {
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final sidebar = _ChatSidebar( final sidebar = _ChatSidebar(
compact: constraints.maxWidth < ChanoraBreakpoints.medium, compact: constraints.maxWidth < _chatMobileBreakpoint,
selectedTarget: _selectedTarget, selectedTarget: _selectedTarget,
privateChats: _privateChats, privateChats: _privateChats,
onSelect: _selectTarget, onSelect: _selectTarget,
@@ -666,7 +650,7 @@ class _ChatPageState extends State<ChatPage> {
_selectTarget(rust.BridgeMessageTarget.client(id), name: name); _selectTarget(rust.BridgeMessageTarget.client(id), name: name);
}), }),
); );
if (constraints.maxWidth < ChanoraBreakpoints.medium) { if (constraints.maxWidth < _chatMobileBreakpoint) {
return Column( return Column(
children: [ children: [
sidebar, sidebar,
@@ -1066,11 +1050,8 @@ class _ChannelGroup extends StatelessWidget {
} }
} }
/// Detail view for a single chat target, including message history and input. class _ChatDetailView extends StatefulWidget {
class ChatDetailView extends StatefulWidget { const _ChatDetailView({
/// Construct a chat detail view.
const ChatDetailView({
super.key,
required this.target, required this.target,
required this.clientName, required this.clientName,
required this.snapshot, required this.snapshot,
@@ -1078,54 +1059,21 @@ class ChatDetailView extends StatefulWidget {
required this.currentChannelId, required this.currentChannelId,
required this.channelName, required this.channelName,
this.onTs3ServerLink, this.onTs3ServerLink,
this.headerTrailing,
this.messageMaxWidth,
this.restoredDraft,
this.onDraftChanged,
this.sendChatMessage,
}); });
/// Chat target displayed by this detail view.
final rust.BridgeMessageTarget target; final rust.BridgeMessageTarget target;
/// Client display name for direct-message and poke targets.
final String clientName; final String clientName;
/// Latest TeamSpeak snapshot.
final rust.BridgeSnapshot snapshot; final rust.BridgeSnapshot snapshot;
/// Backing message list. Self-sent messages are appended here.
final List<ChatEntry> messages; final List<ChatEntry> messages;
/// Current voice channel id for channel-chat send gating.
final BigInt? currentChannelId; final BigInt? currentChannelId;
/// Current voice channel name for labels and placeholders.
final String channelName; final String channelName;
/// Handle TeamSpeak server links embedded in chat messages.
final Ts3ServerLinkHandler? onTs3ServerLink; final Ts3ServerLinkHandler? onTs3ServerLink;
/// Optional widget shown at the trailing edge of the header.
final Widget? headerTrailing;
/// Optional max width for message content.
final double? messageMaxWidth;
/// External draft text to restore when the widget initializes or the target changes.
final String? restoredDraft;
/// Called with the current draft text whenever the target changes or the widget is about to be replaced.
final ValueChanged<String>? onDraftChanged;
/// Sends a chat message. Defaults to the Rust bridge send path.
final ChatMessageSender? sendChatMessage;
@override @override
State<ChatDetailView> createState() => _ChatDetailViewState(); State<_ChatDetailView> createState() => _ChatDetailViewState();
} }
class _ChatDetailViewState extends State<ChatDetailView> { class _ChatDetailViewState extends State<_ChatDetailView> {
final _textCtl = TextEditingController(); final _textCtl = TextEditingController();
final _scrollCtl = ScrollController(); final _scrollCtl = ScrollController();
int _lastRenderedMessageCount = -1; int _lastRenderedMessageCount = -1;
@@ -1152,35 +1100,8 @@ class _ChatDetailViewState extends State<ChatDetailView> {
clientName: widget.clientName, clientName: widget.clientName,
); );
@override
void initState() {
super.initState();
if (widget.restoredDraft != null && widget.restoredDraft!.isNotEmpty) {
_textCtl.text = widget.restoredDraft!;
}
}
@override
void didUpdateWidget(covariant ChatDetailView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.target != widget.target) {
// Propagate the OUTGOING draft unconditionally, including empty
// text. The empty case is load-bearing: if the user typed text,
// saved it, restored it, then deleted everything, the parent
// map must learn the draft is now empty — otherwise the stale
// entry resurrects on the next target swap.
oldWidget.onDraftChanged?.call(_textCtl.text);
_textCtl.text = widget.restoredDraft ?? '';
_lastRenderedTarget = null;
}
}
@override @override
void dispose() { void dispose() {
// Same unconditional flush on tear-down. The `isNotEmpty` guard
// here would silently drop "user cleared the field then closed
// the panel" into the same stale-entry bug class as didUpdateWidget.
widget.onDraftChanged?.call(_textCtl.text);
_textCtl.dispose(); _textCtl.dispose();
_scrollCtl.dispose(); _scrollCtl.dispose();
super.dispose(); super.dispose();
@@ -1188,12 +1109,9 @@ class _ChatDetailViewState extends State<ChatDetailView> {
void _send() { void _send() {
final text = _textCtl.text.trim(); final text = _textCtl.text.trim();
if (!canSendChatMessage(widget.target, widget.currentChannelId, text)) { if (text.isEmpty || !_canSend) return;
return;
}
_textCtl.clear(); _textCtl.clear();
final sendChatMessage = widget.sendChatMessage ?? rust.sendChatMessage; unawaited(rust.sendChatMessage(message: text, target: widget.target));
unawaited(sendChatMessage(message: text, target: widget.target));
final ownId = widget.snapshot.ownClientId; final ownId = widget.snapshot.ownClientId;
setState(() { setState(() {
widget.messages.add( widget.messages.add(
@@ -1246,9 +1164,6 @@ class _ChatDetailViewState extends State<ChatDetailView> {
channelName: widget.channelName, channelName: widget.channelName,
clientName: widget.clientName, clientName: widget.clientName,
); );
final sendTooltip = widget.target is rust.BridgeMessageTarget_Poke
? 'Poke'
: 'Send';
return Column( return Column(
children: [ children: [
@@ -1261,12 +1176,7 @@ class _ChatDetailViewState extends State<ChatDetailView> {
bottom: BorderSide(color: theme.colorScheme.outlineVariant), bottom: BorderSide(color: theme.colorScheme.outlineVariant),
), ),
), ),
child: Row( child: Text(_title, style: theme.textTheme.titleMedium),
children: [
Expanded(child: Text(_title, style: theme.textTheme.titleMedium)),
if (widget.headerTrailing != null) widget.headerTrailing!,
],
),
), ),
Expanded( Expanded(
child: msgs.isEmpty child: msgs.isEmpty
@@ -1306,19 +1216,12 @@ class _ChatDetailViewState extends State<ChatDetailView> {
controller: _scrollCtl, controller: _scrollCtl,
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: msgs.length, itemCount: msgs.length,
itemBuilder: (_, i) => Center( itemBuilder: (_, i) => _MessageBubble(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.messageMaxWidth ?? double.infinity,
),
child: _MessageBubble(
entry: msgs[i], entry: msgs[i],
onTs3ServerLink: widget.onTs3ServerLink, onTs3ServerLink: widget.onTs3ServerLink,
), ),
), ),
), ),
),
),
if (_blockedReason != null) if (_blockedReason != null)
Container( Container(
width: double.infinity, width: double.infinity,
@@ -1358,7 +1261,7 @@ class _ChatDetailViewState extends State<ChatDetailView> {
IconButton.filled( IconButton.filled(
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
onPressed: _send, onPressed: _send,
tooltip: sendTooltip, tooltip: 'Send',
), ),
], ],
), ),
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../design/breakpoints.dart';
import '../l10n/generated/app_localizations.dart'; import '../l10n/generated/app_localizations.dart';
import '../src/rust/api.dart' as rust; import '../src/rust/api.dart' as rust;
@@ -115,6 +114,7 @@ class _ConnectFormState extends State<ConnectForm> {
const SizedBox(height: 16), const SizedBox(height: 16),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
const stackedActionsMaxWidth = 400.0;
final connectButton = FilledButton.icon( final connectButton = FilledButton.icon(
icon: const Icon(Icons.login), icon: const Icon(Icons.login),
label: Text(l10n.connectAction), label: Text(l10n.connectAction),
@@ -125,8 +125,7 @@ class _ConnectFormState extends State<ConnectForm> {
label: Text(l10n.bookmarkAddAction), label: Text(l10n.bookmarkAddAction),
onPressed: widget.onAddBookmark, onPressed: widget.onAddBookmark,
); );
if (constraints.maxWidth <= if (constraints.maxWidth <= stackedActionsMaxWidth) {
ChanoraBreakpoints.connectActionsStackMaxWidth) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import '../l10n/generated/app_localizations.dart';
import '../services/poke_notification_service.dart';
import '../services/poke_preferences_service.dart';
import 'voice_settings_controls.dart';
class PokeNotificationSettingsDialog extends StatelessWidget {
const PokeNotificationSettingsDialog({
super.key,
required this.preferences,
required this.notificationService,
});
final PokePreferencesService preferences;
final PokeNotificationService notificationService;
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final theme = Theme.of(context);
return AlertDialog(
title: Text(l10n.pokeSettingsTitle),
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
content: SizedBox(
width: 400,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ValueListenableBuilder<bool>(
valueListenable: preferences.pokesEnabled,
builder: (context, enabled, _) => SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text(l10n.pokeSettingsEnableLabel),
subtitle: Text(l10n.pokeSettingsEnableDescription),
value: enabled,
onChanged: (value) async {
await preferences.setPokesEnabled(value);
// Request notification permission when enabling pokes
// so the OS prompt appears immediately rather than on
// the first poke event.
if (value) {
await notificationService.requestPermission();
}
},
),
),
const Divider(height: 24),
VoiceSectionHeader(l10n.pokeSettingsMutedSendersHeader),
ValueListenableBuilder<Set<BigInt>>(
valueListenable: preferences.mutedSenders,
builder: (context, mutedSenders, _) {
final senders = mutedSenders.toList()..sort();
if (senders.isEmpty) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Text(
l10n.pokeSettingsMutedSendersEmpty,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final senderId in senders)
ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.notifications_off_outlined),
title: Text(
l10n.pokeSettingsMutedSenderLabel(
senderId.toString(),
),
),
trailing: TextButton(
onPressed: () => preferences.unmuteSender(senderId),
child: Text(l10n.pokeSettingsUnmuteSenderAction),
),
),
],
);
},
),
const SizedBox(height: 8),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n.closeAction),
),
],
);
}
}
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../l10n/generated/app_localizations.dart'; import '../l10n/generated/app_localizations.dart';
import '../services/channel_spacer.dart'; import '../services/channel_spacer.dart';
@@ -11,14 +10,7 @@ import '../src/rust/api.dart' as rust;
import 'bbcode_text.dart'; import 'bbcode_text.dart';
import 'talk_power_warning.dart'; import 'talk_power_warning.dart';
/// Connected-server snapshot displaying welcome text, channel tree, and clients. /// Connected-server snapshot with welcome text, channels, and clients.
///
/// Renders the full channel hierarchy from a [BridgeSnapshot] with expandable
/// channel nodes, client voice-status indicators, unread-message badges, and
/// context menus for client actions (info, chat, poke, volume).
///
/// Channel join is triggered by tapping an unlocked channel row; password-
/// protected channels invoke [onJoinChannelWithPassword] instead.
class SnapshotView extends StatefulWidget { class SnapshotView extends StatefulWidget {
/// Construct a snapshot view. /// Construct a snapshot view.
const SnapshotView({ const SnapshotView({
@@ -31,14 +23,12 @@ class SnapshotView extends StatefulWidget {
required this.localOutputMuted, required this.localOutputMuted,
required this.hasJoinPending, required this.hasJoinPending,
required this.canJoinVoiceChannel, required this.canJoinVoiceChannel,
required this.unreadChannelIds,
required this.onJoinChannel, required this.onJoinChannel,
required this.onJoinChannelWithPassword, required this.onJoinChannelWithPassword,
this.enableClientLongPressMenu = false, this.enableClientLongPressMenu = false,
this.onOpenClientInfo, this.onOpenClientInfo,
this.onOpenClientChat, this.onOpenClientChat,
this.onOpenClientPoke, this.onOpenClientPoke,
this.onOpenChannelChat,
this.onTs3ServerLink, this.onTs3ServerLink,
}); });
@@ -66,9 +56,6 @@ class SnapshotView extends StatefulWidget {
/// True when the local client may join voice channels. /// True when the local client may join voice channels.
final bool canJoinVoiceChannel; final bool canJoinVoiceChannel;
/// Set of channel IDs that have unread chat messages.
final Set<BigInt> unreadChannelIds;
/// Join an unlocked channel. /// Join an unlocked channel.
final ValueChanged<rust.BridgeChannel> onJoinChannel; final ValueChanged<rust.BridgeChannel> onJoinChannel;
@@ -87,9 +74,6 @@ class SnapshotView extends StatefulWidget {
/// Open a poke composer for a non-self client. /// Open a poke composer for a non-self client.
final ValueChanged<rust.BridgeClient>? onOpenClientPoke; final ValueChanged<rust.BridgeClient>? onOpenClientPoke;
/// Open chat for a channel.
final ValueChanged<rust.BridgeChannel>? onOpenChannelChat;
/// Handle TeamSpeak server links embedded in server-provided text. /// Handle TeamSpeak server links embedded in server-provided text.
final Ts3ServerLinkHandler? onTs3ServerLink; final Ts3ServerLinkHandler? onTs3ServerLink;
@@ -256,12 +240,7 @@ class _SnapshotViewState extends State<SnapshotView> {
); );
} }
return _ChannelContextMenu( return InkWell(
channel: channel,
onChat: widget.onOpenChannelChat != null
? () => widget.onOpenChannelChat!(channel)
: null,
child: InkWell(
onTap: onTap, onTap: onTap,
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 40), constraints: const BoxConstraints(minHeight: 40),
@@ -292,17 +271,6 @@ class _SnapshotViewState extends State<SnapshotView> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
if (widget.unreadChannelIds.contains(channel.id)) ...[
const SizedBox(width: 8),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
),
),
],
if (channel.hasPassword) ...[ if (channel.hasPassword) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Icon( Icon(
@@ -313,7 +281,6 @@ class _SnapshotViewState extends State<SnapshotView> {
], ],
), ),
), ),
),
); );
} }
@@ -579,40 +546,11 @@ class _ClientVolumePreference {
} }
class _ClientVolumePreferences extends ChangeNotifier { class _ClientVolumePreferences extends ChangeNotifier {
_ClientVolumePreferences._() { _ClientVolumePreferences._();
_load();
}
static final instance = _ClientVolumePreferences._(); static final instance = _ClientVolumePreferences._();
static const _prefsKey = 'client_volume_prefs';
final Map<BigInt, _ClientVolumePreference> _byClientId = {}; final Map<BigInt, _ClientVolumePreference> _byClientId = {};
bool _loaded = false;
Future<void> _load() async {
if (_loaded) return;
_loaded = true;
try {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getStringList(_prefsKey) ?? const [];
for (final entry in raw) {
final parts = entry.split(':');
if (parts.length == 3) {
final id = BigInt.tryParse(parts[0]);
final volume = double.tryParse(parts[1]);
final muted = parts[2] == '1';
if (id != null && volume != null) {
_byClientId[id] = _ClientVolumePreference(
volume: volume,
muted: muted,
);
}
}
}
notifyListeners();
} catch (_) {}
}
_ClientVolumePreference preferenceFor(BigInt clientId) { _ClientVolumePreference preferenceFor(BigInt clientId) {
return _byClientId[clientId] ?? const _ClientVolumePreference(); return _byClientId[clientId] ?? const _ClientVolumePreference();
@@ -625,17 +563,6 @@ class _ClientVolumePreferences extends ChangeNotifier {
_byClientId.remove(clientId); _byClientId.remove(clientId);
} }
notifyListeners(); notifyListeners();
_save();
}
Future<void> _save() async {
try {
final prefs = await SharedPreferences.getInstance();
final raw = _byClientId.entries
.map((e) => '${e.key}:${e.value.volume}:${e.value.muted ? 1 : 0}')
.toList();
await prefs.setStringList(_prefsKey, raw);
} catch (_) {}
} }
} }
@@ -1155,60 +1082,3 @@ class _ClientVolumeSheetState extends State<_ClientVolumeSheet> {
); );
} }
} }
/// Context menu for channel tiles offering "Chat" on right-click or
/// long-press. Primary tap passes through to the child for voice join.
class _ChannelContextMenu extends StatelessWidget {
const _ChannelContextMenu({
required this.channel,
this.onChat,
required this.child,
});
final rust.BridgeChannel channel;
final VoidCallback? onChat;
final Widget child;
@override
Widget build(BuildContext context) {
if (onChat == null) return child;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onSecondaryTapDown: (details) =>
_show(context, details.globalPosition),
onLongPressStart: (details) =>
_show(context, details.globalPosition),
child: child,
);
}
void _show(BuildContext context, Offset globalPosition) {
final overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final position = RelativeRect.fromLTRB(
globalPosition.dx,
globalPosition.dy,
overlay.size.width - globalPosition.dx,
overlay.size.height - globalPosition.dy,
);
showMenu<String>(
context: context,
position: position,
items: [
PopupMenuItem(
value: 'chat',
child: Row(
children: [
const Icon(Icons.chat_bubble_outline, size: 18),
const SizedBox(width: 12),
Text(AppL10n.of(context).chatAction),
],
),
),
],
).then((value) {
if (value == 'chat') onChat?.call();
});
}
}
@@ -33,7 +33,6 @@ class VoiceBar extends StatelessWidget {
required this.onConfigure, required this.onConfigure,
required this.onPttHeldChanged, required this.onPttHeldChanged,
this.talkPowerBlocked = false, this.talkPowerBlocked = false,
this.inputLevel,
}); });
final bool inChannel; final bool inChannel;
@@ -54,10 +53,6 @@ class VoiceBar extends StatelessWidget {
/// level meter. Pass `null` to render an idle meter. /// level meter. Pass `null` to render an idle meter.
final rust.BridgeAudioStats? audioStats; final rust.BridgeAudioStats? audioStats;
/// Real-time input level from the 30 Hz stream (dBFS).
/// When non-null, takes precedence over `audioStats.inputLevel`.
final double? inputLevel;
/// PTT capability badge inputs — passed through to /// PTT capability badge inputs — passed through to
/// [`PttCapabilityBadge`]. /// [`PttCapabilityBadge`].
final String pttLevel; final String pttLevel;
@@ -201,7 +196,7 @@ class VoiceBar extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// Row 4: level meter // Row 4: level meter
VoiceLevelMeter(active: levelActive, level: inputLevel ?? stats?.inputLevel), VoiceLevelMeter(active: levelActive),
const SizedBox(height: 4), const SizedBox(height: 4),
if (stats != null) if (stats != null)
Text( Text(
@@ -1,13 +1,17 @@
// Compact voice UI for narrow / mobile layouts (Plan E hybrid: // Compact voice UI for narrow / mobile layouts.
// AppBar mutes + status chip with 2-line live readout + wide bottom-
// anchored PTT button + modal sheet for non-essential controls).
// //
// rc.8 follow-up: the AppBar gear icon was removed; the modal sheet // Two-zone voice bar pinned to the bottom:
// is now the **single** voice-controls surface on mobile. Mode + // • Control row: status text + mute + deafen + settings chevron
// release-tail are surfaced inline (radio buttons + slider) inside // • PTT row: full-width hold-to-talk (PTT mode only)
// the modal. // Both share a single container whose background colour reflects
// the current voice state (normal / muted / talk-power-blocked).
//
// Gesture isolation: the control row uses tap-only InkWell /
// IconButton; the PTT row uses a raw Listener for pointer-down /
// pointer-up. Because each row is a disjoint hit-test region, a
// finger holding PTT cannot accidentally toggle mute or deafen.
import 'dart:async' show StreamSubscription, Timer, unawaited; import 'dart:async' show Timer, unawaited;
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
@@ -26,6 +30,21 @@ import 'voice_settings_controls.dart';
import 'voice_status_summary.dart'; import 'voice_status_summary.dart';
import '../src/rust/api.dart' as rust; import '../src/rust/api.dart' as rust;
bool get _isIos {
if (kIsWeb) return false;
return Platform.isIOS;
}
bool get _isMacOS {
if (kIsWeb) return false;
return Platform.isMacOS;
}
bool get _isDesktopSileroVadHost {
if (kIsWeb) return false;
return Platform.isWindows || Platform.isLinux;
}
/// Two-line status chip that summarises the current voice state. /// Two-line status chip that summarises the current voice state.
/// Tap to open the voice details modal. /// Tap to open the voice details modal.
class VoiceStatusChip extends StatelessWidget { class VoiceStatusChip extends StatelessWidget {
@@ -38,11 +57,8 @@ class VoiceStatusChip extends StatelessWidget {
required this.audioStats, required this.audioStats,
required this.isTouchOnly, required this.isTouchOnly,
required this.onTap, required this.onTap,
required this.onToggleInputMute,
required this.onToggleOutputMute,
this.inputMuted = false, this.inputMuted = false,
this.outputMuted = false, this.outputMuted = false,
this.hardMuteByTalkPower = false,
this.talkPower, this.talkPower,
this.neededTalkPower, this.neededTalkPower,
this.talkPowerGranted, this.talkPowerGranted,
@@ -69,9 +85,6 @@ class VoiceStatusChip extends StatelessWidget {
/// True when local speaker is muted. /// True when local speaker is muted.
final bool outputMuted; final bool outputMuted;
/// True when the server talk-power gate forces local hard mute.
final bool hardMuteByTalkPower;
/// Own client's talk power. /// Own client's talk power.
final int? talkPower; final int? talkPower;
@@ -84,12 +97,6 @@ class VoiceStatusChip extends StatelessWidget {
/// Open the voice details modal. /// Open the voice details modal.
final VoidCallback onTap; final VoidCallback onTap;
/// Toggle local input hard mute.
final VoidCallback onToggleInputMute;
/// Toggle local output mute/deafen.
final VoidCallback onToggleOutputMute;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -110,9 +117,18 @@ class VoiceStatusChip extends StatelessWidget {
); );
return Semantics( return Semantics(
button: true,
label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}', label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
hint: l10n.voiceSettingsTitle,
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
borderRadius: BorderRadius.circular(12),
child: ExcludeSemantics(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -144,14 +160,6 @@ class VoiceStatusChip extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: InkWell(
onTap: () {
HapticFeedback.lightImpact();
onTap();
},
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -175,63 +183,18 @@ class VoiceStatusChip extends StatelessWidget {
], ],
), ),
), ),
), const SizedBox(width: 8),
), Icon(
const SizedBox(width: 4),
IconButton(
tooltip: hardMuteByTalkPower
? l10n.voiceTalkPowerBlocked
: l10n.voiceHardMuteLabel,
icon: Icon(inputMuted ? Icons.mic_off : Icons.mic),
color: inputMuted ? theme.colorScheme.error : null,
onPressed: hardMuteByTalkPower ? null : onToggleInputMute,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: 40,
height: 40,
),
padding: EdgeInsets.zero,
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
IconButton(
tooltip: l10n.voiceOutputMuteLabel,
icon: Icon(outputMuted ? Icons.headset_off : Icons.headset),
color: outputMuted ? theme.colorScheme.error : null,
onPressed: onToggleOutputMute,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: 40,
height: 40,
),
padding: EdgeInsets.zero,
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
IconButton(
tooltip: l10n.voiceSettingsTitle,
icon: Icon(
Icons.expand_less, Icons.expand_less,
size: 18, size: 18,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
onPressed: onTap,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints.tightFor(
width: 40,
height: 40,
),
padding: EdgeInsets.zero,
style: const ButtonStyle(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
], ],
), ),
), ),
), ),
),
),
); );
} }
} }
@@ -303,15 +266,6 @@ class _VoicePttButtonState extends State<VoicePttButton> {
playVoicePttHaptic(held); playVoicePttHaptic(held);
} }
@override
void dispose() {
if (_pressed) {
_pressed = false;
widget.onHeldChanged(false);
}
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -505,8 +459,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
int _rateTickCount = 0; int _rateTickCount = 0;
late final AudioProcessingConfigState _audioProcessing; late final AudioProcessingConfigState _audioProcessing;
double? _streamLevel;
StreamSubscription<double>? _levelSub;
@override @override
void initState() { void initState() {
@@ -515,12 +467,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
widget.initialAudioConfig, widget.initialAudioConfig,
); );
_levelSub = rust.inputLevelStream().listen((level) { // Poll audio stats at 250 ms so TX/RX counters and the level meter
if (mounted) setState(() => _streamLevel = level); // update in real time while the sheet is open, independent of the parent.
});
// Poll audio stats at 250 ms so TX/RX counters update in real time
// while the sheet is open.
_statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async { _statsTimer = Timer.periodic(const Duration(milliseconds: 250), (_) async {
try { try {
final s = await rust.audioStats(); final s = await rust.audioStats();
@@ -528,6 +476,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
setState(() { setState(() {
_stats = s; _stats = s;
_rateTickCount++; _rateTickCount++;
// Compute rates every ~1 s (4 × 250 ms).
if (_rateTickCount >= 4) { if (_rateTickCount >= 4) {
_txRate = s.framesSent - _prevSent; _txRate = s.framesSent - _prevSent;
_rxRate = s.framesReceived - _prevReceived; _rxRate = s.framesReceived - _prevReceived;
@@ -542,7 +491,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
@override @override
void dispose() { void dispose() {
_levelSub?.cancel();
_statsTimer?.cancel(); _statsTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -595,6 +543,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge), Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge),
const SizedBox(height: 12), const SizedBox(height: 12),
// ── Primary section (always visible) ────────────────────
// 1) Audio output route picker tile (mobile only). // 1) Audio output route picker tile (mobile only).
if (showRoutePicker) ...[ if (showRoutePicker) ...[
const AudioOutputTile(), const AudioOutputTile(),
@@ -623,13 +573,6 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
selected: _mode == rust.BridgeTransmitMode.continuous, selected: _mode == rust.BridgeTransmitMode.continuous,
onTap: () => _setMode(rust.BridgeTransmitMode.continuous), onTap: () => _setMode(rust.BridgeTransmitMode.continuous),
), ),
// Voice-activity transmit is only honoured by the engine on
// hosts that ship a Chanora-owned VAD pipeline (DEC-030:
// Windows + Linux desktop and Android). iOS / macOS rely
// on Apple VoiceProcessingIO and have no VAD bridge, so
// hiding the row prevents the UI from advertising a
// transmit mode the engine cannot honour.
if (voiceActivityTransmitAvailable)
_ModeRow( _ModeRow(
label: l10n.voiceModeVoiceActivity, label: l10n.voiceModeVoiceActivity,
icon: Icons.graphic_eq, icon: Icons.graphic_eq,
@@ -637,7 +580,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity), onTap: () => _setMode(rust.BridgeTransmitMode.voiceActivity),
), ),
// 3) Release-tail slider (PTT only). // Release-tail slider (PTT only).
if (isPtt) ...[ if (isPtt) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
@@ -687,8 +630,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
Divider(height: 1, color: theme.colorScheme.outlineVariant), Divider(height: 1, color: theme.colorScheme.outlineVariant),
const SizedBox(height: 12), const SizedBox(height: 12),
// 4) Level meter + live TX/RX stats. // Level meter + live TX/RX stats.
VoiceLevelMeter(active: levelActive, level: _streamLevel ?? stats?.inputLevel), VoiceLevelMeter(active: levelActive),
const SizedBox(height: 6), const SizedBox(height: 6),
_StatsRow( _StatsRow(
txRate: _txRate, txRate: _txRate,
@@ -711,36 +654,213 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
), ),
], ],
// Audio processing.
const SizedBox(height: 12),
Divider(height: 1, color: theme.colorScheme.outlineVariant),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(
// ── Collapsible: Audio processing ───────────────────────
ExpansionTile(
initiallyExpanded: false,
shape: const Border(),
collapsedShape: const Border(),
tilePadding: const EdgeInsets.symmetric(horizontal: 0),
title: Text(
'Audio processing', 'Audio processing',
style: theme.textTheme.labelLarge?.copyWith( style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 4), children: [
_buildAudioProcessingSection(theme),
AudioProcessingPanel( ],
config: _audioProcessing,
dense: true,
update: (mutation) {
setState(mutation);
_notifyAudioConfig();
},
), ),
// Debug. // ── Collapsible: PTT capability (PTT mode only) ─────────
if (isPtt)
ExpansionTile(
initiallyExpanded: false,
tilePadding: const EdgeInsets.symmetric(horizontal: 0),
shape: const Border(),
collapsedShape: const Border(),
title: Text(
'PTT capability',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
children: [
PttCapabilityBadge(
level: widget.pttLevel,
backendId: widget.pttBackendId,
boundInputClass: widget.pttBoundInputClass,
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( ],
),
// ── Collapsible: Debug ──────────────────────────────────
ExpansionTile(
initiallyExpanded: false,
shape: const Border(),
collapsedShape: const Border(),
tilePadding: const EdgeInsets.symmetric(horizontal: 0),
title: Text(
'Debug', 'Debug',
style: theme.textTheme.labelLarge?.copyWith( style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
children: [
_buildDebugSection(theme),
],
),
],
),
),
);
}
// ── Audio processing section (inside ExpansionTile) ─────────────────
Widget _buildAudioProcessingSection(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Android HW/SW selector.
if (Platform.isAndroid) ...[
const VoiceSubHeader('Processing backend'),
SegmentedButton<bool>(
style: voiceSegmentedButtonStyle(theme),
segments: androidProcessingSegments,
selected: {_audioProcessing.preferHardware},
onSelectionChanged: (s) {
setState(() => _audioProcessing.preferHardware = s.first);
_notifyAudioConfig();
},
),
const SizedBox(height: 4),
Text(
_audioProcessing.preferHardware
? 'Hardware mode still keeps per-stage WebRTC fallback, so these controls remain effective.'
: 'Software mode applies the full WebRTC APM stage set.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
if (_isIos) ...[
Text(
'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
'hidden here; only settings that still affect the shipping '
'iOS path are shown.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
],
if (!_isIos &&
(!Platform.isAndroid ||
androidShowsNsControl(_audioProcessing)))
AudioProcessingToggleRow(
dense: true,
label: 'Noise suppression',
subtitle: 'Wiener filter',
value: _audioProcessing.nsEnabled,
onChanged: (v) {
setState(() => _audioProcessing.nsEnabled = v);
_notifyAudioConfig();
},
),
if (!_isIos &&
(!Platform.isAndroid ||
androidShowsAecControl(_audioProcessing)))
AudioProcessingToggleRow(
dense: true,
label: 'Echo cancellation',
subtitle: Platform.isAndroid
? (_audioProcessing.preferHardware
? 'Prefers device/OS effect; falls back to WebRTC AEC3'
: 'WebRTC AEC3 · adaptive filter')
: (_isMacOS
? 'Managed by platform VPIO'
: 'WebRTC AEC3 · adaptive filter'),
value: _isMacOS ? true : _audioProcessing.aecEnabled,
onChanged: _isMacOS
? null
: (v) {
setState(() => _audioProcessing.aecEnabled = v);
_notifyAudioConfig();
},
),
if (!_isIos &&
(!Platform.isAndroid ||
androidShowsAgcControl(_audioProcessing)))
AudioProcessingToggleRow(
dense: true,
label: 'Auto gain control',
subtitle: 'AGC2 · -18 dBFS target',
value: _audioProcessing.agcEnabled,
onChanged: (v) {
setState(() => _audioProcessing.agcEnabled = v);
_notifyAudioConfig();
},
),
if (!Platform.isAndroid || androidShowsHpfControl(_audioProcessing))
AudioProcessingToggleRow(
dense: true,
label: 'High-pass filter',
subtitle: '80 Hz · DC removal',
value: _audioProcessing.hpfEnabled,
onChanged: (v) {
setState(() => _audioProcessing.hpfEnabled = v);
_notifyAudioConfig();
},
),
if (!_isIos &&
(!Platform.isAndroid ||
androidShowsLimiterControl(_audioProcessing)))
AudioProcessingToggleRow(
dense: true,
label: 'Peak limiter',
subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
value: _audioProcessing.limiterEnabled,
onChanged: (v) {
setState(() => _audioProcessing.limiterEnabled = v);
_notifyAudioConfig();
},
),
// VAD backend.
const SizedBox(height: 8),
Text(
'Voice activity detection (VAD)',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2), const SizedBox(height: 2),
SegmentedButton<rust.BridgeVadBackend>(
style: voiceSegmentedButtonStyle(theme),
segments: _isDesktopSileroVadHost
? desktopVadBackendSegments
: vadBackendSegments,
selected: {_audioProcessing.vadBackend},
onSelectionChanged: (s) {
setState(() => _audioProcessing.vadBackend = s.first);
_notifyAudioConfig();
},
),
const SizedBox(height: 8),
],
);
}
// ── Debug section (inside ExpansionTile) ─────────────────────────────
Widget _buildDebugSection(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AudioProcessingToggleRow( AudioProcessingToggleRow(
dense: true, dense: true,
label: 'WAV dump', label: 'WAV dump',
@@ -751,23 +871,8 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
_notifyAudioConfig(); _notifyAudioConfig();
}, },
), ),
const SizedBox(height: 8),
// 6) PTT capability badge. On iOS this must remain
// visible even though the resolved level is always
// `L0Focused`, because the P0 acceptance flow requires
// honest capability advertising with an explanation of
// the sandbox limitation.
if (isPtt) ...[
const SizedBox(height: 12),
PttCapabilityBadge(
level: widget.pttLevel,
backendId: widget.pttBackendId,
boundInputClass: widget.pttBoundInputClass,
),
], ],
],
),
),
); );
} }
} }
@@ -893,3 +998,424 @@ class _ModeRow extends StatelessWidget {
); );
} }
} }
// ── Unified mobile voice bar ──────────────────────────────────────────────
/// A unified bottom-anchored voice bar for compact / mobile layouts.
///
/// Combines the former [VoiceStatusChip] and [VoicePttButton] into one
/// visual zone with two rows:
///
/// ┌──────────────────────────────────────────────┐
/// │ 🟢 PTT · Connected [🔇] [🎧] [▲] │ ← control row (tap)
/// ├──────────────────────────────────────────────┤
/// │ ════ hold to talk ════ │ ← PTT row (hold)
/// └──────────────────────────────────────────────┘
///
/// The PTT row is shown only when [transmitMode] is PTT; for continuous
/// or voice-activity modes the bar shrinks to the control row alone.
///
/// State colour is applied to the entire container:
/// - normal: `surfaceContainerHigh`
/// - muted: `errorContainer` (35 % alpha)
/// - talk-power-block: amber (18 % alpha)
class CompactVoiceBar extends StatelessWidget {
/// Construct a compact voice bar.
const CompactVoiceBar({
super.key,
required this.inChannel,
required this.transmitMode,
required this.releaseTailMs,
required this.pttBoundKeyLabel,
required this.audioStats,
required this.isTouchOnly,
required this.inputMuted,
required this.outputMuted,
required this.onToggleInputMute,
required this.onToggleOutputMute,
required this.onOpenDetails,
required this.onPttHeldChanged,
this.talkPower,
this.neededTalkPower,
this.talkPowerGranted,
this.hardMuteByTalkPower = false,
});
/// True when the client is inside a channel (gates PTT row visibility).
final bool inChannel;
/// Current transmit mode.
final rust.BridgeTransmitMode transmitMode;
/// Release-tail in milliseconds.
final int releaseTailMs;
/// Bound key label (empty on touch-only hosts).
final String pttBoundKeyLabel;
/// Current audio stats; null while audio engine not running.
final rust.BridgeAudioStats? audioStats;
/// True on iOS / iPadOS / Android.
final bool isTouchOnly;
/// True when local mic is muted.
final bool inputMuted;
/// True when local speaker is muted.
final bool outputMuted;
/// Toggle hard-mute on / off.
final VoidCallback onToggleInputMute;
/// Toggle output mute on / off.
final VoidCallback onToggleOutputMute;
/// Open the voice details modal sheet.
final VoidCallback onOpenDetails;
/// Called with `true` on finger-down, `false` on finger-up / cancel.
final ValueChanged<bool> onPttHeldChanged;
/// Own client's talk power.
final int? talkPower;
/// Talk power required to speak in current channel.
final int? neededTalkPower;
/// True when server granted talk power.
final bool? talkPowerGranted;
/// True when talk power prevents speaking.
final bool hardMuteByTalkPower;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppL10n.of(context);
final isPtt = transmitMode == rust.BridgeTransmitMode.ptt;
final pttActive = audioStats?.pttActive ?? false;
final summary = voiceStatusSummary(
l10n: l10n,
transmitMode: transmitMode,
releaseTailMs: releaseTailMs,
pttBoundKeyLabel: pttBoundKeyLabel,
isTouchOnly: isTouchOnly,
inputMuted: inputMuted,
outputMuted: outputMuted,
pttActive: pttActive,
talkPower: talkPower,
neededTalkPower: neededTalkPower,
talkPowerGranted: talkPowerGranted,
);
// Container colour based on voice state.
final containerColor = summary.talkPowerBlocked
? Colors.amber.withValues(alpha: 0.18)
: summary.muted
? theme.colorScheme.errorContainer.withValues(alpha: 0.35)
: theme.colorScheme.surfaceContainerHigh;
final borderColor = summary.talkPowerBlocked
? Colors.amber.shade700
: summary.muted
? theme.colorScheme.error
: theme.colorScheme.outlineVariant;
final borderWidth = 1.0;
return Semantics(
label: '${l10n.voiceSheetTitle}: ${summary.line1}, ${summary.line2}',
child: Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: containerColor,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: borderColor, width: borderWidth),
),
clipBehavior: Clip.antiAlias,
child: AnimatedSize(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── Control row ──────────────────────────────────────
_ControlRow(
summary: summary,
inputMuted: inputMuted,
outputMuted: outputMuted,
hardMuteByTalkPower: hardMuteByTalkPower,
onToggleInputMute: onToggleInputMute,
onToggleOutputMute: onToggleOutputMute,
onOpenDetails: onOpenDetails,
),
// ── PTT row (in-channel + PTT mode only) ──────────────
if (isPtt && inChannel) ...[
Divider(
height: 1,
thickness: 0.5,
color: borderColor,
indent: 12,
endIndent: 12,
),
_PttRow(
active: pttActive,
enabled: !hardMuteByTalkPower,
onHeldChanged: onPttHeldChanged,
),
],
],
),
),
),
),
);
}
}
// ── Control row (tap-only zone) ───────────────────────────────────────────
class _ControlRow extends StatelessWidget {
const _ControlRow({
required this.summary,
required this.inputMuted,
required this.outputMuted,
required this.hardMuteByTalkPower,
required this.onToggleInputMute,
required this.onToggleOutputMute,
required this.onOpenDetails,
});
final VoiceStatusSummary summary;
final bool inputMuted;
final bool outputMuted;
final bool hardMuteByTalkPower;
final VoidCallback onToggleInputMute;
final VoidCallback onToggleOutputMute;
final VoidCallback onOpenDetails;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
// Status dot.
Icon(
summary.micOn ? Icons.fiber_manual_record : Icons.fiber_manual_record_outlined,
size: 10,
color: summary.micOn ? theme.colorScheme.primary : theme.colorScheme.outline,
),
const SizedBox(width: 8),
// Status text (tappable → open details).
Expanded(
child: Semantics(
button: true,
label: summary.line1,
child: InkWell(
onTap: onOpenDetails,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
summary.line1,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
summary.line2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
),
),
const SizedBox(width: 4),
// Mute button.
_ToggleButton(
icon: inputMuted ? Icons.mic_off : Icons.mic,
isActive: inputMuted,
tooltip: 'Mute mic',
onPressed: hardMuteByTalkPower ? null : onToggleInputMute,
),
// Deafen button.
_ToggleButton(
icon: outputMuted ? Icons.headset_off : Icons.headset,
isActive: outputMuted,
tooltip: 'Deafen',
onPressed: onToggleOutputMute,
),
// Settings / expand chevron.
IconButton(
icon: Icon(Icons.expand_less, size: 20, color: theme.colorScheme.onSurfaceVariant),
tooltip: 'Voice settings',
onPressed: onOpenDetails,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
padding: EdgeInsets.zero,
),
],
),
);
}
}
// ── Toggle button (mute / deafen) ─────────────────────────────────────────
class _ToggleButton extends StatelessWidget {
const _ToggleButton({
required this.icon,
required this.isActive,
required this.tooltip,
required this.onPressed,
});
final IconData icon;
final bool isActive;
final String tooltip;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return IconButton(
icon: Icon(icon, size: 22),
tooltip: tooltip,
color: isActive ? theme.colorScheme.error : null,
onPressed: onPressed,
visualDensity: VisualDensity.compact,
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
padding: EdgeInsets.zero,
);
}
}
// ── PTT row (hold-only zone) ──────────────────────────────────────────────
class _PttRow extends StatefulWidget {
const _PttRow({
required this.active,
required this.enabled,
required this.onHeldChanged,
});
/// True while the engine reports the gate open.
final bool active;
/// Whether the PTT button can be engaged.
final bool enabled;
/// Called with `true` on pointer-down, `false` on pointer-up / cancel.
final ValueChanged<bool> onHeldChanged;
@override
State<_PttRow> createState() => _PttRowState();
}
class _PttRowState extends State<_PttRow> {
int? _activePointer;
bool _held = false;
@override
void initState() {
super.initState();
prepareVoiceHaptics();
}
void _begin(PointerDownEvent event) {
if (!widget.enabled || _activePointer != null) return;
_activePointer = event.pointer;
_held = true;
widget.onHeldChanged(true);
playVoicePttHaptic(true);
setState(() {});
}
void _end(int pointer) {
if (_activePointer != pointer) return;
_activePointer = null;
_held = false;
widget.onHeldChanged(false);
playVoicePttHaptic(false);
setState(() {});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppL10n.of(context);
final activeNow = _held || widget.active;
return Semantics(
button: true,
liveRegion: true,
label: activeNow ? l10n.pttTransmitting : l10n.pttHoldToTalk,
hint: l10n.pttHoldToTalkSemanticsHint,
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: _begin,
onPointerUp: (e) => _end(e.pointer),
onPointerCancel: (e) => _end(e.pointer),
child: ExcludeSemantics(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: activeNow
? theme.colorScheme.primary
: Colors.transparent,
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
activeNow ? Icons.mic : Icons.mic_none_outlined,
size: 22,
color: activeNow
? theme.colorScheme.onPrimary
: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
activeNow ? l10n.voiceMicOn : l10n.voiceModePtt,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
color: activeNow
? theme.colorScheme.onPrimary
: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
),
),
);
}
}
@@ -1,78 +1,28 @@
import 'dart:math' show max;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Shared compact level meter used by voice surfaces. /// Shared compact level meter used by voice surfaces.
/// class VoiceLevelMeter extends StatelessWidget {
/// When [level] is null (no stats available yet), falls back to [active] const VoiceLevelMeter({super.key, required this.active});
/// for a binary indicator. When [level] is provided it is interpreted as
/// dBFS and mapped to a 01 fill fraction via [dbfsToFraction] (floors
/// at -60 dBFS).
class VoiceLevelMeter extends StatefulWidget {
const VoiceLevelMeter({super.key, this.active = false, this.level});
/// Binary fallback when no dBFS value is available.
final bool active; final bool active;
/// Real input level in dBFS (-120 = silence, 0 = clipping).
/// Null means stats are not yet available; [active] is used instead.
final double? level;
/// Map dBFS [-60, 0] → [0.0, 1.0].
static double dbfsToFraction(double dbfs) {
const floor = -60.0;
if (dbfs <= floor) return 0.0;
if (dbfs >= 0.0) return 1.0;
return (dbfs - floor) / -floor;
}
@override
State<VoiceLevelMeter> createState() => _VoiceLevelMeterState();
}
class _VoiceLevelMeterState extends State<VoiceLevelMeter> {
double _previousFill = 0.0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final double fill;
final Color color;
if (widget.level != null) {
fill = VoiceLevelMeter.dbfsToFraction(widget.level!);
color = fill > 0.0
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant;
} else {
fill = widget.active ? 0.75 : 0.05;
color = widget.active
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant;
}
final begin = _previousFill;
_previousFill = fill;
return Container( return Container(
height: 8, height: 8,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest, color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: TweenAnimationBuilder<double>( child: FractionallySizedBox(
tween: Tween<double>(begin: begin, end: fill),
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
builder: (context, animatedFill, child) {
return FractionallySizedBox(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
widthFactor: max(animatedFill, 0.02), widthFactor: active ? 0.75 : 0.05,
child: child,
);
},
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: active
? theme.colorScheme.primary
: theme.colorScheme.outlineVariant,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
), ),
@@ -2,17 +2,9 @@ import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
// TODO(refactor): Scattered Platform.isX checks exist across ~6 Dart files. /// True when the host is a touch-only mobile platform without a
// Centralize all platform checks here and update call sites to use these /// hardware keyboard the user would bind a PTT key on.
// getters instead of raw Platform.isAndroid/isIOS/etc. bool get isTouchOnlyPttHost {
bool get _notWeb => !kIsWeb; if (kIsWeb) return false;
return Platform.isIOS || Platform.isAndroid;
bool get isTouchOnlyPttHost => _notWeb && (Platform.isIOS || Platform.isAndroid); }
bool get isAndroidHost => _notWeb && Platform.isAndroid;
bool get isIosHost => _notWeb && Platform.isIOS;
bool get isMacOsHost => _notWeb && Platform.isMacOS;
bool get isDesktopSileroVadHost => _notWeb && (Platform.isWindows || Platform.isLinux);
@@ -7,6 +7,9 @@
// - VAD backend // - VAD backend
// - platform audio-processing mode selection where available // - platform audio-processing mode selection where available
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../l10n/generated/app_localizations.dart'; import '../l10n/generated/app_localizations.dart';
@@ -19,8 +22,27 @@ import 'voice_platform.dart';
import 'voice_settings_controls.dart'; import 'voice_settings_controls.dart';
import '../src/rust/api.dart' as rust; import '../src/rust/api.dart' as rust;
/// Result returned by [VoiceSettingsDialog] when the user saves or bool get _isAndroid {
/// requests a key bind. if (kIsWeb) return false;
return Platform.isAndroid;
}
bool get _isIos {
if (kIsWeb) return false;
return Platform.isIOS;
}
bool get _isMacOS {
if (kIsWeb) return false;
return Platform.isMacOS;
}
bool get _isDesktopSileroVadHost {
if (kIsWeb) return false;
return Platform.isWindows || Platform.isLinux;
}
/// Result returned by [VoiceSettingsDialog].
class VoiceSettingsResult { class VoiceSettingsResult {
const VoiceSettingsResult({ const VoiceSettingsResult({
required this.mode, required this.mode,
@@ -35,15 +57,7 @@ class VoiceSettingsResult {
final rust.BridgeAudioProcessingConfig audioConfig; final rust.BridgeAudioProcessingConfig audioConfig;
} }
/// Dialog for configuring voice transmission and audio processing settings. /// Voice + audio processing settings dialog.
///
/// Surfaces transmit mode selection (continuous, PTT, voice-activity),
/// PTT release-tail slider, key-bind request, and a full audio processing
/// panel covering noise suppression, echo cancellation, AGC, HPF, and VAD
/// backend selection.
///
/// On mobile hosts, the voice-activity segment is hidden when no
/// Chanora-owned VAD pipeline is available (iOS, macOS, web).
class VoiceSettingsDialog extends StatefulWidget { class VoiceSettingsDialog extends StatefulWidget {
const VoiceSettingsDialog({ const VoiceSettingsDialog({
super.key, super.key,
@@ -56,8 +70,6 @@ class VoiceSettingsDialog extends StatefulWidget {
this.talkPower, this.talkPower,
this.neededTalkPower, this.neededTalkPower,
this.talkPowerGranted, this.talkPowerGranted,
this.onInputDeviceChanged,
this.onOutputDeviceChanged,
}); });
final rust.BridgeTransmitMode initialMode; final rust.BridgeTransmitMode initialMode;
@@ -69,8 +81,6 @@ class VoiceSettingsDialog extends StatefulWidget {
final int? talkPower; final int? talkPower;
final int? neededTalkPower; final int? neededTalkPower;
final bool? talkPowerGranted; final bool? talkPowerGranted;
final ValueChanged<String?>? onInputDeviceChanged;
final ValueChanged<String?>? onOutputDeviceChanged;
@override @override
State<VoiceSettingsDialog> createState() => _VoiceSettingsDialogState(); State<VoiceSettingsDialog> createState() => _VoiceSettingsDialogState();
@@ -96,7 +106,7 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
rust.BridgeAudioProcessingConfig _buildConfig() { rust.BridgeAudioProcessingConfig _buildConfig() {
return _audioProcessing.buildConfig( return _audioProcessing.buildConfig(
base: widget.initialAudioConfig, base: widget.initialAudioConfig,
isAndroid: isAndroidHost, isAndroid: _isAndroid,
); );
} }
@@ -118,12 +128,7 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
VoiceSectionHeader(l10n.voiceModeLabel), VoiceSectionHeader(l10n.voiceModeLabel),
SegmentedButton<rust.BridgeTransmitMode>( SegmentedButton<rust.BridgeTransmitMode>(
style: voiceSegmentedButtonStyle(theme), style: voiceSegmentedButtonStyle(theme),
// DEC-030: hide the voice-activity segment on hosts segments: transmitModeSegments,
// that ship no Chanora-owned VAD pipeline (iOS,
// macOS, web).
segments: transmitModeSegmentsFor(
voiceActivityAvailable: voiceActivityTransmitAvailable,
),
selected: {_mode}, selected: {_mode},
onSelectionChanged: (s) => setState(() => _mode = s.first), onSelectionChanged: (s) => setState(() => _mode = s.first),
), ),
@@ -179,9 +184,91 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
const Divider(height: 24), const Divider(height: 24),
const VoiceSectionHeader('Audio processing'), const VoiceSectionHeader('Audio processing'),
AudioProcessingPanel( // Android HW/SW selector
config: _audioProcessing, if (_isAndroid) ...[
update: (mutation) => setState(mutation), const VoiceSubHeader('Processing backend'),
SegmentedButton<bool>(
style: voiceSegmentedButtonStyle(theme),
segments: androidProcessingSegments,
selected: {_audioProcessing.preferHardware},
onSelectionChanged: (s) =>
setState(() => _audioProcessing.preferHardware = s.first),
),
const SizedBox(height: 4),
Text(
_audioProcessing.preferHardware
? 'Android hardware mode still falls back to WebRTC APM per stage when device effects are missing, so these controls remain available.'
: 'Android software mode applies the full WebRTC APM control set.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
],
// DSP toggles
const VoiceSubHeader('DSP stages'),
if (_isIos) ...[
Text(
'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
'hidden here; only settings that still affect the shipping '
'iOS path are shown.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
],
if (!_isIos &&
(!_isAndroid || androidShowsNsControl(_audioProcessing)))
AudioProcessingToggleRow(
label: 'Noise suppression (NS)',
subtitle: 'Wiener filter · stationary noise',
value: _audioProcessing.nsEnabled,
onChanged: (v) =>
setState(() => _audioProcessing.nsEnabled = v),
),
if (!_isIos &&
(!_isAndroid || androidShowsAecControl(_audioProcessing)))
AudioProcessingToggleRow(
label: 'Echo cancellation (AEC3)',
subtitle: _isAndroid
? (_audioProcessing.preferHardware
? 'Prefers device/OS effect; WebRTC AEC3 fallback when binding is unavailable'
: 'WebRTC AEC3 · adaptive filter')
: (_isMacOS
? 'Managed by platform VPIO'
: 'WebRTC AEC3 · adaptive filter'),
value: _isMacOS ? true : _audioProcessing.aecEnabled,
onChanged: _isMacOS
? null
: (v) => setState(() => _audioProcessing.aecEnabled = v),
),
if (!_isIos &&
(!_isAndroid || androidShowsAgcControl(_audioProcessing)))
AudioProcessingToggleRow(
label: 'Auto gain control (AGC2)',
subtitle: 'RNN VAD-gated · -18 dBFS target',
value: _audioProcessing.agcEnabled,
onChanged: (v) =>
setState(() => _audioProcessing.agcEnabled = v),
),
if (!_isAndroid || androidShowsHpfControl(_audioProcessing))
AudioProcessingToggleRow(
label: 'High-pass filter (HPF)',
subtitle: '80 Hz Butterworth · DC removal',
value: _audioProcessing.hpfEnabled,
onChanged: (v) =>
setState(() => _audioProcessing.hpfEnabled = v),
),
if (!_isIos &&
(!_isAndroid || androidShowsLimiterControl(_audioProcessing)))
AudioProcessingToggleRow(
label: 'Peak limiter',
subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
value: _audioProcessing.limiterEnabled,
onChanged: (v) =>
setState(() => _audioProcessing.limiterEnabled = v),
), ),
if (isTalkPowerBlocked( if (isTalkPowerBlocked(
@@ -197,6 +284,22 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
), ),
], ],
// ── VAD ────────────────────────────────────────────────
const Divider(height: 24),
const VoiceSectionHeader('Voice activity detection (VAD)'),
const VoiceSubHeader('Backend'),
SegmentedButton<rust.BridgeVadBackend>(
style: voiceSegmentedButtonStyle(theme),
segments: _isDesktopSileroVadHost
? desktopVadBackendSegments
: vadBackendSegments,
selected: {_audioProcessing.vadBackend},
onSelectionChanged: (s) =>
setState(() => _audioProcessing.vadBackend = s.first),
),
const SizedBox(height: 8),
// ── PTT capability badge ──────────────────────────────── // ── PTT capability badge ────────────────────────────────
if (_mode == rust.BridgeTransmitMode.ptt && if (_mode == rust.BridgeTransmitMode.ptt &&
widget.pttLevel.isNotEmpty) ...[ widget.pttLevel.isNotEmpty) ...[
@@ -210,25 +313,23 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
], ],
// ── Audio output route picker (mobile only) ───────────── // ── Audio output route picker (mobile only) ─────────────
if (isAndroidHost || isIosHost) ...[ if (_isAndroid || _isIos) ...[
const Divider(height: 24), const Divider(height: 24),
const VoiceSectionHeader('Audio output'), const VoiceSectionHeader('Audio output'),
const AudioOutputTile(), const AudioOutputTile(),
], ],
// ── Audio devices (desktop only, SRS-026) ────────────── // ── Audio devices (desktop only, SRS-026) ──────────────
if (!isAndroidHost && !isIosHost) ...[ if (!_isAndroid && !_isIos) ...[
const Divider(height: 24), const Divider(height: 24),
const VoiceSectionHeader('Audio devices'), const VoiceSectionHeader('Audio devices'),
AudioDeviceListTile( const AudioDeviceListTile(
label: 'Input', label: 'Input',
kind: AudioDeviceKind.input, kind: AudioDeviceKind.input,
onDeviceChanged: widget.onInputDeviceChanged,
), ),
AudioDeviceListTile( const AudioDeviceListTile(
label: 'Output', label: 'Output',
kind: AudioDeviceKind.output, kind: AudioDeviceKind.output,
onDeviceChanged: widget.onOutputDeviceChanged,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@@ -1,10 +1,5 @@
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'audio_processing_config_state.dart';
import 'voice_platform.dart';
import '../src/rust/api.dart' as rust; import '../src/rust/api.dart' as rust;
/// Shared compact style for voice settings segmented buttons. /// Shared compact style for voice settings segmented buttons.
@@ -15,11 +10,7 @@ ButtonStyle voiceSegmentedButtonStyle(ThemeData theme) {
); );
} }
/// Transmit mode selector segments — full set, all three modes. /// Transmit mode selector segments.
///
/// This list is kept stable for legacy call sites and tests; UI
/// surfaces that must respect DEC-030 platform gating should prefer
/// [transmitModeSegmentsFor] with [voiceActivityTransmitAvailable].
const transmitModeSegments = [ const transmitModeSegments = [
ButtonSegment( ButtonSegment(
value: rust.BridgeTransmitMode.ptt, value: rust.BridgeTransmitMode.ptt,
@@ -38,43 +29,6 @@ const transmitModeSegments = [
), ),
]; ];
/// Transmit mode selector segments, optionally dropping the
/// voice-activity entry on hosts that do not ship a VAD pipeline.
///
/// Voice activity transmit is gated by [voiceActivityTransmitAvailable]
/// because the underlying VAD pipeline ships only on Windows, Linux, and
/// Android per DEC-030. Builds for unsupported platforms (iOS, macOS,
/// web) drop the VAD segment entirely so the UI never advertises a
/// transmit mode the engine cannot honour.
List<ButtonSegment<rust.BridgeTransmitMode>> transmitModeSegmentsFor({
required bool voiceActivityAvailable,
}) {
if (voiceActivityAvailable) return transmitModeSegments;
return const [
ButtonSegment(
value: rust.BridgeTransmitMode.ptt,
label: Text('PTT'),
icon: Icon(Icons.radio_button_checked, size: 14),
),
ButtonSegment(
value: rust.BridgeTransmitMode.continuous,
label: Text('Always'),
icon: Icon(Icons.podcasts, size: 14),
),
];
}
/// True when this host advertises VAD transmit per DEC-030.
///
/// The desktop Silero ONNX + WebRTC fallback ships on Windows and
/// Linux; the Android Oboe + WebRTC path covers Android; iOS uses the
/// Apple CoreML Silero VAD pipeline via `vad::apple_coreml`. macOS is
/// still gated until its VAD pipeline is confirmed.
bool get voiceActivityTransmitAvailable {
if (kIsWeb) return false;
return Platform.isWindows || Platform.isLinux || Platform.isAndroid || Platform.isIOS;
}
/// Android hardware/WebRTC selector segments. /// Android hardware/WebRTC selector segments.
const androidProcessingSegments = [ const androidProcessingSegments = [
ButtonSegment( ButtonSegment(
@@ -232,173 +186,3 @@ class AudioProcessingToggleRow extends StatelessWidget {
); );
} }
} }
typedef AudioFieldUpdater = void Function(VoidCallback mutation);
class AudioProcessingPanel extends StatelessWidget {
const AudioProcessingPanel({
super.key,
required this.config,
required this.update,
this.dense = false,
});
final AudioProcessingConfigState config;
final AudioFieldUpdater update;
final bool dense;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isAndroid = isAndroidHost;
final isIos = isIosHost;
final isMacOS = isMacOsHost;
final isDesktopSilero = isDesktopSileroVadHost;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (isAndroid) ...[
if (!dense) const VoiceSubHeader('Processing backend'),
if (dense) _compactLabel(context, 'Processing backend'),
SegmentedButton<bool>(
style: voiceSegmentedButtonStyle(theme),
segments: androidProcessingSegments,
selected: {config.preferHardware},
onSelectionChanged: (s) {
update(() => config.preferHardware = s.first);
},
),
const SizedBox(height: 4),
Text(
config.preferHardware
? (dense
? 'Hardware mode still keeps per-stage WebRTC fallback, so these controls remain effective.'
: 'Android hardware mode still falls back to WebRTC APM per stage when device effects are missing, so these controls remain available.')
: (dense
? 'Software mode applies the full WebRTC APM stage set.'
: 'Android software mode applies the full WebRTC APM control set.'),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
if (!dense) const SizedBox(height: 8),
],
if (!dense) const VoiceSubHeader('DSP stages'),
if (isIos) ...[
Text(
'iOS uses Apple VoiceProcessingIO. WebRTC APM controls are '
'hidden here; only settings that still affect the shipping '
'iOS path are shown.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
],
if (!isIos &&
(!isAndroid || androidShowsNsControl(config)))
AudioProcessingToggleRow(
dense: dense,
label: dense ? 'Noise suppression' : 'Noise suppression (NS)',
subtitle: dense
? 'Wiener filter'
: 'Wiener filter · stationary noise',
value: config.nsEnabled,
onChanged: (v) => update(() => config.nsEnabled = v),
),
if (!isIos &&
(!isAndroid || androidShowsAecControl(config)))
AudioProcessingToggleRow(
dense: dense,
label: dense ? 'Echo cancellation' : 'Echo cancellation (AEC3)',
subtitle: _aecSubtitle(isAndroid, isMacOS, config),
value: isMacOS ? true : config.aecEnabled,
onChanged:
isMacOS ? null : (v) => update(() => config.aecEnabled = v),
),
if (!isIos &&
(!isAndroid || androidShowsAgcControl(config)))
AudioProcessingToggleRow(
dense: dense,
label: dense ? 'Auto gain control' : 'Auto gain control (AGC2)',
subtitle: dense
? 'AGC2 · -18 dBFS target'
: 'RNN VAD-gated · -18 dBFS target',
value: config.agcEnabled,
onChanged: (v) => update(() => config.agcEnabled = v),
),
if (!isAndroid || androidShowsHpfControl(config))
AudioProcessingToggleRow(
dense: dense,
label: dense ? 'High-pass filter' : 'High-pass filter (HPF)',
subtitle: dense
? '80 Hz · DC removal'
: '80 Hz Butterworth · DC removal',
value: config.hpfEnabled,
onChanged: (v) => update(() => config.hpfEnabled = v),
),
if (!isIos &&
(!isAndroid || androidShowsLimiterControl(config)))
AudioProcessingToggleRow(
dense: dense,
label: 'Peak limiter',
subtitle: '-1 dBFS soft-knee · 2 ms look-ahead',
value: config.limiterEnabled,
onChanged: (v) => update(() => config.limiterEnabled = v),
),
if (!dense) ...[
const Divider(height: 24),
const VoiceSectionHeader('Voice activity detection (VAD)'),
const VoiceSubHeader('Backend'),
] else ...[
const SizedBox(height: 8),
Text(
'Voice activity detection (VAD)',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 2),
],
SegmentedButton<rust.BridgeVadBackend>(
style: voiceSegmentedButtonStyle(theme),
segments:
isDesktopSilero ? desktopVadBackendSegments : vadBackendSegments,
selected: {config.vadBackend},
onSelectionChanged: (s) {
update(() => config.vadBackend = s.first);
},
),
if (!dense) const SizedBox(height: 8),
],
);
}
String _aecSubtitle(bool isAndroid, bool isMacOS, AudioProcessingConfigState c) {
if (isAndroid) {
return c.preferHardware
? 'Prefers device/OS effect; falls back to WebRTC AEC3'
: 'WebRTC AEC3 · adaptive filter';
}
if (isMacOS) return 'Managed by platform VPIO';
return 'WebRTC AEC3 · adaptive filter';
}
Widget _compactLabel(BuildContext context, String text) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
text,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
}
@@ -1,6 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig"
// Mirror Flutter-Release.xcconfig (see explanation there).
OTHER_LDFLAGS = $(inherited) -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string
STRIP_STYLE = non-global
@@ -1,11 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig"
// macOS Release defaults to DEAD_CODE_STRIPPING = YES. See
// ios/Flutter/Release.xcconfig for the full rationale; -u is the
// load-bearing flag, -exported_symbol re-exports for dlsym, both
// are intentionally present per @_cdecl symbol.
OTHER_LDFLAGS = $(inherited) -Xlinker -u -Xlinker _chanora_silero_vad_create -Xlinker -u -Xlinker _chanora_silero_vad_destroy -Xlinker -u -Xlinker _chanora_silero_vad_reset -Xlinker -u -Xlinker _chanora_silero_vad_process -Xlinker -u -Xlinker _chanora_silero_vad_last_error -Xlinker -u -Xlinker _chanora_silero_vad_free_string -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string
// See ios/Flutter/Release.xcconfig for the STRIP_STYLE rationale.
STRIP_STYLE = non-global
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key><string>chanora_bridge</string>
<key>CFBundleIdentifier</key><string>app.chanora.bridge</string>
<key>CFBundleName</key><string>chanora_bridge</string>
<key>CFBundlePackageType</key><string>FMWK</string>
<key>CFBundleShortVersionString</key><string>1.0.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>CFBundleSupportedPlatforms</key><array><string>MacOSX</string></array>
<key>MinimumOSVersion</key><string>10.15</string>
</dict>
</plist>
@@ -0,0 +1 @@
Versions/Current/chanora_bridge
+38 -1
View File
@@ -1,20 +1,57 @@
PODS: PODS:
- audio_session (0.0.1):
- FlutterMacOS
- chanora_bridge (1.0.0) - chanora_bridge (1.0.0)
- connectivity_plus (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- package_info_plus (0.0.1):
- FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- chanora_bridge (from `/Users/edison/dev/chanora/apps/chanora_flutter/macos`) - chanora_bridge (from `/Users/edison/dev/chanora/apps/chanora_flutter/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
chanora_bridge: chanora_bridge:
:path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos" :path: "/Users/edison/dev/chanora/apps/chanora_flutter/macos"
connectivity_plus:
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
chanora_bridge: 9d1469952801a1caa3bb56d5d3bce91df8dca4ad audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb
@@ -32,7 +32,6 @@
45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; }; 45F255D1DE0134185DB5423D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */; };
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */; }; 9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */; };
C2DC22E19FDCE26B9D79442E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */; }; C2DC22E19FDCE26B9D79442E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -94,7 +93,6 @@
ED6F5EE0C5FD4DA22D79188C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; ED6F5EE0C5FD4DA22D79188C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EFEADEEFAB54DFEAAD7A70E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FDE04BBB936AA14C2B58FA0E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -110,7 +108,6 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
8C6000042DD0000000000001 /* SileroCoreML in Frameworks */, 8C6000042DD0000000000001 /* SileroCoreML in Frameworks */,
9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */, 9B86175918197ECC64969956 /* Pods_Runner.framework in Frameworks */,
); );
@@ -173,7 +170,6 @@
33CEB47122A05771004F2AC0 /* Flutter */ = { 33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
@@ -248,7 +244,6 @@
4287874B577AE59BBE39386D /* [CP] Check Pods Manifest.lock */, 4287874B577AE59BBE39386D /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */, 33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EA2044A3C60003C045 /* Frameworks */,
CA110002000000000000A200 /* Verify Silero Exports */,
33CC10EB2044A3C60003C045 /* Resources */, 33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */, 33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */, 3399D490228B24CF009A79C7 /* ShellScript */,
@@ -261,7 +256,6 @@
); );
name = Runner; name = Runner;
packageProductDependencies = ( packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
8C6000032DD0000000000001 /* SileroCoreML */, 8C6000032DD0000000000001 /* SileroCoreML */,
); );
productName = Runner; productName = Runner;
@@ -308,7 +302,6 @@
); );
mainGroup = 33CC10E42044A3C60003C045; mainGroup = 33CC10E42044A3C60003C045;
packageReferences = ( packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */, 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */,
); );
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
@@ -442,21 +435,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
CA110002000000000000A200 /* Verify Silero Exports */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Verify Silero Exports";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/../scripts/verify_silero_exports.sh\"\n";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -887,10 +865,6 @@
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = ../../../silero-coreml; relativePath = ../../../silero-coreml;
}; };
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -899,10 +873,6 @@
package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */; package = 8C6000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */;
productName = SileroCoreML; productName = SileroCoreML;
}; };
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 33CC10E52044A3C60003C045 /* Project object */; rootObject = 33CC10E52044A3C60003C045 /* Project object */;
@@ -5,24 +5,6 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "chanora_flutter.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -7,10 +7,6 @@ class AppDelegate: FlutterAppDelegate {
override func applicationDidFinishLaunching(_ notification: Notification) { override func applicationDidFinishLaunching(_ notification: Notification) {
super.applicationDidFinishLaunching(notification) super.applicationDidFinishLaunching(notification)
DispatchQueue.global(qos: .utility).async {
ChanoraSileroSelfTest.run()
}
// Ask for microphone access on launch rather than on first // Ask for microphone access on launch rather than on first
// voice-channel join. Matches user expectations for a voice // voice-channel join. Matches user expectations for a voice
// chat client and saves the user from a surprising prompt // chat client and saves the user from a surprising prompt
@@ -2,10 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<!-- Flutter default: app-sandbox + JIT for the Dart VM. network.server is <!-- Flutter default: app-sandbox + JIT for the Dart VM + server
required for the `flutter run` hot-reload listener AND, on macOS, for listener for `flutter run` hot-reload. -->
every UDP bind() the app does (tsclientlib binds 0.0.0.0:0 for
outbound TS3 traffic and the sandbox treats that as a server op). -->
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.cs.allow-jit</key> <key>com.apple.security.cs.allow-jit</key>
@@ -22,8 +22,6 @@
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
@@ -42,8 +40,6 @@
<string>Chanora uses Input Monitoring so push-to-talk keys work even when other apps are focused. Chanora never records what you type — only the key you bound for talking.</string> <string>Chanora uses Input Monitoring so push-to-talk keys work even when other apps are focused. Chanora never records what you type — only the key you bound for talking.</string>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Chanora needs local network access to connect to TeamSpeak-compatible voice servers.</string> <string>Chanora needs local network access to connect to TeamSpeak-compatible voice servers.</string>
<key>NSUserNotificationsUsageDescription</key>
<string>Chanora sends you a notification when another user pokes you.</string>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_ts3._tcp</string> <string>_ts3._tcp</string>
@@ -1,106 +0,0 @@
import Cocoa
import CoreAudio
import FlutterMacOS
import os.log
// ---------------------------------------------------------------------------
// MacOSAudioLifecycle
//
// Native-side MethodChannel handler for macOS audio lifecycle events.
// Closes the iOS / macOS asymmetry that the iOS AppDelegate handles via
// AVAudioSession (no AVAudioSession equivalent on macOS). The macOS
// equivalents of the iOS lifecycle events are:
//
// * Default audio device change Core Audio HAL default-input /
// default-output device property listeners (mirrors iOS route
// change). Posted as `handleDefaultDeviceChange` with payload
// `{role: 'input' | 'output'}`.
//
// * VPIO AudioUnit configuration change observed via the VPIO
// unit's `kAudioUnitProperty_StreamFormat` property listener.
// Posted as `handleConfigurationChange` with no payload.
//
// Channel name: `chanora/macos_audio_lifecycle`. The Dart side wires the
// matching `chanora/ios_audio_lifecycle`-shaped event surface in
// `audio_lifecycle_service.dart` and currently logs the events; the
// engine-restart call (FRB `macos_default_device_changed`) is a
// follow-up.
//
// Trace: SysRS-051 (macOS audio-lifecycle asymmetry), SysRS-311
// (the deferred macOS audio-lifecycle platform-adapter allocation).
// ---------------------------------------------------------------------------
private let kLogTag = "chanora_flutter.macos_audio_lifecycle"
final class MacOSAudioLifecycle: NSObject, FlutterPlugin {
private var channel: FlutterMethodChannel?
private var listenerBlocks: [AudioObjectID: AudioObjectPropertyListenerBlock] = [:]
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "chanora/macos_audio_lifecycle",
binaryMessenger: registrar.messenger
)
let instance = MacOSAudioLifecycle()
instance.channel = channel
registrar.addMethodCallDelegate(instance, channel: channel)
instance.startObserving()
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// macOS audio lifecycle is a one-way push: Swift Dart.
// The Dart side never invokes methods on this channel.
result(FlutterMethodNotImplemented)
}
// MARK: - Core Audio HAL listeners
private func startObserving() {
registerDefaultDeviceListener(role: "output")
registerDefaultDeviceListener(role: "input")
}
private func registerDefaultDeviceListener(role: String) {
let selector: AudioObjectPropertySelector = (role == "input")
? kAudioHardwarePropertyDefaultInputDevice
: kAudioHardwarePropertyDefaultOutputDevice
var address = AudioObjectPropertyAddress(
mSelector: selector,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
let objectID: AudioObjectID = AudioObjectID(kAudioObjectSystemObject)
let channel = self.channel
let block: AudioObjectPropertyListenerBlock = { _, _ in
os_log("default %{public}@ device changed", log: OSLog(subsystem: kLogTag, category: "lifecycle"), type: .info, role)
channel?.invokeMethod("handleDefaultDeviceChange", arguments: ["role": role])
}
let status = AudioObjectAddPropertyListener(objectID, &address, block, nil)
if status == noErr {
listenerBlocks[objectID + UInt32(role.hashValue & 0xFFFF)] = block
os_log("registered default %{public}@ device listener", log: OSLog(subsystem: kLogTag, category: "lifecycle"), type: .info, role)
} else {
os_log("failed to register default %{public}@ device listener (OSStatus %{public}d)",
log: OSLog(subsystem: kLogTag, category: "lifecycle"),
type: .error, role, Int(status))
}
}
deinit {
// Best-effort cleanup; AudioObjectRemovePropertyListener only
// matters if the system still holds the block.
for (id, block) in listenerBlocks {
for selector in [
kAudioHardwarePropertyDefaultInputDevice,
kAudioHardwarePropertyDefaultOutputDevice,
] {
var address = AudioObjectPropertyAddress(
mSelector: selector,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
_ = AudioObjectRemovePropertyListener(id, &address, block, nil)
}
}
}
}
@@ -48,7 +48,6 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
// -- Input Monitoring --------------------------------------------------- // -- Input Monitoring ---------------------------------------------------
case "checkInputMonitoring": case "checkInputMonitoring":
result(inputMonitoringStateString()) result(inputMonitoringStateString())
startInputMonitoringPolling()
case "requestInputMonitoring": case "requestInputMonitoring":
requestInputMonitoring(result: result) requestInputMonitoring(result: result)
@@ -63,18 +62,6 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
case "triggerLocalNetworkPrompt": case "triggerLocalNetworkPrompt":
triggerLocalNetworkPrompt(result: result) triggerLocalNetworkPrompt(result: result)
case "checkLocalNetworkAccess":
guard let args = call.arguments as? [String: Any],
let host = args["host"] as? String,
let port = args["port"] as? Int else {
result(FlutterError(
code: "INVALID_ARGS",
message: "checkLocalNetworkAccess requires host (String) and port (Int)",
details: nil))
return
}
checkLocalNetworkAccess(host: host, port: port, result: result)
// -- Notifications ------------------------------------------------------ // -- Notifications ------------------------------------------------------
case "checkNotifications": case "checkNotifications":
checkNotifications(result: result) checkNotifications(result: result)
@@ -211,10 +198,10 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
case .failed(let error): case .failed(let error):
if !resolved { if !resolved {
resolved = true resolved = true
// Check for DNS policy-denied error (kDNSServiceErr_PolicyDenied = -65570). let code = error.errorCode
// This is the canonical signal that the user denied the Local Network prompt. // POSIX permission-denied or network-down signals that
if case .dns(let dnsError) = error, // the user denied the Local Network prompt.
dnsError == DNSServiceErrorType(kDNSServiceErr_PolicyDenied) { if code == ENETDOWN || code == EACCES || code == EPERM {
result("Denied") result("Denied")
self?.channel?.invokeMethod("localNetworkStateChanged", arguments: [ self?.channel?.invokeMethod("localNetworkStateChanged", arguments: [
"state": "Denied", "state": "Denied",
@@ -226,23 +213,11 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
} }
} }
browser.cancel() browser.cancel()
case .waiting(let error): case .waiting:
// The browser is waiting for network. If the specific DNS error // The browser is waiting for network this is normal and
// is kDNSServiceErr_PolicyDenied, the user explicitly denied // may mean the permission dialog is showing. Don't resolve
// the Local Network prompt report immediately. // yet; wait for .ready or .failed or the timeout.
if case .dns(let dnsError) = error, break
dnsError == DNSServiceErrorType(kDNSServiceErr_PolicyDenied) {
if !resolved {
resolved = true
result("Denied")
self?.channel?.invokeMethod("localNetworkStateChanged", arguments: [
"state": "Denied",
])
}
browser.cancel()
}
// Otherwise the system dialog may be showing wait for
// .ready, .failed, or the timeout.
case .setup, .cancelled: case .setup, .cancelled:
break break
@unknown default: @unknown default:
@@ -271,77 +246,6 @@ class MacOSPermissionsHandler: NSObject, FlutterPlugin {
} }
} }
/// Probe whether Local Network access is currently denied for a specific
/// host:port by creating a short-lived NWConnection and checking
/// `unsatisfiedReason == .localNetworkDenied`. This does NOT trigger a
/// new system prompt it is a read-only check.
private func checkLocalNetworkAccess(
host: String, port: Int, result: @escaping FlutterResult
) {
if #available(macOS 15.0, *) {
checkLocalNetworkAccessImpl(host: host, port: port, result: result)
} else {
result("Unsupported")
}
}
@available(macOS 15.0, *)
private func checkLocalNetworkAccessImpl(
host: String, port: Int, result: @escaping FlutterResult
) {
guard let endpointPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
result("NotDetermined")
return
}
let endpointHost = NWEndpoint.Host(host)
let connection = NWConnection(
host: endpointHost, port: endpointPort, using: .tcp)
let queue = DispatchQueue(
label: "app.chanora.macos_permissions.local_network_check")
var didComplete = false
func finish(_ state: String) {
guard !didComplete else { return }
didComplete = true
connection.cancel()
result(state)
}
connection.stateUpdateHandler = { state in
switch state {
case .waiting, .failed:
if connection.currentPath?.unsatisfiedReason
== .localNetworkDenied {
finish("Denied")
} else {
finish("NotDetermined")
}
case .ready:
finish("Granted")
case .cancelled:
finish("NotDetermined")
case .setup, .preparing:
break
@unknown default:
break
}
}
connection.start(queue: queue)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
if !didComplete {
if connection.currentPath?.unsatisfiedReason
== .localNetworkDenied {
finish("Denied")
} else {
finish("NotDetermined")
}
}
}
}
// ========================================================================= // =========================================================================
// Notifications // Notifications
// ========================================================================= // =========================================================================
@@ -399,8 +303,6 @@ class MainFlutterWindow: NSWindow {
// Register the macOS permissions MethodChannel handler. // Register the macOS permissions MethodChannel handler.
MacOSPermissionsHandler.register(with: flutterViewController.registrar(forPlugin: "MacOSPermissionsHandler")) MacOSPermissionsHandler.register(with: flutterViewController.registrar(forPlugin: "MacOSPermissionsHandler"))
// Register the macOS audio-lifecycle MethodChannel handler (closes the iOS/macOS asymmetry in SysRS-051).
MacOSAudioLifecycle.register(with: flutterViewController.registrar(forPlugin: "MacOSAudioLifecycle"))
super.awakeFromNib() super.awakeFromNib()
} }
@@ -2,26 +2,12 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<!-- Release builds omit cs.allow-jit (Flutter hot-reload only). They keep <!-- Release builds omit the cs.allow-jit + network.server entitlements
network.server because the macOS App Sandbox classifies UDP bind() used only by Flutter's debug hot-reload. -->
against a local port - including the ephemeral 0.0.0.0:0 that
tsclientlib's tokio::net::UdpSocket::bind() issues for outbound
voice traffic - as a server operation that requires
com.apple.security.network.server, regardless of whether the
socket is later used only to sendto() a remote peer. Without it,
bind() returns EPERM and the sandbox log records
"Sandbox: chanora(...) deny(1) network-bind". network.client
alone gates outbound connect()-style flows (TCP, connected UDP)
and is insufficient for the bind()-then-sendto() pattern Tokio's
UdpSocket uses. See Apple's App Sandbox entitlement reference:
"Network Server" covers any process that listens on, or binds
to, a network port. -->
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
</dict> </dict>
@@ -1,5 +1,4 @@
import CoreML import CoreML
import Darwin
import Foundation import Foundation
import SileroCoreML import SileroCoreML
@@ -97,136 +96,3 @@ public func chanoraSileroVadFreeString(_ string: UnsafeMutablePointer<CChar>?) {
guard let string else { return } guard let string else { return }
free(string) free(string)
} }
@objc public final class ChanoraSileroSelfTest: NSObject {
// Validates the same code path the Rust framework uses: dlsym(RTLD_DEFAULT) for all
// six @_cdecl symbols, then exercises create -> reset -> process -> destroy. Catches
// the dead-strip / linker-export class of bug that broke TestFlight; calling the Swift
// functions directly would mask it because direct calls bypass the dynamic symbol table.
@objc public static func run() {
let started = DispatchTime.now()
// Static linker references: keep the Swift compiler / linker from
// dead-stripping the @_cdecl symbols under Whole-Module-Optimization
// + LTO in Archive builds. dlsym(RTLD_DEFAULT) below does NOT count
// as a static reference for the dead-stripper these `_ = ` lines
// do. Without them, TestFlight builds shipped without the symbols
// even though Debug builds (no LTO) worked.
//
// The `withoutActuallyEscaping` dance prevents the optimizer from
// proving the references are unused: assigning the function value
// to a `@convention(c)` typealias forces address-taken semantics.
_ = unsafeBitCast(
chanoraSileroVadCreate as @convention(c) () -> UnsafeMutableRawPointer?,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadDestroy as @convention(c) (UnsafeMutableRawPointer?) -> Void,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadReset as @convention(c) (UnsafeMutableRawPointer?) -> Int32,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadProcess
as @convention(c) (
UnsafeMutableRawPointer?, UnsafePointer<Float>?, Int,
UnsafeMutablePointer<Float>?
) -> Int32,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadLastError as @convention(c) () -> UnsafeMutablePointer<CChar>?,
to: UnsafeRawPointer.self,
)
_ = unsafeBitCast(
chanoraSileroVadFreeString as @convention(c) (UnsafeMutablePointer<CChar>?) -> Void,
to: UnsafeRawPointer.self,
)
typealias CreateFn = @convention(c) () -> UnsafeMutableRawPointer?
typealias DestroyFn = @convention(c) (UnsafeMutableRawPointer?) -> Void
typealias ResetFn = @convention(c) (UnsafeMutableRawPointer?) -> Int32
typealias ProcessFn = @convention(c) (
UnsafeMutableRawPointer?, UnsafePointer<Float>?, Int, UnsafeMutablePointer<Float>?
) -> Int32
typealias LastErrorFn = @convention(c) () -> UnsafeMutablePointer<CChar>?
typealias FreeStringFn = @convention(c) (UnsafeMutablePointer<CChar>?) -> Void
func resolve<T>(_ name: String, as type: T.Type) -> T? {
guard let raw = dlsym(UnsafeMutableRawPointer(bitPattern: -2), name) else {
return nil
}
return unsafeBitCast(raw, to: type)
}
let names = [
"chanora_silero_vad_create",
"chanora_silero_vad_destroy",
"chanora_silero_vad_reset",
"chanora_silero_vad_process",
"chanora_silero_vad_last_error",
"chanora_silero_vad_free_string",
]
let missing = names.filter { dlsym(UnsafeMutableRawPointer(bitPattern: -2), $0) == nil }
if !missing.isEmpty {
NSLog("chanora_flutter: SileroCoreML self-test FAILED dlsym missing=\(missing.joined(separator: ","))")
return
}
guard
let create = resolve("chanora_silero_vad_create", as: CreateFn.self),
let destroy = resolve("chanora_silero_vad_destroy", as: DestroyFn.self),
let reset = resolve("chanora_silero_vad_reset", as: ResetFn.self),
let process = resolve("chanora_silero_vad_process", as: ProcessFn.self),
let lastError = resolve("chanora_silero_vad_last_error", as: LastErrorFn.self),
let freeString = resolve("chanora_silero_vad_free_string", as: FreeStringFn.self)
else {
NSLog("chanora_flutter: SileroCoreML self-test FAILED unsafeBitCast resolution")
return
}
func readError() -> String {
guard let ptr = lastError() else { return "unknown" }
let msg = String(cString: ptr)
freeString(ptr)
return msg
}
guard let handle = create() else {
let elapsedMs = elapsedMs(since: started)
NSLog("chanora_flutter: SileroCoreML self-test FAILED at create err=\(readError()) elapsed_ms=\(elapsedMs)")
return
}
let resetRc = reset(handle)
if resetRc != 0 {
destroy(handle)
let elapsedMs = elapsedMs(since: started)
NSLog("chanora_flutter: SileroCoreML self-test FAILED at reset rc=\(resetRc) err=\(readError()) elapsed_ms=\(elapsedMs)")
return
}
let chunkSize = SileroVADRunner.chunkSize
var probability: Float = 0
let samples = [Float](repeating: 0, count: chunkSize)
let processRc = samples.withUnsafeBufferPointer { buf -> Int32 in
process(handle, buf.baseAddress, chunkSize, &probability)
}
destroy(handle)
let elapsedMs = elapsedMs(since: started)
if processRc == 0 {
NSLog("chanora_flutter: SileroCoreML self-test OK probability=\(probability) elapsed_ms=\(elapsedMs)")
} else {
NSLog("chanora_flutter: SileroCoreML self-test FAILED at process rc=\(processRc) err=\(readError()) elapsed_ms=\(elapsedMs)")
}
}
private static func elapsedMs(since start: DispatchTime) -> String {
let ns = DispatchTime.now().uptimeNanoseconds &- start.uptimeNanoseconds
return String(format: "%.1f", Double(ns) / 1_000_000.0)
}
}
@@ -52,25 +52,19 @@ Pod::Spec.new do |s|
echo "[chanora_bridge.podspec] cargo build aarch64-apple-darwin" echo "[chanora_bridge.podspec] cargo build aarch64-apple-darwin"
cd "$REPO_ROOT" cd "$REPO_ROOT"
PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \\ PATH="$HOME/.cargo/bin:$PATH" \\
MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \\ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \\
CMAKE_POLICY_VERSION_MINIMUM=3.5 \\ CMAKE_POLICY_VERSION_MINIMUM=3.5 \\
LIBOPUS_STATIC=1 \\ LIBOPUS_STATIC=1 \\
LIBOPUS_NO_PKG=1 \\ LIBOPUS_NO_PKG=1 \\
CARGO_PROFILE_RELEASE_DEBUG=true \\
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \\
CARGO_PROFILE_RELEASE_STRIP=false \\
cargo build --release --target aarch64-apple-darwin -p chanora_bridge cargo build --release --target aarch64-apple-darwin -p chanora_bridge
echo "[chanora_bridge.podspec] cargo build x86_64-apple-darwin" echo "[chanora_bridge.podspec] cargo build x86_64-apple-darwin"
PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \\ PATH="$HOME/.cargo/bin:$PATH" \\
MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \\ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \\
CMAKE_POLICY_VERSION_MINIMUM=3.5 \\ CMAKE_POLICY_VERSION_MINIMUM=3.5 \\
LIBOPUS_STATIC=1 \\ LIBOPUS_STATIC=1 \\
LIBOPUS_NO_PKG=1 \\ LIBOPUS_NO_PKG=1 \\
CARGO_PROFILE_RELEASE_DEBUG=true \\
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \\
CARGO_PROFILE_RELEASE_STRIP=false \\
cargo build --release --target x86_64-apple-darwin -p chanora_bridge cargo build --release --target x86_64-apple-darwin -p chanora_bridge
if [ ! -f "$BRIDGE_ARM64" ]; then if [ ! -f "$BRIDGE_ARM64" ]; then
@@ -118,19 +112,7 @@ PLIST
install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \\ install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \\
"$FW/Versions/A/chanora_bridge" "$FW/Versions/A/chanora_bridge"
echo "[chanora_bridge.podspec] framework ready at $FW"
# Generate the framework's dSYM bundle. Apple's archive validator
# rejects uploads when an embedded framework has no matching dSYM
# (UUID lookup miss in the archive's dSYMs/ folder). dsymutil reads
# the DWARF that cargo emitted (enabled by [profile.release]
# debug = true at the workspace root) and writes the bundle next
# to the framework. We then strip the in-framework binary so the
# shipped app stays slim; symbols live in the dSYM bundle, which
# is the layout xcodebuild -exportArchive and notarisation expect.
rm -rf "$FW.dSYM"
xcrun dsymutil "$FW/Versions/A/chanora_bridge" -o "$FW.dSYM"
xcrun strip -S -x "$FW/Versions/A/chanora_bridge"
echo "[chanora_bridge.podspec] framework + dSYM ready at $FW"
SCRIPT SCRIPT
# Pod CocoaPods picks this up; the framework gets embedded into # Pod CocoaPods picks this up; the framework gets embedded into
@@ -159,42 +141,32 @@ PLIST
echo "[chanora_bridge script_phase] cargo build aarch64-apple-darwin" echo "[chanora_bridge script_phase] cargo build aarch64-apple-darwin"
cd "$REPO_ROOT" cd "$REPO_ROOT"
PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \ PATH="$HOME/.cargo/bin:$PATH" \
MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \
CMAKE_POLICY_VERSION_MINIMUM=3.5 \ CMAKE_POLICY_VERSION_MINIMUM=3.5 \
LIBOPUS_STATIC=1 \ LIBOPUS_STATIC=1 \
LIBOPUS_NO_PKG=1 \ LIBOPUS_NO_PKG=1 \
CARGO_PROFILE_RELEASE_DEBUG=true \
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \
CARGO_PROFILE_RELEASE_STRIP=false \
cargo build --release --target aarch64-apple-darwin -p chanora_bridge cargo build --release --target aarch64-apple-darwin -p chanora_bridge
echo "[chanora_bridge script_phase] cargo build x86_64-apple-darwin" echo "[chanora_bridge script_phase] cargo build x86_64-apple-darwin"
PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \ PATH="$HOME/.cargo/bin:$PATH" \
MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \
CMAKE_POLICY_VERSION_MINIMUM=3.5 \ CMAKE_POLICY_VERSION_MINIMUM=3.5 \
LIBOPUS_STATIC=1 \ LIBOPUS_STATIC=1 \
LIBOPUS_NO_PKG=1 \ LIBOPUS_NO_PKG=1 \
CARGO_PROFILE_RELEASE_DEBUG=true \
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \
CARGO_PROFILE_RELEASE_STRIP=false \
cargo build --release --target x86_64-apple-darwin -p chanora_bridge cargo build --release --target x86_64-apple-darwin -p chanora_bridge
cd "$REPO_ROOT/apps/chanora_flutter/macos" cd "$REPO_ROOT/apps/chanora_flutter/macos"
FW=Frameworks/chanora_bridge.framework FW=Frameworks/chanora_bridge.framework
FW_UP_TO_DATE=0
# Skip the wrap step if the framework's binary is already # Skip the wrap step if the framework's binary is already
# up-to-date with the cargo output (fast no-op on incremental # up-to-date with the cargo output (fast no-op on incremental
# builds where Rust didn't change). We still publish the dSYM # builds where Rust didn't change).
# into DWARF_DSYM_FOLDER_PATH below so archive builds always
# have the symbols, even when the framework itself is cached.
if [ -f "$FW/Versions/A/chanora_bridge" ] && [ "$FW/Versions/A/chanora_bridge" -nt "$BRIDGE_ARM64" ] && [ "$FW/Versions/A/chanora_bridge" -nt "$BRIDGE_X86_64" ]; then if [ -f "$FW/Versions/A/chanora_bridge" ] && [ "$FW/Versions/A/chanora_bridge" -nt "$BRIDGE_ARM64" ] && [ "$FW/Versions/A/chanora_bridge" -nt "$BRIDGE_X86_64" ]; then
echo "[chanora_bridge script_phase] framework already up-to-date" echo "[chanora_bridge script_phase] framework already up-to-date"
FW_UP_TO_DATE=1 exit 0
fi fi
if [ "$FW_UP_TO_DATE" = 0 ]; then
# Create universal binary with lipo. # Create universal binary with lipo.
mkdir -p "$(dirname "$UNIVERSAL")" mkdir -p "$(dirname "$UNIVERSAL")"
lipo -create "$BRIDGE_ARM64" "$BRIDGE_X86_64" -output "$UNIVERSAL" lipo -create "$BRIDGE_ARM64" "$BRIDGE_X86_64" -output "$UNIVERSAL"
@@ -225,22 +197,7 @@ PLIST
install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \ install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \
"$FW/Versions/A/chanora_bridge" "$FW/Versions/A/chanora_bridge"
rm -rf "$FW.dSYM" echo "[chanora_bridge script_phase] framework refreshed"
xcrun dsymutil "$FW/Versions/A/chanora_bridge" -o "$FW.dSYM"
xcrun strip -S -x "$FW/Versions/A/chanora_bridge"
echo "[chanora_bridge script_phase] framework refreshed (with dSYM)"
fi
# Publish the dSYM into Xcode's archive dSYM folder on every
# build (cached or not). See the iOS podspec for the full
# rationale — same constraint applies to macOS notarisation
# and archive-based distribution.
if [ -n "${DWARF_DSYM_FOLDER_PATH:-}" ] && [ -d "$FW.dSYM" ]; then
mkdir -p "$DWARF_DSYM_FOLDER_PATH"
rm -rf "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM"
cp -R "$FW.dSYM" "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM"
echo "[chanora_bridge script_phase] dSYM published to $DWARF_DSYM_FOLDER_PATH"
fi
SCRIPT SCRIPT
:execution_position => :before_compile, :execution_position => :before_compile,
} }
+36 -76
View File
@@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc" sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "96.0.0" version: "93.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0" sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.2.0" version: "10.0.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -133,10 +133,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: code_assets name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -197,10 +197,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dbus name: dbus
sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.13" version: "0.7.12"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -262,46 +262,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: be38e3854d2baabcda8e16966a5fe8748cebb655bb94701494da0f052c2fc352
url: "https://pub.dev"
source: hosted
version: "22.0.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b"
url: "https://pub.dev"
source: hosted
version: "8.0.1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: ff0013eae795e8dc8fad4a8992a209e64d3ba2fbd8bf5e43c36bf448f95bd814
url: "https://pub.dev"
source: hosted
version: "12.0.0"
flutter_local_notifications_web:
dependency: transitive
description:
name: flutter_local_notifications_web
sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: "5aeed973a0c1480706784fad05c5c3a911335ebb561b2274b47fe80b375201e1"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -361,18 +321,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: haptic_kit name: haptic_kit
sha256: "457f825a3413be2651954639bed27bb2987570f75d90c4e8e1cb9be62db2e59d" sha256: "39efffa513c9f8ce3cdded8a4423797f69d71c9281779b83727337f3ee1ed9b8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.0"
hooks: hooks:
dependency: transitive dependency: transitive
description: description:
name: hooks name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "1.0.3"
http: http:
dependency: transitive dependency: transitive
description: description:
@@ -497,10 +457,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -509,6 +469,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nm: nm:
dependency: transitive dependency: transitive
description: description:
@@ -521,10 +489,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: objective_c name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.4.1" version: "9.3.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -697,10 +665,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0 sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.25" version: "2.4.23"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@@ -822,18 +790,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.10"
timezone:
dependency: transitive
description:
name: timezone
sha256: "784a5e34d2eb62e1326f24d6f600aaaee452eb8ca8ef2f384a59244e292d158b"
url: "https://pub.dev"
source: hosted
version: "0.11.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -854,10 +814,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.32" version: "6.3.30"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -966,10 +926,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.0" version: "6.2.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -982,10 +942,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "6.6.1"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@@ -995,5 +955,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.12.0 <4.0.0" dart: ">=3.11.5 <4.0.0"
flutter: ">=3.44.0" flutter: ">=3.38.4"
-1
View File
@@ -75,7 +75,6 @@ dependencies:
# DEC-003 iOS 13 floor; haptic_kit supports iOS 12+). # DEC-003 iOS 13 floor; haptic_kit supports iOS 12+).
haptic_kit: ^1.0.0 haptic_kit: ^1.0.0
flutter_foreground_task: ^9.2.2 flutter_foreground_task: ^9.2.2
flutter_local_notifications: ^22.0.0
url_launcher: ^6.3.2 url_launcher: ^6.3.2
shared_preferences: ^2.5.5 shared_preferences: ^2.5.5
share_plus: ^13.1.0 share_plus: ^13.1.0
@@ -1,91 +0,0 @@
#!/bin/sh
# verify_silero_exports.sh
#
# Asserts that all six chanora_silero_vad_* C symbols that the Rust
# chanora_bridge framework resolves via dlsym(RTLD_DEFAULT) are present
# in the linked app binary's dynamic export table — verified per
# architecture slice for macOS universal builds, because `nm` on a
# universal Mach-O without -arch will succeed if a symbol exists in ANY
# slice, not every slice. A missing symbol in just the x86_64 slice
# would silently break Intel Macs.
#
# Without this check, Xcode Archive's -dead_strip can remove these
# Swift @_cdecl symbols (no Swift caller exists) and CoreML VAD falls
# back to WebRTC on TestFlight/App Store with no compile-time,
# link-time, or runtime warning. We hit that bug once; this script
# ensures we never ship it again.
#
# Runs as an Xcode build phase after Link Binary, on iOS and macOS.
set -e
if [ -z "${TARGET_BUILD_DIR}" ] || [ -z "${EXECUTABLE_PATH}" ]; then
echo "error: verify_silero_exports.sh requires TARGET_BUILD_DIR and EXECUTABLE_PATH (run from Xcode build phase)" >&2
exit 1
fi
BINARY="${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}"
if [ ! -f "${BINARY}" ]; then
echo "error: app binary not found at ${BINARY}" >&2
exit 1
fi
# Flutter Debug builds split user code into a sibling `<App>.debug.dylib`
# alongside a tiny launcher executable; the @_cdecl symbols live in the
# dylib. Release/Profile builds put everything in the main executable.
# Prefer the dylib when both exist so the check verifies the slice that
# actually carries the symbols.
EXECUTABLE_DIR=$(dirname "${BINARY}")
EXECUTABLE_NAME=$(basename "${BINARY}")
DEBUG_DYLIB="${EXECUTABLE_DIR}/${EXECUTABLE_NAME}.debug.dylib"
if [ -f "${DEBUG_DYLIB}" ]; then
BINARY="${DEBUG_DYLIB}"
fi
REQUIRED_SYMBOLS="
_chanora_silero_vad_create
_chanora_silero_vad_destroy
_chanora_silero_vad_reset
_chanora_silero_vad_process
_chanora_silero_vad_last_error
_chanora_silero_vad_free_string
"
# Enumerate slices. `lipo -archs` prints arches space-separated for
# universal Mach-O; for thin binaries it prints the single arch.
ARCHS=$(xcrun lipo -archs "${BINARY}" 2>/dev/null || echo "")
if [ -z "${ARCHS}" ]; then
echo "error: xcrun lipo -archs failed for ${BINARY}; cannot enumerate slices" >&2
exit 1
fi
FAILED=0
for arch in ${ARCHS}; do
EXPORTED=$(xcrun nm -arch "${arch}" -gU "${BINARY}" 2>/dev/null | awk '{print $NF}')
if [ -z "${EXPORTED}" ]; then
echo "error: xcrun nm produced no output for arch=${arch} on ${BINARY}" >&2
FAILED=1
continue
fi
MISSING=""
for sym in ${REQUIRED_SYMBOLS}; do
if ! echo "${EXPORTED}" | grep -qx "${sym}"; then
MISSING="${MISSING} ${sym}"
fi
done
if [ -n "${MISSING}" ]; then
echo "error: chanora_silero_vad exports missing from $(basename "${BINARY}") [arch=${arch}]:${MISSING}" >&2
FAILED=1
fi
done
if [ "${FAILED}" -ne 0 ]; then
echo "error: Rust dlsym(RTLD_DEFAULT) resolution will fail and CoreML VAD will fall back to WebRTC." >&2
echo "error: Check OTHER_LDFLAGS -exported_symbol entries in Flutter/*.xcconfig and the @_cdecl exports in Runner/SileroCoreMLBridge.swift." >&2
exit 1
fi
echo "verify_silero_exports: all 6 chanora_silero_vad_* symbols present in $(basename "${BINARY}") [arches: ${ARCHS}]"
@@ -4,7 +4,7 @@ import 'package:chanora_flutter/services/audio_lifecycle_service.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust; import 'package:chanora_flutter/src/rust/api.dart' as rust;
void main() { void main() {
test('parseBridgeAudioRoute maps iOS-classified route strings', () { test('parseBridgeAudioRoute maps platform route names', () {
expect(parseBridgeAudioRoute('Earpiece'), rust.BridgeAudioRoute.earpiece); expect(parseBridgeAudioRoute('Earpiece'), rust.BridgeAudioRoute.earpiece);
expect(parseBridgeAudioRoute('Speaker'), rust.BridgeAudioRoute.speaker); expect(parseBridgeAudioRoute('Speaker'), rust.BridgeAudioRoute.speaker);
expect( expect(
@@ -22,34 +22,4 @@ void main() {
expect(parseBridgeAudioRoute('Unknown'), rust.BridgeAudioRoute.unknown); expect(parseBridgeAudioRoute('Unknown'), rust.BridgeAudioRoute.unknown);
expect(parseBridgeAudioRoute('Other'), rust.BridgeAudioRoute.unknown); expect(parseBridgeAudioRoute('Other'), rust.BridgeAudioRoute.unknown);
}); });
test('parseBridgeAudioRoute maps Android UsbHeadset to wiredHeadset', () {
// AndroidAudioLifecycleController.classifyDevice emits 'UsbHeadset' for
// AudioDeviceInfo.TYPE_USB_HEADSET. USB audio is functionally a
// wired-class device — the Kotlin classifier's own preference ordering
// (line 176 of AndroidAudioLifecycleController.kt) groups it with
// WiredHeadset/BluetoothHfp/BluetoothA2dp.
expect(parseBridgeAudioRoute('UsbHeadset'),
rust.BridgeAudioRoute.wiredHeadset);
});
test('parseBridgeAudioRoute maps Android Hdmi to unknown', () {
// HDMI is a display-out transport, not a voice-call audio path; no
// existing BridgeAudioRoute variant fits. Treat as unknown rather
// than misclassify as Speaker.
expect(parseBridgeAudioRoute('Hdmi'), rust.BridgeAudioRoute.unknown);
});
test('wireMacosAudioLifecycle is a no-op on non-macOS and registers on macOS', () {
// MethodChannel needs a binary messenger, which requires the test
// binding to be initialised first.
TestWidgetsFlutterBinding.ensureInitialized();
// Channel name constant matches the Swift side (MacOSAudioLifecycle.swift).
expect(macosAudioLifecycleChannelName, 'chanora/macos_audio_lifecycle');
// The wire is a no-op on the test platform (CI defaults to host OS
// which may be macOS or Linux). On Linux it returns early; on macOS
// it installs a handler. Either way, it must not throw.
expect(() => wireMacosAudioLifecycle(), returnsNormally);
});
} }
@@ -1,36 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/services/hard_mute_owners.dart';
void main() {
group('HardMuteOwners', () {
test('manual mute survives talk-power block and restore', () {
const owners = HardMuteOwners(manual: true);
final blocked = owners.copyWith(talkPower: true);
expect(blocked.effective, isTrue);
final restored = blocked.copyWith(talkPower: false);
expect(restored.manual, isTrue);
expect(restored.talkPower, isFalse);
expect(restored.effective, isTrue);
});
test('effective mute is the union of independent owners', () {
expect(const HardMuteOwners().effective, isFalse);
expect(const HardMuteOwners(manual: true).effective, isTrue);
expect(const HardMuteOwners(permission: true).effective, isTrue);
expect(const HardMuteOwners(talkPower: true).effective, isTrue);
});
test('bridge mute does not convert talk-power owner into manual owner', () {
const owners = HardMuteOwners(talkPower: true);
final synced = owners.withBridgeManualMute(true);
expect(synced.manual, isFalse);
expect(synced.talkPower, isTrue);
expect(synced.effective, isTrue);
});
});
}
@@ -1,99 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/services/ios_audio_session_controller.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('IosAudioSessionController', () {
const channel = MethodChannel(iosAudioSessionChannelName);
final messenger = TestDefaultBinaryMessengerBinding
.instance.defaultBinaryMessenger;
tearDown(() {
messenger.setMockMethodCallHandler(channel, null);
});
test('channel name matches Swift contract', () {
expect(iosAudioSessionChannelName, 'chanora/ios_audio_session');
});
test('activate invokes activateVoiceSession on iOS', () async {
final calls = <MethodCall>[];
messenger.setMockMethodCallHandler(channel, (call) async {
calls.add(call);
return null;
});
final controller = IosAudioSessionController(
channel: channel,
isIos: true,
);
await controller.activate();
expect(calls.map((c) => c.method), ['activateVoiceSession']);
expect(calls.single.arguments, isNull);
});
test('deactivate invokes deactivateVoiceSession on iOS', () async {
final calls = <MethodCall>[];
messenger.setMockMethodCallHandler(channel, (call) async {
calls.add(call);
return null;
});
final controller = IosAudioSessionController(
channel: channel,
isIos: true,
);
await controller.deactivate();
expect(calls.map((c) => c.method), ['deactivateVoiceSession']);
expect(calls.single.arguments, isNull);
});
test('activate is a no-op on non-iOS platforms', () async {
var invoked = false;
messenger.setMockMethodCallHandler(channel, (call) async {
invoked = true;
return null;
});
final controller = IosAudioSessionController(
channel: channel,
isIos: false,
);
await controller.activate();
await controller.deactivate();
expect(invoked, isFalse);
});
test('activate swallows PlatformException so engine keeps running',
() async {
messenger.setMockMethodCallHandler(channel, (call) async {
throw PlatformException(code: 'avaudiosession_failed');
});
final controller = IosAudioSessionController(
channel: channel,
isIos: true,
);
await expectLater(controller.activate(), completes);
await expectLater(controller.deactivate(), completes);
});
test('activate swallows MissingPluginException when channel is absent',
() async {
final controller = IosAudioSessionController(
channel: channel,
isIos: true,
);
await expectLater(controller.activate(), completes);
await expectLater(controller.deactivate(), completes);
});
});
}
@@ -301,61 +301,6 @@ void main() {
}, },
); );
test(
'SWE4-UV / SRS-300: checkLocalNetworkAccess() emits outbound '
'checkLocalNetworkAccess MethodCall with host and port arguments',
() async {
final svc = MacOSPermissionsService(channel: channel)..start();
outgoingResponder = (call) async {
if (call.method == methodCheckLocalNetworkAccess) {
return 'Denied';
}
return null;
};
final result = await svc.checkLocalNetworkAccess(
host: '192.168.1.42',
port: 9987,
);
final calls = outgoingCalls
.where((c) => c.method == methodCheckLocalNetworkAccess)
.toList();
expect(calls, hasLength(1));
final args = calls.single.arguments as Map;
expect(args['host'], '192.168.1.42');
expect(args['port'], 9987);
expect(result, MacOSLocalNetworkState.denied);
expect(svc.localNetworkState.value, MacOSLocalNetworkState.denied);
svc.dispose();
},
);
test(
'SWE4-UV / SRS-300: checkLocalNetworkAccess() parses Granted and updates '
'localNetworkState',
() async {
final svc = MacOSPermissionsService(channel: channel)..start();
outgoingResponder = (call) async {
if (call.method == methodCheckLocalNetworkAccess) {
return 'Granted';
}
return null;
};
final result = await svc.checkLocalNetworkAccess(
host: 'ts.example.com',
port: 9987,
);
expect(result, MacOSLocalNetworkState.granted);
expect(svc.localNetworkState.value, MacOSLocalNetworkState.granted);
svc.dispose();
},
);
test( test(
'SWE4-UV / SysRS-166: requestNotifications() emits outbound ' 'SWE4-UV / SysRS-166: requestNotifications() emits outbound '
'requestNotifications MethodCall; returns the platform response', 'requestNotifications MethodCall; returns the platform response',
@@ -557,30 +502,4 @@ void main() {
svc.dispose(); svc.dispose();
}, },
); );
test(
'SWE4-UV / SRS-300: checkLocalNetworkAccess() returns cached state '
'when the channel throws',
() async {
final svc = MacOSPermissionsService(channel: channel)..start();
outgoingResponder = (call) async {
if (call.method == methodCheckLocalNetworkAccess) {
throw PlatformException(code: 'probe-failed');
}
return null;
};
// Probe path failures (e.g. NWConnection couldn't establish a
// listener, or the Swift side threw) must not crash callers
// — they must fall back to whatever the service already cached.
final result = await svc.checkLocalNetworkAccess(
host: '127.0.0.1',
port: 9987,
);
expect(result, isNotNull);
svc.dispose();
},
);
} }
@@ -1,48 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/main.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
void main() {
test('active poke chat suppresses same sender notification only', () {
final sender = BigInt.from(42);
expect(
isPokeSenderActiveChat(
chatOpen: true,
inlineChatTarget: rust.BridgeMessageTarget.poke(sender),
senderId: sender,
),
isTrue,
);
expect(
isPokeSenderActiveChat(
chatOpen: true,
inlineChatTarget: rust.BridgeMessageTarget.poke(BigInt.from(7)),
senderId: sender,
),
isFalse,
);
});
test('active private chat also suppresses same sender poke notification', () {
final sender = BigInt.from(42);
expect(
isPokeSenderActiveChat(
chatOpen: true,
inlineChatTarget: rust.BridgeMessageTarget.client(sender),
senderId: sender,
),
isTrue,
);
expect(
isPokeSenderActiveChat(
chatOpen: false,
inlineChatTarget: rust.BridgeMessageTarget.client(sender),
senderId: sender,
),
isFalse,
);
});
}
@@ -1,81 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:chanora_flutter/services/poke_notification_service.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
late List<MethodCall> calls;
setUp(() {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
AndroidFlutterLocalNotificationsPlugin.registerWith();
calls = <MethodCall>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
calls.add(call);
return switch (call.method) {
'initialize' => true,
'requestNotificationsPermission' => true,
_ => null,
};
});
});
tearDown(() {
debugDefaultTargetPlatformOverride = null;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
test(
'show dispatches a silent poke notification with sender payload',
() async {
final service = PokeNotificationService();
await service.show(
senderName: 'Alice',
message: 'wake up',
senderId: BigInt.from(42),
strength: rust.BridgePokeStrength.strong,
);
final showCall = calls.singleWhere((call) => call.method == 'show');
final arguments = Map<Object?, Object?>.from(showCall.arguments as Map);
expect(arguments['id'], 42);
expect(arguments['title'], 'Poke from Alice');
expect(arguments['body'], 'wake up');
expect(arguments['payload'], 'poke:42');
final specifics = Map<Object?, Object?>.from(
arguments['platformSpecifics'] as Map,
);
expect(specifics['silent'], true);
expect(specifics['playSound'], false);
expect(specifics['groupKey'], 'chanora.pokes');
},
);
test('show uses fallback body for empty poke messages', () async {
final service = PokeNotificationService();
await service.show(
senderName: 'Alice',
message: ' ',
senderId: BigInt.from(42),
strength: rust.BridgePokeStrength.strong,
);
final showCall = calls.singleWhere((call) => call.method == 'show');
final arguments = Map<Object?, Object?>.from(showCall.arguments as Map);
expect(arguments['title'], 'Poke from Alice');
expect(arguments['body'], 'Alice pokes you');
expect(arguments['payload'], 'poke:42');
});
}
@@ -1,58 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chanora_flutter/services/poke_preferences_service.dart';
void main() {
late PokePreferencesService service;
setUp(() {
SharedPreferences.setMockInitialValues({});
service = PokePreferencesService();
});
test('loads defaults when preferences are unset', () async {
await service.load();
expect(service.pokesEnabled.value, isTrue);
expect(service.mutedSenders.value, isEmpty);
});
test('persists global enabled state', () async {
await service.load();
await service.setPokesEnabled(false);
final reloaded = PokePreferencesService();
await reloaded.load();
expect(reloaded.pokesEnabled.value, isFalse);
});
test('persists muted senders and removes them on unmute', () async {
await service.load();
final alice = BigInt.from(42);
final bob = BigInt.from(7);
await service.muteSender(alice);
await service.muteSender(bob);
await service.unmuteSender(alice);
final reloaded = PokePreferencesService();
await reloaded.load();
expect(reloaded.mutedSenders.value, {bob});
});
test('isMuted reflects in-memory changes synchronously', () async {
await service.load();
final sender = BigInt.from(99);
expect(service.isMuted(sender), isFalse);
await service.muteSender(sender);
expect(service.isMuted(sender), isTrue);
await service.unmuteSender(sender);
expect(service.isMuted(sender), isFalse);
});
}
@@ -67,44 +67,4 @@ void main() {
expect(settings.themeMode, UiThemeMode.system); expect(settings.themeMode, UiThemeMode.system);
}); });
test('loads null voice settings when unset', () async {
final settings = await service.loadSettings();
expect(settings.transmitModeIndex, isNull);
expect(settings.releaseTailMs, isNull);
expect(settings.inputDeviceId, isNull);
expect(settings.outputDeviceId, isNull);
});
test('saves and loads transmit mode index', () async {
await service.saveTransmitModeIndex(1);
final settings = await service.loadSettings();
expect(settings.transmitModeIndex, 1);
});
test('saves and loads release tail ms', () async {
await service.saveReleaseTailMs(300);
final settings = await service.loadSettings();
expect(settings.releaseTailMs, 300);
});
test('saves and loads audio device ids', () async {
await service.saveInputDeviceId('input-123');
await service.saveOutputDeviceId('output-456');
final settings = await service.loadSettings();
expect(settings.inputDeviceId, 'input-123');
expect(settings.outputDeviceId, 'output-456');
});
test('removes audio device id when set to null', () async {
await service.saveInputDeviceId('input-123');
await service.saveInputDeviceId(null);
final settings = await service.loadSettings();
expect(settings.inputDeviceId, isNull);
});
} }
@@ -1,123 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/services/voice_join_ordering.dart';
void main() {
group('joinVoiceChannelWithIosAudioSession', () {
test('activates the iOS audio session before Rust voiceJoin', () async {
final calls = <String>[];
await joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: 'secret',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
expect(channelId, BigInt.from(42));
expect(password, 'secret');
calls.add('voiceJoin');
},
);
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
});
test('deactivates the iOS audio session when Rust voiceJoin fails',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw StateError('join rejected');
},
),
throwsStateError,
);
expect(calls, [
'activateIosAudioSession',
'voiceJoin',
'deactivateIosAudioSession',
]);
});
test(
'keeps the iOS audio session active when voiceJoin throws but the '
'error is recognised as already-in-channel (treated as success); '
'still rethrows so the caller runs its success-on-already-joined branch',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw _FakeAlreadyInChannel();
},
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
),
throwsA(isA<_FakeAlreadyInChannel>()),
);
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
},
);
test(
'deactivates the iOS audio session when isJoinSuccess returns false '
'for a non-success error',
() async {
final calls = <String>[];
await expectLater(
joinVoiceChannelWithIosAudioSession(
channelId: BigInt.from(42),
password: '',
activateIosAudioSession: () async {
calls.add('activateIosAudioSession');
},
deactivateIosAudioSession: () async {
calls.add('deactivateIosAudioSession');
},
voiceJoin: ({required channelId, required password}) async {
calls.add('voiceJoin');
throw StateError('join rejected');
},
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
),
throwsStateError,
);
expect(calls, [
'activateIosAudioSession',
'voiceJoin',
'deactivateIosAudioSession',
]);
},
);
});
}
class _FakeAlreadyInChannel implements Exception {}
@@ -1,201 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
import 'package:chanora_flutter/widgets/chat_panel.dart';
import 'package:chanora_flutter/widgets/chat_views.dart';
void main() {
rust.BridgeSnapshot snapshot() {
final channelId = BigInt.from(10);
return rust.BridgeSnapshot(
serverName: 'Server',
welcomeMessage: '',
platform: '',
version: '',
channels: [
rust.BridgeChannel(
id: channelId,
parent: BigInt.zero,
name: 'Lobby',
order: 0,
hasPassword: false,
neededTalkPower: 0,
),
],
clients: [
rust.BridgeClient(
id: BigInt.one,
channel: channelId,
name: 'Me',
inputMuted: false,
outputMuted: false,
isSpeaking: false,
isServerQuery: false,
talkPower: 0,
talkPowerGranted: true,
),
],
ownClientId: BigInt.one,
);
}
testWidgets('inline chat panel renders target, messages, and close action', (
tester,
) async {
var closed = false;
final messages = [
ChatEntry(
senderId: BigInt.from(2),
senderName: 'Alice',
message: 'Hello from channel',
target: const rust.BridgeMessageTarget.channel(),
),
];
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ChatPanel(
messages: messages,
snapshot: snapshot(),
target: const rust.BridgeMessageTarget.channel(),
clientName: '',
onClose: () => closed = true,
),
),
),
);
expect(find.text('# Lobby'), findsOneWidget);
expect(find.text('Hello from channel'), findsOneWidget);
await tester.tap(find.byTooltip('Close chat'));
await tester.pump();
expect(closed, isTrue);
});
testWidgets('chat detail restores target drafts when the target changes', (
tester,
) async {
String? savedDraft;
final messages = <ChatEntry>[];
Widget detail({
required rust.BridgeMessageTarget target,
required String? restoredDraft,
}) {
return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ChatDetailView(
messages: messages,
snapshot: snapshot(),
target: target,
clientName: '',
currentChannelId: BigInt.from(10),
channelName: 'Lobby',
restoredDraft: restoredDraft,
onDraftChanged: (text) => savedDraft = text,
),
),
);
}
await tester.pumpWidget(
detail(
target: const rust.BridgeMessageTarget.channel(),
restoredDraft: 'channel draft',
),
);
expect(
tester.widget<TextField>(find.byType(TextField)).controller!.text,
'channel draft',
);
await tester.enterText(find.byType(TextField), 'typed channel draft');
await tester.pumpWidget(
detail(
target: const rust.BridgeMessageTarget.server(),
restoredDraft: 'server draft',
),
);
expect(savedDraft, 'typed channel draft');
expect(
tester.widget<TextField>(find.byType(TextField)).controller!.text,
'server draft',
);
});
testWidgets(
'chat detail propagates empty draft when the user clears it before switching target',
(tester) async {
// Regression: previously, _ChatDetailViewState only emitted
// onDraftChanged when the text was non-empty. If the user
// restored a saved draft, deleted it, then switched target,
// the stale entry stayed in the parent's draft map and
// resurrected on the next target swap.
String? savedDraft = 'sentinel-unset';
final messages = <ChatEntry>[];
Widget detail({
required rust.BridgeMessageTarget target,
required String? restoredDraft,
}) {
return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ChatDetailView(
messages: messages,
snapshot: snapshot(),
target: target,
clientName: '',
currentChannelId: BigInt.from(10),
channelName: 'Lobby',
restoredDraft: restoredDraft,
onDraftChanged: (text) => savedDraft = text,
),
),
);
}
await tester.pumpWidget(
detail(
target: const rust.BridgeMessageTarget.channel(),
restoredDraft: 'previously saved channel draft',
),
);
expect(
tester.widget<TextField>(find.byType(TextField)).controller!.text,
'previously saved channel draft',
);
// User clears the field, then switches target.
await tester.enterText(find.byType(TextField), '');
await tester.pumpWidget(
detail(
target: const rust.BridgeMessageTarget.server(),
restoredDraft: null,
),
);
// The empty string MUST reach the parent so the stale entry
// is overwritten in the draft map. With the previous guarded
// implementation, savedDraft would still hold the sentinel.
expect(
savedDraft,
'',
reason: 'empty draft must overwrite stale entry on target swap',
);
},
);
}
@@ -455,7 +455,7 @@ void main() {
channelName: '', channelName: '',
clientName: 'Alpha', clientName: 'Alpha',
), ),
'Poke message optional...', 'Poke message...',
); );
}); });
@@ -737,179 +737,6 @@ void main() {
refresh.dispose(); refresh.dispose();
}); });
test('evaluates target-aware chat message send policy', () {
final clientTarget = rust.BridgeMessageTarget.client(BigInt.from(2));
final pokeTarget = rust.BridgeMessageTarget.poke(BigInt.from(2));
expect(canSendChatMessage(pokeTarget, null, ''), isTrue);
expect(canSendChatMessage(pokeTarget, null, ' '), isTrue);
expect(canSendChatMessage(pokeTarget, null, 'wake up'), isTrue);
expect(
canSendChatMessage(const rust.BridgeMessageTarget.server(), null, ''),
isFalse,
);
expect(
canSendChatMessage(
const rust.BridgeMessageTarget.channel(),
BigInt.from(10),
'',
),
isFalse,
);
expect(canSendChatMessage(clientTarget, null, ''), isFalse);
expect(
canSendChatMessage(
const rust.BridgeMessageTarget.channel(),
null,
'hello',
),
isFalse,
);
expect(
canSendChatMessage(
const rust.BridgeMessageTarget.channel(),
BigInt.from(10),
'hello',
),
isTrue,
);
});
testWidgets('poke detail sends an empty poke when the composer is empty', (
tester,
) async {
String? sentMessage;
rust.BridgeMessageTarget? sentTarget;
final messages = <ChatEntry>[];
final target = rust.BridgeMessageTarget.poke(BigInt.from(2));
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ChatDetailView(
messages: messages,
snapshot: snapshot(
channels: const [],
clients: [
client(id: BigInt.one, name: 'Me', channelId: BigInt.zero),
],
),
target: target,
clientName: 'Alpha',
currentChannelId: null,
channelName: '',
sendChatMessage: ({required message, required target}) async {
sentMessage = message;
sentTarget = target;
},
),
),
),
);
expect(find.byTooltip('Poke'), findsOneWidget);
expect(find.byTooltip('Send'), findsNothing);
await tester.tap(find.byTooltip('Poke'));
await tester.pump();
expect(sentMessage, '');
expect(sentTarget, target);
expect(messages, hasLength(1));
expect(messages.single.isPoke, isTrue);
expect(messages.single.message, '');
expect(find.textContaining('You poked "Alpha"'), findsOneWidget);
expect(find.byType(CircleAvatar), findsNothing);
});
testWidgets('poke detail sends typed optional poke message', (tester) async {
String? sentMessage;
rust.BridgeMessageTarget? sentTarget;
final messages = <ChatEntry>[];
final target = rust.BridgeMessageTarget.poke(BigInt.from(2));
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ChatDetailView(
messages: messages,
snapshot: snapshot(
channels: const [],
clients: [
client(id: BigInt.one, name: 'Me', channelId: BigInt.zero),
],
),
target: target,
clientName: 'Alpha',
currentChannelId: null,
channelName: '',
sendChatMessage: ({required message, required target}) async {
sentMessage = message;
sentTarget = target;
},
),
),
),
);
await tester.enterText(find.byType(TextField), 'wake up');
await tester.tap(find.byTooltip('Poke'));
await tester.pump();
expect(sentMessage, 'wake up');
expect(sentTarget, target);
expect(messages.single.message, 'wake up');
expect(
find.textContaining('You poked "Alpha" with message: wake up'),
findsOneWidget,
);
});
testWidgets('channel detail blocks empty sends with a joined channel', (
tester,
) async {
var sendCount = 0;
final messages = <ChatEntry>[];
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ChatDetailView(
messages: messages,
snapshot: snapshot(
channels: [channel(BigInt.from(10), 'Lobby')],
clients: [
client(id: BigInt.one, name: 'Me', channelId: BigInt.from(10)),
],
),
target: const rust.BridgeMessageTarget.channel(),
clientName: '',
currentChannelId: BigInt.from(10),
channelName: 'Lobby',
sendChatMessage: ({required message, required target}) async {
sendCount++;
},
),
),
),
);
expect(find.byTooltip('Send'), findsOneWidget);
await tester.tap(find.byTooltip('Send'));
await tester.pump();
expect(sendCount, 0);
expect(messages, isEmpty);
});
test('blocks channel chat when no voice channel is joined', () { test('blocks channel chat when no voice channel is joined', () {
expect( expect(
canSendToChatTarget(const rust.BridgeMessageTarget.channel(), null), canSendToChatTarget(const rust.BridgeMessageTarget.channel(), null),
@@ -1,205 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
import 'package:chanora_flutter/widgets/connect_widgets.dart';
void main() {
group('ConnectForm', () {
late TextEditingController hostCtl;
late TextEditingController nickCtl;
late TextEditingController passwordCtl;
Widget buildForm({
VoidCallback? onConnect,
VoidCallback? onAddBookmark,
}) {
return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: ConnectForm(
hostCtl: hostCtl,
nickCtl: nickCtl,
passwordCtl: passwordCtl,
onConnect: onConnect ?? () {},
onAddBookmark: onAddBookmark ?? () {},
),
),
);
}
setUp(() {
hostCtl = TextEditingController();
nickCtl = TextEditingController();
passwordCtl = TextEditingController();
});
tearDown(() {
hostCtl.dispose();
nickCtl.dispose();
passwordCtl.dispose();
});
testWidgets('renders all three text fields', (tester) async {
await tester.pumpWidget(buildForm());
expect(find.byType(TextField), findsNWidgets(3));
expect(find.byIcon(Icons.dns_outlined), findsOneWidget);
expect(find.byIcon(Icons.login), findsOneWidget);
expect(find.byIcon(Icons.bookmark_add_outlined), findsOneWidget);
});
testWidgets('connect callback fires on button tap', (tester) async {
var connected = false;
await tester.pumpWidget(buildForm(
onConnect: () => connected = true,
));
await tester.tap(find.byIcon(Icons.login));
expect(connected, isTrue);
});
testWidgets('bookmark callback fires on button tap', (tester) async {
var bookmarked = false;
await tester.pumpWidget(buildForm(
onAddBookmark: () => bookmarked = true,
));
await tester.tap(find.byIcon(Icons.bookmark_add_outlined));
expect(bookmarked, isTrue);
});
testWidgets('host field lowercases and strips whitespace', (tester) async {
await tester.pumpWidget(buildForm());
await tester.enterText(
find.widgetWithText(TextField, 'host[:port]'),
' MyServer.COM ',
);
await tester.pump();
expect(hostCtl.text, 'myserver.com');
});
testWidgets('password field is obscured', (tester) async {
await tester.pumpWidget(buildForm());
final passwordField = tester.widgetList<TextField>(
find.byType(TextField),
).last;
expect(passwordField.obscureText, isTrue);
});
testWidgets('form uses outlined border decoration', (tester) async {
await tester.pumpWidget(buildForm());
final fields = tester.widgetList<TextField>(find.byType(TextField));
for (final field in fields) {
final decoration = field.decoration as InputDecoration;
expect(decoration.border, isA<OutlineInputBorder>());
}
});
});
group('BookmarkList', () {
Widget buildBookmarkList({
required List<rust.BridgeBookmark> bookmarks,
ValueChanged<rust.BridgeBookmark>? onConnect,
ValueChanged<rust.BridgeBookmark>? onDelete,
}) {
return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: SingleChildScrollView(
child: BookmarkList(
bookmarks: bookmarks,
onConnect: onConnect ?? (_) {},
onDelete: onDelete ?? (_) {},
),
),
),
);
}
const testBookmark = rust.BridgeBookmark(
id: 1,
displayName: 'My Server',
host: 'ts.example.com',
nickname: 'TestUser',
password: '',
);
const testBookmark2 = rust.BridgeBookmark(
id: 2,
displayName: 'Work Server',
host: 'work.ts.com',
nickname: 'WorkNick',
password: 'secret',
);
testWidgets('shows empty message when no bookmarks', (tester) async {
await tester.pumpWidget(buildBookmarkList(bookmarks: const []));
expect(find.byType(BookmarkList), findsOneWidget);
expect(find.byType(Card), findsNothing);
});
testWidgets('renders bookmark cards with name and host', (tester) async {
await tester.pumpWidget(buildBookmarkList(
bookmarks: const [testBookmark],
));
expect(find.text('My Server'), findsOneWidget);
expect(find.text('ts.example.com — TestUser'), findsOneWidget);
expect(find.byType(Card), findsOneWidget);
});
testWidgets('renders multiple bookmarks', (tester) async {
await tester.pumpWidget(buildBookmarkList(
bookmarks: const [testBookmark, testBookmark2],
));
expect(find.text('My Server'), findsOneWidget);
expect(find.text('Work Server'), findsOneWidget);
expect(find.byType(Card), findsNWidgets(2));
});
testWidgets('connect callback fires with correct bookmark', (tester) async {
rust.BridgeBookmark? connectedBookmark;
await tester.pumpWidget(buildBookmarkList(
bookmarks: const [testBookmark],
onConnect: (b) => connectedBookmark = b,
));
final connectButtons = find.byIcon(Icons.login);
await tester.tap(connectButtons.first);
expect(connectedBookmark, testBookmark);
});
testWidgets('delete callback fires with correct bookmark', (tester) async {
rust.BridgeBookmark? deletedBookmark;
await tester.pumpWidget(buildBookmarkList(
bookmarks: const [testBookmark],
onDelete: (b) => deletedBookmark = b,
));
final deleteButtons = find.byIcon(Icons.delete_outline);
await tester.tap(deleteButtons.first);
expect(deletedBookmark, testBookmark);
});
testWidgets('each bookmark card has connect and delete buttons', (tester) async {
await tester.pumpWidget(buildBookmarkList(
bookmarks: const [testBookmark],
));
expect(find.byIcon(Icons.login), findsOneWidget);
expect(find.byIcon(Icons.delete_outline), findsOneWidget);
});
});
}
@@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/services/poke_notification_service.dart';
import 'package:chanora_flutter/services/poke_preferences_service.dart';
import 'package:chanora_flutter/widgets/poke_notification_settings.dart';
void main() {
testWidgets('toggles poke notifications and unmutes senders', (tester) async {
SharedPreferences.setMockInitialValues({});
final preferences = PokePreferencesService();
await preferences.load();
await preferences.muteSender(BigInt.from(42));
addTearDown(preferences.dispose);
final notificationService = PokeNotificationService();
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: PokeNotificationSettingsDialog(
preferences: preferences,
notificationService: notificationService,
),
),
);
expect(find.text('Poke notifications'), findsOneWidget);
expect(find.text('Client ID 42'), findsOneWidget);
await tester.tap(find.byType(Switch));
await tester.pumpAndSettle();
expect(preferences.pokesEnabled.value, isFalse);
await tester.tap(find.text('Unmute'));
await tester.pumpAndSettle();
expect(preferences.isMuted(BigInt.from(42)), isFalse);
expect(find.text('No muted poke senders.'), findsOneWidget);
});
}
@@ -52,13 +52,11 @@ void main() {
String welcomeMessage = '', String welcomeMessage = '',
BigInt? ownClientId, BigInt? ownClientId,
BigInt? currentVoiceChannelId, BigInt? currentVoiceChannelId,
Set<BigInt> unreadChannelIds = const {},
rust.BridgeAudioStats? audioStats, rust.BridgeAudioStats? audioStats,
bool enableClientLongPressMenu = false, bool enableClientLongPressMenu = false,
ValueChanged<rust.BridgeClient>? onOpenClientInfo, ValueChanged<rust.BridgeClient>? onOpenClientInfo,
ValueChanged<rust.BridgeClient>? onOpenClientChat, ValueChanged<rust.BridgeClient>? onOpenClientChat,
ValueChanged<rust.BridgeClient>? onOpenClientPoke, ValueChanged<rust.BridgeClient>? onOpenClientPoke,
ValueChanged<rust.BridgeChannel>? onOpenChannelChat,
}) { }) {
return MaterialApp( return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates, localizationsDelegates: AppL10n.localizationsDelegates,
@@ -81,14 +79,12 @@ void main() {
localOutputMuted: false, localOutputMuted: false,
hasJoinPending: false, hasJoinPending: false,
canJoinVoiceChannel: true, canJoinVoiceChannel: true,
unreadChannelIds: unreadChannelIds,
onJoinChannel: (_) {}, onJoinChannel: (_) {},
onJoinChannelWithPassword: (_) {}, onJoinChannelWithPassword: (_) {},
enableClientLongPressMenu: enableClientLongPressMenu, enableClientLongPressMenu: enableClientLongPressMenu,
onOpenClientInfo: onOpenClientInfo, onOpenClientInfo: onOpenClientInfo,
onOpenClientChat: onOpenClientChat, onOpenClientChat: onOpenClientChat,
onOpenClientPoke: onOpenClientPoke, onOpenClientPoke: onOpenClientPoke,
onOpenChannelChat: onOpenChannelChat,
), ),
), ),
); );
@@ -137,32 +133,6 @@ void main() {
); );
}); });
testWidgets('renders unread dot on channels with unread chat messages', (
tester,
) async {
await tester.pumpWidget(
snapshotHarness(
channels: [channel(id: 1, name: 'Lobby')],
clients: const [],
unreadChannelIds: {BigInt.one},
),
);
await tester.pumpAndSettle();
expect(
find.byWidgetPredicate(
(widget) =>
widget is Container &&
widget.constraints?.maxWidth == 8 &&
widget.constraints?.maxHeight == 8 &&
widget.decoration is BoxDecoration &&
(widget.decoration! as BoxDecoration).shape == BoxShape.circle,
),
findsOneWidget,
);
});
testWidgets('collapsing a channel hides users and child channels', ( testWidgets('collapsing a channel hides users and child channels', (
tester, tester,
) async { ) async {
@@ -523,7 +493,6 @@ void main() {
framesSent: 1, framesSent: 1,
framesReceived: 0, framesReceived: 0,
pttActive: true, pttActive: true,
inputLevel: -30.0,
), ),
), ),
); );
@@ -580,7 +549,6 @@ void main() {
localOutputMuted: false, localOutputMuted: false,
hasJoinPending: false, hasJoinPending: false,
canJoinVoiceChannel: true, canJoinVoiceChannel: true,
unreadChannelIds: const {},
onJoinChannel: (channel) => tapped = channel, onJoinChannel: (channel) => tapped = channel,
onJoinChannelWithPassword: (_) {}, onJoinChannelWithPassword: (_) {},
), ),
@@ -608,64 +576,6 @@ void main() {
expect(tapped!.neededTalkPower, 12); expect(tapped!.neededTalkPower, 12);
}); });
testWidgets('channel context menu opens chat without replacing voice join', (
tester,
) async {
final lobby = channel(id: 1, name: 'Lobby');
rust.BridgeChannel? joined;
rust.BridgeChannel? openedChat;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: SnapshotView(
snapshot: rust.BridgeSnapshot(
serverName: 'Server',
welcomeMessage: '',
platform: '',
version: '',
channels: [lobby],
clients: const [],
ownClientId: BigInt.one,
),
audioStats: null,
currentVoiceChannelId: null,
pendingVoiceChannelId: null,
localInputMuted: false,
localOutputMuted: false,
hasJoinPending: false,
canJoinVoiceChannel: true,
unreadChannelIds: const {},
onJoinChannel: (channel) => joined = channel,
onJoinChannelWithPassword: (_) {},
onOpenChannelChat: (channel) => openedChat = channel,
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Lobby'));
await tester.pump();
expect(joined, lobby);
expect(openedChat, isNull);
expect(find.text('Chat'), findsNothing);
await tester.tap(find.text('Lobby'), buttons: kSecondaryMouseButton);
await tester.pumpAndSettle();
expect(find.text('Chat'), findsOneWidget);
await tester.tap(find.text('Chat'));
await tester.pumpAndSettle();
expect(openedChat, lobby);
});
testWidgets('separator spacers render as line painters without raw text', ( testWidgets('separator spacers render as line painters without raw text', (
tester, tester,
) async { ) async {
@@ -700,7 +610,6 @@ void main() {
localOutputMuted: false, localOutputMuted: false,
hasJoinPending: false, hasJoinPending: false,
canJoinVoiceChannel: true, canJoinVoiceChannel: true,
unreadChannelIds: const {},
onJoinChannel: (_) {}, onJoinChannel: (_) {},
onJoinChannelWithPassword: (_) {}, onJoinChannelWithPassword: (_) {},
), ),
@@ -755,7 +664,6 @@ void main() {
localOutputMuted: false, localOutputMuted: false,
hasJoinPending: false, hasJoinPending: false,
canJoinVoiceChannel: true, canJoinVoiceChannel: true,
unreadChannelIds: const {},
onJoinChannel: (_) {}, onJoinChannel: (_) {},
onJoinChannelWithPassword: (_) {}, onJoinChannelWithPassword: (_) {},
), ),
@@ -1,170 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:chanora_flutter/l10n/generated/app_localizations.dart';
import 'package:chanora_flutter/src/rust/api.dart' as rust;
import 'package:chanora_flutter/widgets/voice_bar.dart';
void main() {
const defaultStats = rust.BridgeAudioStats(
framesSent: 100,
framesReceived: 200,
pttActive: false,
inputLevel: -30.0,
);
Widget buildVoiceBar({
bool inChannel = true,
rust.BridgeTransmitMode transmitMode = rust.BridgeTransmitMode.ptt,
bool hardMute = false,
bool outputMuted = false,
bool talkPowerBlocked = false,
int releaseTailMs = 150,
String channelName = 'Test Channel',
rust.BridgeAudioStats? audioStats = defaultStats,
double? inputLevel,
String pttLevel = 'L1WindowsHook',
String pttBackendId = 'windows-raw-input',
String pttBoundInputClass = 'keyboard',
String pttBoundKeyLabel = 'Space',
VoidCallback? onConfigure,
ValueChanged<bool>? onPttHeldChanged,
}) {
return MaterialApp(
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Scaffold(
body: SingleChildScrollView(
child: VoiceBar(
inChannel: inChannel,
transmitMode: transmitMode,
hardMute: hardMute,
outputMuted: outputMuted,
talkPowerBlocked: talkPowerBlocked,
releaseTailMs: releaseTailMs,
channelName: channelName,
audioStats: audioStats,
inputLevel: inputLevel,
pttLevel: pttLevel,
pttBackendId: pttBackendId,
pttBoundInputClass: pttBoundInputClass,
pttBoundKeyLabel: pttBoundKeyLabel,
onConfigure: onConfigure ?? () {},
onPttHeldChanged: onPttHeldChanged ?? (_) {},
),
),
),
);
}
testWidgets('VoiceBar renders in PTT mode with stats', (tester) async {
await tester.pumpWidget(buildVoiceBar());
expect(find.byType(VoiceBar), findsOneWidget);
expect(find.byIcon(Icons.radio_button_checked), findsOneWidget);
expect(find.byIcon(Icons.tune), findsOneWidget);
});
testWidgets('VoiceBar renders in continuous mode', (tester) async {
await tester.pumpWidget(buildVoiceBar(
transmitMode: rust.BridgeTransmitMode.continuous,
));
expect(find.byType(VoiceBar), findsOneWidget);
expect(find.byIcon(Icons.podcasts), findsOneWidget);
});
testWidgets('VoiceBar shows channel status when in channel', (tester) async {
await tester.pumpWidget(buildVoiceBar(inChannel: true));
expect(find.byType(VoiceBar), findsOneWidget);
});
testWidgets('VoiceBar hides channel status when not in channel', (tester) async {
await tester.pumpWidget(buildVoiceBar(
inChannel: false,
channelName: '',
));
expect(find.byType(VoiceBar), findsOneWidget);
});
testWidgets('VoiceBar shows talk power blocked indicator', (tester) async {
await tester.pumpWidget(buildVoiceBar(
talkPowerBlocked: true,
));
expect(find.text('Insufficient talk power'), findsOneWidget);
});
testWidgets('VoiceBar shows audio stats line when stats available', (tester) async {
await tester.pumpWidget(buildVoiceBar());
expect(find.byType(VoiceBar), findsOneWidget);
});
testWidgets('VoiceBar hides audio stats line when stats null', (tester) async {
await tester.pumpWidget(buildVoiceBar(
audioStats: null,
));
expect(find.byType(VoiceBar), findsOneWidget);
});
testWidgets('VoiceBar onConfigure callback fires', (tester) async {
var configured = false;
await tester.pumpWidget(buildVoiceBar(
onConfigure: () => configured = true,
));
await tester.tap(find.byIcon(Icons.tune));
expect(configured, isTrue);
});
testWidgets('VoiceBar renders PTT capability badge in PTT mode', (tester) async {
await tester.pumpWidget(buildVoiceBar(
transmitMode: rust.BridgeTransmitMode.ptt,
pttLevel: 'L1WindowsHook',
));
expect(find.byIcon(Icons.public), findsOneWidget);
});
testWidgets('VoiceBar does not render PTT badge in continuous mode', (tester) async {
await tester.pumpWidget(buildVoiceBar(
transmitMode: rust.BridgeTransmitMode.continuous,
));
expect(find.byIcon(Icons.public), findsNothing);
});
testWidgets('VoiceBar renders with active PTT state', (tester) async {
const activeStats = rust.BridgeAudioStats(
framesSent: 500,
framesReceived: 300,
pttActive: true,
inputLevel: -10.0,
);
await tester.pumpWidget(buildVoiceBar(audioStats: activeStats));
expect(find.byType(VoiceBar), findsOneWidget);
});
testWidgets('VoiceBar renders with input level override', (tester) async {
await tester.pumpWidget(buildVoiceBar(
inputLevel: -20.0,
));
expect(find.byType(VoiceBar), findsOneWidget);
});
testWidgets('VoiceBar renders with null input level', (tester) async {
await tester.pumpWidget(buildVoiceBar(
audioStats: null,
inputLevel: null,
));
expect(find.byType(VoiceBar), findsOneWidget);
});
}

Some files were not shown because too many files have changed in this diff Show More