feat(ios,p0): iOS P0 platform, audio fixes, channel UX
This commit is contained in:
@@ -4,6 +4,8 @@ import AVFoundation
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
private var iosAudioLifecycleChannel: FlutterMethodChannel?
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
@@ -98,7 +100,10 @@ import AVFoundation
|
||||
// output-only Bluetooth devices.
|
||||
options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
|
||||
)
|
||||
NSLog("chanora_flutter: AVAudioSession category set (playAndRecord/default + defaultToSpeaker)")
|
||||
// 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)")
|
||||
}
|
||||
@@ -115,6 +120,20 @@ import AVFoundation
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleRouteChange(_:)),
|
||||
name: AVAudioSession.routeChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleInterruption(_:)),
|
||||
name: AVAudioSession.interruptionNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Request microphone access on first launch rather than waiting
|
||||
// for the user's first voice-channel join. The latter is
|
||||
// surprising: the user has only tapped "connect to server" and
|
||||
@@ -161,17 +180,13 @@ import AVFoundation
|
||||
// 44.1 kHz (which would explain the user's broken playback
|
||||
// \u2014 our render callback would be writing samples at the
|
||||
// wrong rate, causing pitch + timing artifacts).
|
||||
logAudioSessionState(context: "setActive")
|
||||
let s = AVAudioSession.sharedInstance()
|
||||
let route = s.currentRoute
|
||||
let outs = route.outputs.map { "\($0.portType.rawValue)/\($0.portName)" }.joined(separator: ",")
|
||||
let ins = route.inputs.map { "\($0.portType.rawValue)/\($0.portName)" }.joined(separator: ",")
|
||||
let ins = s.currentRoute.inputs.map { "\($0.portType.rawValue)/\($0.portName)" }.joined(separator: ",")
|
||||
NSLog(
|
||||
"chanora_flutter: AVAudioSession actual: " +
|
||||
"category=\(s.category.rawValue) " +
|
||||
"mode=\(s.mode.rawValue) " +
|
||||
"sampleRate=\(s.sampleRate) " +
|
||||
"ioBufferDuration=\(String(format: "%.4f", s.ioBufferDuration)) " +
|
||||
"outputs=[\(outs)] " +
|
||||
"inputs=[\(ins)] " +
|
||||
"outputVolume=\(s.outputVolume)"
|
||||
)
|
||||
@@ -180,7 +195,65 @@ import AVFoundation
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads back the actual AVAudioSession state and logs it for
|
||||
/// SDD-098 compliance. Called after both setCategory and setActive
|
||||
/// to verify that the session accepted the requested configuration.
|
||||
private func logAudioSessionState(context: String) {
|
||||
let s = AVAudioSession.sharedInstance()
|
||||
NSLog("chanora_flutter: [\(context)] category=\(s.category.rawValue) mode=\(s.mode.rawValue) options=\(s.categoryOptions.rawValue) route outputs=\(s.currentRoute.outputs.map { "\($0.portType.rawValue)" })")
|
||||
if s.sampleRate != 48000.0 {
|
||||
NSLog("chanora_flutter: WARNING: actual sample rate \(s.sampleRate) != requested 48000")
|
||||
}
|
||||
if s.ioBufferDuration > 0.025 {
|
||||
NSLog("chanora_flutter: WARNING: IO buffer duration \(s.ioBufferDuration) > 25ms, may cause latency")
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleRouteChange(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
||||
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let routeDescription = AVAudioSession.sharedInstance().currentRoute
|
||||
let outputs = routeDescription.outputs.map { $0.portType.rawValue }.joined(separator: ",")
|
||||
NSLog("chanora_flutter: route change reason=\(reason.rawValue) outputs=\(outputs)")
|
||||
|
||||
if reason == .oldDeviceUnavailable || reason == .newDeviceAvailable {
|
||||
iosAudioLifecycleChannel?.invokeMethod("handleRouteChange", arguments: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleInterruption(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
switch type {
|
||||
case .began:
|
||||
NSLog("chanora_flutter: audio interruption began")
|
||||
iosAudioLifecycleChannel?.invokeMethod("handleInterruptionBegan", arguments: nil)
|
||||
case .ended:
|
||||
let shouldResume = (userInfo[AVAudioSessionInterruptionOptionKey] as? UInt)
|
||||
.map { $0 & AVAudioSession.InterruptionOptions.shouldResume.rawValue != 0 }
|
||||
?? false
|
||||
NSLog("chanora_flutter: audio interruption ended shouldResume=\(shouldResume)")
|
||||
iosAudioLifecycleChannel?.invokeMethod("handleInterruptionEnded", arguments: shouldResume)
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
iosAudioLifecycleChannel = FlutterMethodChannel(
|
||||
name: "chanora/ios_audio_lifecycle",
|
||||
binaryMessenger: engineBridge.applicationRegistrar.messenger()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"pttCapabilityExplainGoGlobalWindows": "On Windows, Global PTT is engaged automatically once you bind a key. No additional permission is required.",
|
||||
"pttCapabilityExplainGoGlobalMacos": "On macOS, Global PTT requires Input Monitoring permission. Open System Settings → Privacy & Security → Input Monitoring, allow Chanora, then re-bind the key.",
|
||||
"pttCapabilityExplainGoGlobalLinux": "On Linux, Global PTT requires a GNOME-Wayland desktop with the GlobalShortcuts portal. Re-bind the key and accept the desktop's shortcut dialog when it appears.",
|
||||
"pttCapabilityExplainGoGlobalIos": "On iOS, Apple does not expose global hotkeys to apps. Chanora uses the on-screen Push-to-Talk button and only transmits while the app is in the foreground.",
|
||||
"pttCapabilityExplainGoGlobalGeneric": "Global PTT is not available in this environment. Focused PTT will keep working while the Chanora window has focus.",
|
||||
"pttConfigureAction": "Configure",
|
||||
"pttConfigureTitle": "Configure Push-to-Talk binding",
|
||||
@@ -66,6 +67,7 @@
|
||||
"outputMuteAction": "Mute speaker",
|
||||
"outputUnmuteAction": "Unmute speaker",
|
||||
"joinChannelAction": "Join channel",
|
||||
"leaveChannelAction": "Leave channel",
|
||||
"channelPasswordTitle": "Channel password",
|
||||
"bookmarksHeading": "Bookmarks",
|
||||
"bookmarksEmpty": "No bookmarks yet. Enter a server above and tap \"Save bookmark\".",
|
||||
@@ -140,6 +142,7 @@
|
||||
"audioRouteCarAudio": "Car audio",
|
||||
"audioRouteAirplay": "AirPlay",
|
||||
"audioRouteUnknown": "Unknown",
|
||||
"channelJoinAlreadyIn": "Already in this channel.",
|
||||
"channelJoinFailedPermission": "Insufficient permission to join this channel.",
|
||||
"channelJoinFailedPassword": "Wrong channel password.",
|
||||
"channelJoinFailedFull": "Channel is full.",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"pttCapabilityExplainGoGlobalWindows": "在 Windows 上,绑定按键后会自动启用全局对讲,无需额外权限。",
|
||||
"pttCapabilityExplainGoGlobalMacos": "在 macOS 上,启用全局对讲需要「输入监视」权限。请打开「系统设置 → 隐私与安全 → 输入监视」,授权 Chanora 后重新绑定按键。",
|
||||
"pttCapabilityExplainGoGlobalLinux": "在 Linux 上,启用全局对讲需要带 GlobalShortcuts 门户的 GNOME-Wayland 桌面。请重新绑定按键,并在桌面弹出快捷键对话框时接受。",
|
||||
"pttCapabilityExplainGoGlobalIos": "在 iOS 上,Apple 不向应用开放全局热键。Chanora 使用屏幕上的按住说话按钮,并且仅在应用位于前台时响应。",
|
||||
"pttCapabilityExplainGoGlobalGeneric": "当前环境暂不支持全局对讲。聚焦对讲在 Chanora 窗口获得焦点时仍可正常使用。",
|
||||
"pttConfigureAction": "配置",
|
||||
"pttConfigureTitle": "配置对讲按键",
|
||||
@@ -51,6 +52,7 @@
|
||||
"outputMuteAction": "静音扬声器",
|
||||
"outputUnmuteAction": "取消扬声器静音",
|
||||
"joinChannelAction": "加入频道",
|
||||
"leaveChannelAction": "离开频道",
|
||||
"channelPasswordTitle": "频道密码",
|
||||
"bookmarksHeading": "书签",
|
||||
"bookmarksEmpty": "尚无书签。先在上方填写服务器,然后点击“保存书签”。",
|
||||
@@ -97,6 +99,7 @@
|
||||
"audioRouteCarAudio": "车载音频",
|
||||
"audioRouteAirplay": "AirPlay",
|
||||
"audioRouteUnknown": "未知",
|
||||
"channelJoinAlreadyIn": "您已在此频道中。",
|
||||
"channelJoinFailedPermission": "权限不足,无法加入此频道。",
|
||||
"channelJoinFailedPassword": "频道密码错误。",
|
||||
"channelJoinFailedFull": "频道已满。",
|
||||
|
||||
@@ -295,6 +295,12 @@ abstract class AppL10n {
|
||||
/// **'On Linux, Global PTT requires a GNOME-Wayland desktop with the GlobalShortcuts portal. Re-bind the key and accept the desktop\'s shortcut dialog when it appears.'**
|
||||
String get pttCapabilityExplainGoGlobalLinux;
|
||||
|
||||
/// No description provided for @pttCapabilityExplainGoGlobalIos.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'On iOS, Apple does not expose global hotkeys to apps. Chanora uses the on-screen Push-to-Talk button and only transmits while the app is in the foreground.'**
|
||||
String get pttCapabilityExplainGoGlobalIos;
|
||||
|
||||
/// No description provided for @pttCapabilityExplainGoGlobalGeneric.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -379,6 +385,12 @@ abstract class AppL10n {
|
||||
/// **'Join channel'**
|
||||
String get joinChannelAction;
|
||||
|
||||
/// No description provided for @leaveChannelAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Leave channel'**
|
||||
String get leaveChannelAction;
|
||||
|
||||
/// No description provided for @channelPasswordTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -625,6 +637,12 @@ abstract class AppL10n {
|
||||
/// **'Unknown'**
|
||||
String get audioRouteUnknown;
|
||||
|
||||
/// No description provided for @channelJoinAlreadyIn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Already in this channel.'**
|
||||
String get channelJoinAlreadyIn;
|
||||
|
||||
/// No description provided for @channelJoinFailedPermission.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -123,6 +123,10 @@ class AppL10nEn extends AppL10n {
|
||||
String get pttCapabilityExplainGoGlobalLinux =>
|
||||
'On Linux, Global PTT requires a GNOME-Wayland desktop with the GlobalShortcuts portal. Re-bind the key and accept the desktop\'s shortcut dialog when it appears.';
|
||||
|
||||
@override
|
||||
String get pttCapabilityExplainGoGlobalIos =>
|
||||
'On iOS, Apple does not expose global hotkeys to apps. Chanora uses the on-screen Push-to-Talk button and only transmits while the app is in the foreground.';
|
||||
|
||||
@override
|
||||
String get pttCapabilityExplainGoGlobalGeneric =>
|
||||
'Global PTT is not available in this environment. Focused PTT will keep working while the Chanora window has focus.';
|
||||
@@ -169,6 +173,9 @@ class AppL10nEn extends AppL10n {
|
||||
@override
|
||||
String get joinChannelAction => 'Join channel';
|
||||
|
||||
@override
|
||||
String get leaveChannelAction => 'Leave channel';
|
||||
|
||||
@override
|
||||
String get channelPasswordTitle => 'Channel password';
|
||||
|
||||
@@ -305,6 +312,9 @@ class AppL10nEn extends AppL10n {
|
||||
@override
|
||||
String get audioRouteUnknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get channelJoinAlreadyIn => 'Already in this channel.';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPermission =>
|
||||
'Insufficient permission to join this channel.';
|
||||
|
||||
@@ -120,6 +120,10 @@ class AppL10nZh extends AppL10n {
|
||||
String get pttCapabilityExplainGoGlobalLinux =>
|
||||
'在 Linux 上,启用全局对讲需要带 GlobalShortcuts 门户的 GNOME-Wayland 桌面。请重新绑定按键,并在桌面弹出快捷键对话框时接受。';
|
||||
|
||||
@override
|
||||
String get pttCapabilityExplainGoGlobalIos =>
|
||||
'在 iOS 上,Apple 不向应用开放全局热键。Chanora 使用屏幕上的按住说话按钮,并且仅在应用位于前台时响应。';
|
||||
|
||||
@override
|
||||
String get pttCapabilityExplainGoGlobalGeneric =>
|
||||
'当前环境暂不支持全局对讲。聚焦对讲在 Chanora 窗口获得焦点时仍可正常使用。';
|
||||
@@ -164,6 +168,9 @@ class AppL10nZh extends AppL10n {
|
||||
@override
|
||||
String get joinChannelAction => '加入频道';
|
||||
|
||||
@override
|
||||
String get leaveChannelAction => '离开频道';
|
||||
|
||||
@override
|
||||
String get channelPasswordTitle => '频道密码';
|
||||
|
||||
@@ -299,6 +306,9 @@ class AppL10nZh extends AppL10n {
|
||||
@override
|
||||
String get audioRouteUnknown => '未知';
|
||||
|
||||
@override
|
||||
String get channelJoinAlreadyIn => '您已在此频道中。';
|
||||
|
||||
@override
|
||||
String get channelJoinFailedPermission => '权限不足,无法加入此频道。';
|
||||
|
||||
|
||||
+317
-249
@@ -67,9 +67,43 @@ Future<void> main() async {
|
||||
await _resolveAppVersion();
|
||||
unawaited(_wireStorage());
|
||||
unawaited(_wireConnectivity());
|
||||
_wireIosAudioLifecycle();
|
||||
runApp(const ChanoraApp());
|
||||
}
|
||||
|
||||
/// Wire the iOS AVAudioSession lifecycle MethodChannel.
|
||||
///
|
||||
/// Swift side (AppDelegate) posts route-change and interruption
|
||||
/// events through `FlutterMethodChannel` named
|
||||
/// `"chanora/ios_audio_lifecycle"`. This handler dispatches them to
|
||||
/// the FRB bridge functions on the Rust side.
|
||||
void _wireIosAudioLifecycle() {
|
||||
const channel = MethodChannel('chanora/ios_audio_lifecycle');
|
||||
channel.setMethodCallHandler((call) async {
|
||||
try {
|
||||
switch (call.method) {
|
||||
case 'handleRouteChange':
|
||||
rust.handleRouteChange();
|
||||
break;
|
||||
case 'handleInterruptionBegan':
|
||||
rust.handleInterruptionBegan();
|
||||
break;
|
||||
case 'handleInterruptionEnded':
|
||||
// `shouldResume` is passed from Swift as a bool argument.
|
||||
final shouldResume = call.arguments as bool? ?? false;
|
||||
rust.handleInterruptionEnded(shouldResume: shouldResume);
|
||||
break;
|
||||
default:
|
||||
// Unknown method — ignore gracefully rather than crashing.
|
||||
break;
|
||||
}
|
||||
} catch (_) {
|
||||
// Errors from the Rust side are already logged there;
|
||||
// don't propagate exceptions to the iOS framework.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Populate `_kAppVersion` by suffixing the platform-canonical
|
||||
/// build number to `_kSemverBaseline`. Format:
|
||||
/// `v1.0.0-rc.8+<build>` (e.g. `v1.0.0-rc.8+64`). The build
|
||||
@@ -186,6 +220,12 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
String _pttLevel = 'L0Focused';
|
||||
String _pttBackendId = 'focused';
|
||||
String _pttBoundInputClass = 'keyboard';
|
||||
// iOS interruption state from BridgeEvent::InterruptionState
|
||||
// (SDD-101). Can be used by UI surfaces (banner/snackbar).
|
||||
// ignore: unused_field
|
||||
bool _iosAudioInterrupted = false;
|
||||
// ignore: unused_field
|
||||
bool _iosInterruptionShouldResume = false;
|
||||
// Last platform-neutral key label the user saved in the
|
||||
// `_PttBindingCaptureDialog` (e.g. "Space", "F10",
|
||||
// "mouse-side-button:8"). Surfaced next to the capability
|
||||
@@ -272,21 +312,21 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
case rust.BridgeEvent_SnapshotChanged():
|
||||
unawaited(_onRefresh());
|
||||
case rust.BridgeEvent_PttCapability(
|
||||
:final level,
|
||||
:final backendId,
|
||||
:final boundInputClass,
|
||||
):
|
||||
:final level,
|
||||
:final backendId,
|
||||
:final boundInputClass,
|
||||
):
|
||||
setState(() {
|
||||
_pttLevel = level;
|
||||
_pttBackendId = backendId;
|
||||
_pttBoundInputClass = boundInputClass;
|
||||
});
|
||||
case rust.BridgeEvent_VoiceState(
|
||||
:final inChannel,
|
||||
:final transmitMode,
|
||||
:final mute,
|
||||
:final releaseTailMs,
|
||||
):
|
||||
:final inChannel,
|
||||
:final transmitMode,
|
||||
:final mute,
|
||||
:final releaseTailMs,
|
||||
):
|
||||
setState(() {
|
||||
_inChannel = inChannel;
|
||||
_transmitMode = transmitMode;
|
||||
@@ -300,6 +340,39 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = null;
|
||||
}
|
||||
case rust.BridgeEvent_InterruptionState(
|
||||
:final began,
|
||||
:final shouldResume,
|
||||
):
|
||||
setState(() {
|
||||
_iosAudioInterrupted = began;
|
||||
_iosInterruptionShouldResume = shouldResume;
|
||||
});
|
||||
// Surface iOS audio interruption to the user (SDD-101).
|
||||
// Use unawaited to stay inside the sync _onEvent stream
|
||||
// without blocking it.
|
||||
if (!mounted) return;
|
||||
unawaited(() async {
|
||||
if (!mounted) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (began) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Audio interrupted by system (phone call)'),
|
||||
duration: Duration(seconds: 3),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
} else if (shouldResume) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Audio resuming'),
|
||||
duration: Duration(seconds: 2),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +483,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
|
||||
Future<void> _onJoinChannel(rust.BridgeChannel ch) async {
|
||||
if (ch.id == _currentVoiceChannelId) return;
|
||||
final l10n = AppL10n.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
String? password;
|
||||
@@ -419,10 +493,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
if (password == null) return; // cancelled
|
||||
}
|
||||
try {
|
||||
await rust.voiceJoin(
|
||||
channelId: ch.id,
|
||||
password: password ?? '',
|
||||
);
|
||||
await rust.voiceJoin(channelId: ch.id, password: password ?? '');
|
||||
if (!mounted) return;
|
||||
setState(() => _currentVoiceChannelId = ch.id);
|
||||
} catch (e) {
|
||||
@@ -455,7 +526,8 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
l10n.channelJoinFailedGeneric('$host: $reason'),
|
||||
connection: (msg) => l10n.channelJoinFailedGeneric(msg),
|
||||
notConnected: () => l10n.channelJoinFailedGeneric('not connected'),
|
||||
alreadyConnected: () => l10n.channelJoinFailedGeneric('already connected'),
|
||||
alreadyConnected: () =>
|
||||
l10n.channelJoinFailedGeneric('already connected'),
|
||||
serverRejected: (code, message) {
|
||||
// Canonical TS3 error codes per ReSpeak/tsdeclarations
|
||||
// Errors.csv.
|
||||
@@ -465,6 +537,8 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
return l10n.channelJoinFailedTimeout;
|
||||
case 0x0a08: // permissions_client_insufficient
|
||||
return l10n.channelJoinFailedPermission;
|
||||
case 0x0302: // channel_already_in
|
||||
return l10n.channelJoinAlreadyIn;
|
||||
case 0x030d: // channel_invalid_password
|
||||
return l10n.channelJoinFailedPassword;
|
||||
case 0x0309: // channel_maxclients_reached
|
||||
@@ -702,9 +776,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
if (_pttBackendId == 'gnome-wayland-portal') {
|
||||
try {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(l10n.pttConfigurePortalRedirect),
|
||||
));
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.pttConfigurePortalRedirect)),
|
||||
);
|
||||
await rust.setPttBinding(
|
||||
inputClass: rust.BridgePttInputClass.keyboard,
|
||||
platformKey: 'portal',
|
||||
@@ -712,9 +786,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
final messenger = ScaffoldMessenger.of(this.context);
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(l10n.statusError(e.toString())),
|
||||
));
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.statusError(e.toString()))),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -750,9 +824,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
// Use the State's context (guaranteed valid because we
|
||||
// re-checked `mounted` immediately above).
|
||||
final messenger = ScaffoldMessenger.of(this.context);
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(l10n.statusError(e.toString())),
|
||||
));
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.statusError(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,40 +847,25 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.appTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
Text(l10n.appTitle, style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.aboutVersion(_kAppVersion),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.aboutNonAffiliation,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
Text(l10n.aboutNonAffiliation, style: theme.textTheme.bodyMedium),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.aboutLicenseHeading,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
Text(l10n.aboutLicenseHeading, style: theme.textTheme.titleSmall),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.aboutLicenseBody,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(l10n.aboutLicenseBody, style: theme.textTheme.bodySmall),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.aboutThirdPartyHeading,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.aboutThirdPartyBody,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(l10n.aboutThirdPartyBody, style: theme.textTheme.bodySmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -879,11 +938,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_hostCtl.text = b.host;
|
||||
_nickCtl.text = b.nickname;
|
||||
_passwordCtl.text = b.password;
|
||||
await _onConnect(
|
||||
host: b.host,
|
||||
nickname: b.nickname,
|
||||
password: b.password,
|
||||
);
|
||||
await _onConnect(host: b.host, nickname: b.nickname, password: b.password);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -994,191 +1049,191 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!isWideSnapshot) ...[
|
||||
banner,
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Text(statusText(), style: theme.textTheme.titleMedium),
|
||||
if (_lostReason != null || _reconnectAttempt != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)],
|
||||
Text(statusText(), style: theme.textTheme.titleMedium),
|
||||
if (_lostReason != null || _reconnectAttempt != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_reconnectAttempt != null
|
||||
? l10n.statusReconnecting(
|
||||
_reconnectAttempt!,
|
||||
_reconnectDelay ?? 0,
|
||||
)
|
||||
: l10n.statusConnectionLost(_lostReason ?? ''),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
if (_phase == _Phase.idle) ...[
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
// Dismiss keyboard when the user drags away from
|
||||
// a focused field — friendlier mobile UX than
|
||||
// forcing them to tap outside.
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ConnectForm(
|
||||
hostCtl: _hostCtl,
|
||||
nickCtl: _nickCtl,
|
||||
passwordCtl: _passwordCtl,
|
||||
onConnect: () => _onConnect(),
|
||||
onAddBookmark: _onAddCurrentBookmark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_BookmarkList(
|
||||
bookmarks: _bookmarks,
|
||||
onConnect: _onUseBookmark,
|
||||
onDelete: _onDeleteBookmark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (_phase == _Phase.connecting) ...[
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
] else if (_phase == _Phase.connected && _snapshot != null) ...[
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final voiceBar = VoiceBar(
|
||||
inChannel: _inChannel,
|
||||
transmitMode: _transmitMode,
|
||||
hardMute: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
channelName: _currentVoiceChannelName(),
|
||||
audioStats: _audioStats,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
onToggleMute: _onToggleHardMute,
|
||||
onToggleOutputMute: _toggleOutputMute,
|
||||
onConfigure: _onOpenVoiceSettings,
|
||||
onPttHeldChanged: _onOnscreenPttHeldChanged,
|
||||
);
|
||||
final snapshotView = _SnapshotView(
|
||||
snapshot: _snapshot!,
|
||||
onJoinChannel: _onJoinChannel,
|
||||
);
|
||||
// Responsive: at <840 dp use a stacked layout
|
||||
// (Voice Bar on top, channel tree below). At
|
||||
// ≥840 dp use a side-by-side layout with the
|
||||
// Voice Bar pinned to 320 dp on the left and
|
||||
// the channel tree expanding on the right.
|
||||
// 840 dp matches Material's tablet / desktop
|
||||
// breakpoint.
|
||||
const wideBreakpoint = 840.0;
|
||||
const voiceBarWidthWide = 320.0;
|
||||
if (constraints.maxWidth >= wideBreakpoint) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: voiceBarWidthWide,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
banner,
|
||||
const SizedBox(height: 12),
|
||||
voiceBar,
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_reconnectAttempt != null
|
||||
? l10n.statusReconnecting(
|
||||
_reconnectAttempt!,
|
||||
_reconnectDelay ?? 0,
|
||||
)
|
||||
: l10n.statusConnectionLost(_lostReason ?? ''),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: snapshotView),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// In narrow / one-column layout the layout
|
||||
// is:
|
||||
//
|
||||
// [ channel tree (Expanded) ]
|
||||
// [ status chip (2-line live readout) ]
|
||||
// [ PTT button (mobile-only,
|
||||
// PTT-mode-only) ]
|
||||
//
|
||||
// The chip is a tap target that opens
|
||||
// `showVoiceDetailsSheet` with the mic
|
||||
// level meter + TX/RX counts + capability
|
||||
// badge. Mode + bind key + release-tail
|
||||
// live in `VoiceSettingsDialog`
|
||||
// (gear icon in AppBar).
|
||||
//
|
||||
// Wide mode (Row branch above) is
|
||||
// unchanged from rc.8.
|
||||
Expanded(child: snapshotView),
|
||||
const SizedBox(height: 8),
|
||||
VoiceStatusChip(
|
||||
transmitMode: _transmitMode,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
audioStats: _audioStats,
|
||||
isTouchOnly: _isTouchOnlyPttHost,
|
||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
||||
),
|
||||
// PTT button only when PTT mode is active
|
||||
// AND the user is in a voice channel. In
|
||||
// Continuous mode there is nothing to
|
||||
// hold; the chip alone surfaces the
|
||||
// "Mic on / off" state.
|
||||
if (_inChannel &&
|
||||
_transmitMode ==
|
||||
rust.BridgeTransmitMode.ptt) ...[
|
||||
const SizedBox(height: 8),
|
||||
VoicePttButton(
|
||||
active: _audioStats?.pttActive ?? false,
|
||||
onHeldChanged: _onOnscreenPttHeldChanged,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
if (_phase == _Phase.idle) ...[
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
// Dismiss keyboard when the user drags away from
|
||||
// a focused field — friendlier mobile UX than
|
||||
// forcing them to tap outside.
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ConnectForm(
|
||||
hostCtl: _hostCtl,
|
||||
nickCtl: _nickCtl,
|
||||
passwordCtl: _passwordCtl,
|
||||
onConnect: () => _onConnect(),
|
||||
onAddBookmark: _onAddCurrentBookmark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_BookmarkList(
|
||||
bookmarks: _bookmarks,
|
||||
onConnect: _onUseBookmark,
|
||||
onDelete: _onDeleteBookmark,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (_phase == _Phase.connecting) ...[
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
] else if (_phase == _Phase.connected && _snapshot != null) ...[
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final voiceBar = VoiceBar(
|
||||
inChannel: _inChannel,
|
||||
transmitMode: _transmitMode,
|
||||
hardMute: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
channelName: _currentVoiceChannelName(),
|
||||
audioStats: _audioStats,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
onToggleMute: _onToggleHardMute,
|
||||
onToggleOutputMute: _toggleOutputMute,
|
||||
onConfigure: _onOpenVoiceSettings,
|
||||
onPttHeldChanged: _onOnscreenPttHeldChanged,
|
||||
);
|
||||
final snapshotView = _SnapshotView(
|
||||
snapshot: _snapshot!,
|
||||
currentVoiceChannelId: _currentVoiceChannelId,
|
||||
onJoinChannel: _onJoinChannel,
|
||||
onLeaveVoice: _onLeaveVoice,
|
||||
);
|
||||
// Responsive: at <840 dp use a stacked layout
|
||||
// (Voice Bar on top, channel tree below). At
|
||||
// ≥840 dp use a side-by-side layout with the
|
||||
// Voice Bar pinned to 320 dp on the left and
|
||||
// the channel tree expanding on the right.
|
||||
// 840 dp matches Material's tablet / desktop
|
||||
// breakpoint.
|
||||
const wideBreakpoint = 840.0;
|
||||
const voiceBarWidthWide = 320.0;
|
||||
if (constraints.maxWidth >= wideBreakpoint) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: voiceBarWidthWide,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
banner,
|
||||
const SizedBox(height: 12),
|
||||
voiceBar,
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: snapshotView),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// In narrow / one-column layout the layout
|
||||
// is:
|
||||
//
|
||||
// [ channel tree (Expanded) ]
|
||||
// [ status chip (2-line live readout) ]
|
||||
// [ PTT button (mobile-only,
|
||||
// PTT-mode-only) ]
|
||||
//
|
||||
// The chip is a tap target that opens
|
||||
// `showVoiceDetailsSheet` with the mic
|
||||
// level meter + TX/RX counts + capability
|
||||
// badge. Mode + bind key + release-tail
|
||||
// live in `VoiceSettingsDialog`
|
||||
// (gear icon in AppBar).
|
||||
//
|
||||
// Wide mode (Row branch above) is
|
||||
// unchanged from rc.8.
|
||||
Expanded(child: snapshotView),
|
||||
const SizedBox(height: 8),
|
||||
VoiceStatusChip(
|
||||
transmitMode: _transmitMode,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
audioStats: _audioStats,
|
||||
isTouchOnly: _isTouchOnlyPttHost,
|
||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
||||
),
|
||||
// PTT button only when PTT mode is active
|
||||
// AND the user is in a voice channel. In
|
||||
// Continuous mode there is nothing to
|
||||
// hold; the chip alone surfaces the
|
||||
// "Mic on / off" state.
|
||||
if (_inChannel &&
|
||||
_transmitMode ==
|
||||
rust.BridgeTransmitMode.ptt) ...[
|
||||
const SizedBox(height: 8),
|
||||
VoicePttButton(
|
||||
active: _audioStats?.pttActive ?? false,
|
||||
onHeldChanged: _onOnscreenPttHeldChanged,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1225,10 +1280,7 @@ class _AppBarTitle extends StatelessWidget {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isNarrow) ...[
|
||||
Text(l10n.appTitle),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
if (!isNarrow) ...[Text(l10n.appTitle), const SizedBox(width: 12)],
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
@@ -1288,8 +1340,9 @@ class _BookmarkNameDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BookmarkNameDialogState extends State<_BookmarkNameDialog> {
|
||||
late final TextEditingController _ctl =
|
||||
TextEditingController(text: widget.initialName);
|
||||
late final TextEditingController _ctl = TextEditingController(
|
||||
text: widget.initialName,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -1330,8 +1383,7 @@ class _ChannelPasswordDialog extends StatefulWidget {
|
||||
const _ChannelPasswordDialog();
|
||||
|
||||
@override
|
||||
State<_ChannelPasswordDialog> createState() =>
|
||||
_ChannelPasswordDialogState();
|
||||
State<_ChannelPasswordDialog> createState() => _ChannelPasswordDialogState();
|
||||
}
|
||||
|
||||
class _ChannelPasswordDialogState extends State<_ChannelPasswordDialog> {
|
||||
@@ -1624,10 +1676,7 @@ class _BookmarkList extends StatelessWidget {
|
||||
if (bookmarks.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
l10n.bookmarksEmpty,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
child: Text(l10n.bookmarksEmpty, style: theme.textTheme.bodySmall),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
@@ -1663,7 +1712,6 @@ class _BookmarkList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// PTT capability badge (gen2 v0.9.3 / SDD-091).
|
||||
///
|
||||
/// Renders the active PTT level + backend in the Voice Bar so the
|
||||
@@ -1710,6 +1758,8 @@ class PttCapabilityBadge extends StatelessWidget {
|
||||
return l10n.pttCapabilityExplainGoGlobalMacos;
|
||||
case TargetPlatform.linux:
|
||||
return l10n.pttCapabilityExplainGoGlobalLinux;
|
||||
case TargetPlatform.iOS:
|
||||
return l10n.pttCapabilityExplainGoGlobalIos;
|
||||
default:
|
||||
return l10n.pttCapabilityExplainGoGlobalGeneric;
|
||||
}
|
||||
@@ -1816,11 +1866,15 @@ class PttCapabilityBadge extends StatelessWidget {
|
||||
class _SnapshotView extends StatelessWidget {
|
||||
const _SnapshotView({
|
||||
required this.snapshot,
|
||||
required this.currentVoiceChannelId,
|
||||
required this.onJoinChannel,
|
||||
required this.onLeaveVoice,
|
||||
});
|
||||
|
||||
final rust.BridgeSnapshot snapshot;
|
||||
final BigInt? currentVoiceChannelId;
|
||||
final ValueChanged<rust.BridgeChannel> onJoinChannel;
|
||||
final VoidCallback onLeaveVoice;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1876,7 +1930,10 @@ class _SnapshotView extends StatelessWidget {
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(snapshot.welcomeMessage, style: theme.textTheme.bodySmall),
|
||||
child: Text(
|
||||
snapshot.welcomeMessage,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Divider(height: 24),
|
||||
@@ -1889,15 +1946,26 @@ class _SnapshotView extends StatelessWidget {
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.tag),
|
||||
leading: Icon(
|
||||
ch.id == currentVoiceChannelId ? Icons.volume_up : Icons.tag,
|
||||
),
|
||||
title: Text(ch.name),
|
||||
subtitle: Text('id=${ch.id} parent=${ch.parent}'),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.login),
|
||||
tooltip: l10n.joinChannelAction,
|
||||
onPressed: () => onJoinChannel(ch),
|
||||
icon: Icon(
|
||||
ch.id == currentVoiceChannelId ? Icons.logout : Icons.login,
|
||||
),
|
||||
tooltip: ch.id == currentVoiceChannelId
|
||||
? l10n.leaveChannelAction
|
||||
: l10n.joinChannelAction,
|
||||
onPressed: ch.id == currentVoiceChannelId
|
||||
? onLeaveVoice
|
||||
: () => onJoinChannel(ch),
|
||||
),
|
||||
onTap: () => onJoinChannel(ch),
|
||||
selected: ch.id == currentVoiceChannelId,
|
||||
onTap: ch.id == currentVoiceChannelId
|
||||
? null
|
||||
: () => onJoinChannel(ch),
|
||||
),
|
||||
),
|
||||
for (final cl in byChannel[ch.id] ?? const <rust.BridgeClient>[])
|
||||
@@ -2121,11 +2189,11 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
||||
onPressed: _captured == null
|
||||
? null
|
||||
: () => Navigator.of(context).pop(
|
||||
_CapturedBinding(
|
||||
inputClass: _capturedClass,
|
||||
platformKey: _captured!,
|
||||
),
|
||||
_CapturedBinding(
|
||||
inputClass: _capturedClass,
|
||||
platformKey: _captured!,
|
||||
),
|
||||
),
|
||||
child: Text(l10n.pttConfigureSaveAction),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -41,6 +41,19 @@ Future<void> disconnect() => RustLib.instance.api.crateApiDisconnect();
|
||||
/// True if a connection is currently active.
|
||||
Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
|
||||
|
||||
/// Handle iOS AVAudioSession route changes (SDD-100).
|
||||
void handleRouteChange() => RustLib.instance.api.crateApiHandleRouteChange();
|
||||
|
||||
/// Handle iOS AVAudioSession interruption begin (SDD-101).
|
||||
void handleInterruptionBegan() =>
|
||||
RustLib.instance.api.crateApiHandleInterruptionBegan();
|
||||
|
||||
/// Handle iOS AVAudioSession interruption end (SDD-101).
|
||||
void handleInterruptionEnded({required bool shouldResume}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateApiHandleInterruptionEnded(shouldResume: shouldResume);
|
||||
|
||||
/// Set the push-to-talk state.
|
||||
///
|
||||
/// Superseded in v1 by [`set_transmit_mode`] + the binding capture
|
||||
@@ -427,6 +440,15 @@ sealed class BridgeEvent with _$BridgeEvent {
|
||||
/// Current release-tail in milliseconds (0..=500).
|
||||
required int releaseTailMs,
|
||||
}) = BridgeEvent_VoiceState;
|
||||
|
||||
/// iOS audio interruption state (SDD-101).
|
||||
const factory BridgeEvent.interruptionState({
|
||||
/// True when interruption began, false when it ended.
|
||||
required bool began,
|
||||
|
||||
/// Resume recommendation from the platform. False on begin.
|
||||
required bool shouldResume,
|
||||
}) = BridgeEvent_InterruptionState;
|
||||
}
|
||||
|
||||
/// Coarse OS-reported network state. Mirrors
|
||||
|
||||
@@ -55,7 +55,7 @@ extension BridgeEventPatterns on BridgeEvent {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,TResult Function( BridgeEvent_VoiceState value)? voiceState,required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,TResult Function( BridgeEvent_VoiceState value)? voiceState,TResult Function( BridgeEvent_InterruptionState value)? interruptionState,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
@@ -67,7 +67,8 @@ return audioStarted(_that);case BridgeEvent_AudioStopped() when audioStopped !=
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that);case _:
|
||||
return voiceState(_that);case BridgeEvent_InterruptionState() when interruptionState != null:
|
||||
return interruptionState(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -85,7 +86,7 @@ return voiceState(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,required TResult Function( BridgeEvent_VoiceState value) voiceState,}){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,required TResult Function( BridgeEvent_VoiceState value) voiceState,required TResult Function( BridgeEvent_InterruptionState value) interruptionState,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected():
|
||||
@@ -97,7 +98,8 @@ return audioStarted(_that);case BridgeEvent_AudioStopped():
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged():
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability():
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState():
|
||||
return voiceState(_that);}
|
||||
return voiceState(_that);case BridgeEvent_InterruptionState():
|
||||
return interruptionState(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
@@ -111,7 +113,7 @@ return voiceState(_that);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,TResult? Function( BridgeEvent_VoiceState value)? voiceState,}){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,TResult? Function( BridgeEvent_VoiceState value)? voiceState,TResult? Function( BridgeEvent_InterruptionState value)? interruptionState,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
@@ -123,7 +125,8 @@ return audioStarted(_that);case BridgeEvent_AudioStopped() when audioStopped !=
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that);case _:
|
||||
return voiceState(_that);case BridgeEvent_InterruptionState() when interruptionState != null:
|
||||
return interruptionState(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -140,7 +143,7 @@ return voiceState(_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( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,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( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,TResult Function( bool began, bool shouldResume)? interruptionState,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
|
||||
@@ -151,7 +154,8 @@ return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case _:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case BridgeEvent_InterruptionState() when interruptionState != null:
|
||||
return interruptionState(_that.began,_that.shouldResume);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -169,7 +173,7 @@ return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTai
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@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( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs) voiceState,}) {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( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs) voiceState,required TResult Function( bool began, bool shouldResume) interruptionState,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected():
|
||||
return connected(_that.serverName);case BridgeEvent_Lost():
|
||||
@@ -180,7 +184,8 @@ return audioStarted();case BridgeEvent_AudioStopped():
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged():
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability():
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState():
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);}
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case BridgeEvent_InterruptionState():
|
||||
return interruptionState(_that.began,_that.shouldResume);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -194,7 +199,7 @@ return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTai
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@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( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,}) {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( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,TResult? Function( bool began, bool shouldResume)? interruptionState,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
|
||||
@@ -205,7 +210,8 @@ return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case _:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case BridgeEvent_InterruptionState() when interruptionState != null:
|
||||
return interruptionState(_that.began,_that.shouldResume);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -769,6 +775,76 @@ as int,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class BridgeEvent_InterruptionState extends BridgeEvent {
|
||||
const BridgeEvent_InterruptionState({required this.began, required this.shouldResume}): super._();
|
||||
|
||||
|
||||
/// True when interruption began, false when it ended.
|
||||
final bool began;
|
||||
/// Resume recommendation from the platform. False on begin.
|
||||
final bool shouldResume;
|
||||
|
||||
/// Create a copy of BridgeEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BridgeEvent_InterruptionStateCopyWith<BridgeEvent_InterruptionState> get copyWith => _$BridgeEvent_InterruptionStateCopyWithImpl<BridgeEvent_InterruptionState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_InterruptionState&&(identical(other.began, began) || other.began == began)&&(identical(other.shouldResume, shouldResume) || other.shouldResume == shouldResume));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,began,shouldResume);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BridgeEvent.interruptionState(began: $began, shouldResume: $shouldResume)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $BridgeEvent_InterruptionStateCopyWith<$Res> implements $BridgeEventCopyWith<$Res> {
|
||||
factory $BridgeEvent_InterruptionStateCopyWith(BridgeEvent_InterruptionState value, $Res Function(BridgeEvent_InterruptionState) _then) = _$BridgeEvent_InterruptionStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool began, bool shouldResume
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BridgeEvent_InterruptionStateCopyWithImpl<$Res>
|
||||
implements $BridgeEvent_InterruptionStateCopyWith<$Res> {
|
||||
_$BridgeEvent_InterruptionStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final BridgeEvent_InterruptionState _self;
|
||||
final $Res Function(BridgeEvent_InterruptionState) _then;
|
||||
|
||||
/// Create a copy of BridgeEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? began = null,Object? shouldResume = null,}) {
|
||||
return _then(BridgeEvent_InterruptionState(
|
||||
began: null == began ? _self.began : began // ignore: cast_nullable_to_non_nullable
|
||||
as bool,shouldResume: null == shouldResume ? _self.shouldResume : shouldResume // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => 1306308591;
|
||||
int get rustContentHash => 1322894465;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@@ -105,6 +105,12 @@ abstract class RustLibApi extends BaseApi {
|
||||
|
||||
Future<BridgeTransmitMode> crateApiGetTransmitMode();
|
||||
|
||||
void crateApiHandleInterruptionBegan();
|
||||
|
||||
void crateApiHandleInterruptionEnded({required bool shouldResume});
|
||||
|
||||
void crateApiHandleRouteChange();
|
||||
|
||||
Future<void> crateApiInitStorage({required String dir});
|
||||
|
||||
Future<bool> crateApiIsConnected();
|
||||
@@ -469,6 +475,76 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiGetTransmitModeConstMeta =>
|
||||
const TaskConstMeta(debugName: "get_transmit_mode", argNames: []);
|
||||
|
||||
@override
|
||||
void crateApiHandleInterruptionBegan() {
|
||||
return handler.executeSync(
|
||||
SyncTask(
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 12)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: null,
|
||||
),
|
||||
constMeta: kCrateApiHandleInterruptionBeganConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiHandleInterruptionBeganConstMeta =>
|
||||
const TaskConstMeta(debugName: "handle_interruption_began", argNames: []);
|
||||
|
||||
@override
|
||||
void crateApiHandleInterruptionEnded({required bool shouldResume}) {
|
||||
return handler.executeSync(
|
||||
SyncTask(
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bool(shouldResume, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 13)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: null,
|
||||
),
|
||||
constMeta: kCrateApiHandleInterruptionEndedConstMeta,
|
||||
argValues: [shouldResume],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiHandleInterruptionEndedConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "handle_interruption_ended",
|
||||
argNames: ["shouldResume"],
|
||||
);
|
||||
|
||||
@override
|
||||
void crateApiHandleRouteChange() {
|
||||
return handler.executeSync(
|
||||
SyncTask(
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 14)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: null,
|
||||
),
|
||||
constMeta: kCrateApiHandleRouteChangeConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiHandleRouteChangeConstMeta =>
|
||||
const TaskConstMeta(debugName: "handle_route_change", argNames: []);
|
||||
|
||||
@override
|
||||
Future<void> crateApiInitStorage({required String dir}) {
|
||||
return handler.executeNormal(
|
||||
@@ -479,7 +555,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 12,
|
||||
funcId: 15,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -506,7 +582,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 13,
|
||||
funcId: 16,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -533,7 +609,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 14,
|
||||
funcId: 17,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -557,7 +633,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
SyncTask(
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -587,7 +663,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 16,
|
||||
funcId: 19,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -616,7 +692,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 17,
|
||||
funcId: 20,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -644,7 +720,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 18,
|
||||
funcId: 21,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -672,7 +748,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 19,
|
||||
funcId: 22,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -697,7 +773,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bridge_network_state(state, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 20)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -723,7 +799,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 21,
|
||||
funcId: 24,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -751,7 +827,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 22,
|
||||
funcId: 25,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -779,7 +855,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 23,
|
||||
funcId: 26,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -811,7 +887,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 24,
|
||||
funcId: 27,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -841,7 +917,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 25,
|
||||
funcId: 28,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -869,7 +945,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 26,
|
||||
funcId: 29,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -896,7 +972,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 27,
|
||||
funcId: 30,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -924,7 +1000,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 28,
|
||||
funcId: 31,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -956,7 +1032,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 29,
|
||||
funcId: 32,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -985,7 +1061,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 30,
|
||||
funcId: 33,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1156,6 +1232,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
mute: dco_decode_bool(raw[3]),
|
||||
releaseTailMs: dco_decode_u_32(raw[4]),
|
||||
);
|
||||
case 9:
|
||||
return BridgeEvent_InterruptionState(
|
||||
began: dco_decode_bool(raw[1]),
|
||||
shouldResume: dco_decode_bool(raw[2]),
|
||||
);
|
||||
default:
|
||||
throw Exception("unreachable");
|
||||
}
|
||||
@@ -1461,6 +1542,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
mute: var_mute,
|
||||
releaseTailMs: var_releaseTailMs,
|
||||
);
|
||||
case 9:
|
||||
var var_began = sse_decode_bool(deserializer);
|
||||
var var_shouldResume = sse_decode_bool(deserializer);
|
||||
return BridgeEvent_InterruptionState(
|
||||
began: var_began,
|
||||
shouldResume: var_shouldResume,
|
||||
);
|
||||
default:
|
||||
throw UnimplementedError('');
|
||||
}
|
||||
@@ -1792,6 +1880,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_bridge_transmit_mode(transmitMode, serializer);
|
||||
sse_encode_bool(mute, serializer);
|
||||
sse_encode_u_32(releaseTailMs, serializer);
|
||||
case BridgeEvent_InterruptionState(
|
||||
began: final began,
|
||||
shouldResume: final shouldResume,
|
||||
):
|
||||
sse_encode_i_32(9, serializer);
|
||||
sse_encode_bool(began, serializer);
|
||||
sse_encode_bool(shouldResume, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,9 +183,7 @@ class VoiceBar extends StatelessWidget {
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: l10n.voiceOutputMuteLabel,
|
||||
icon: Icon(
|
||||
outputMuted ? Icons.headset_off : Icons.headset,
|
||||
),
|
||||
icon: Icon(outputMuted ? Icons.headset_off : Icons.headset),
|
||||
isSelected: outputMuted,
|
||||
selectedIcon: const Icon(Icons.headset_off),
|
||||
onPressed: onToggleOutputMute,
|
||||
@@ -274,13 +272,11 @@ class VoiceBar extends StatelessWidget {
|
||||
// (single configuration entry point — see the comment
|
||||
// on `onConfigure`).
|
||||
//
|
||||
// Hidden on touch-only mobile hosts (iOS / iPadOS /
|
||||
// Android) because the capability story there is always
|
||||
// "L0Focused via on-screen button" and that's already
|
||||
// visually obvious from the PTT button being on the
|
||||
// bar. Showing a degraded-capability badge there would
|
||||
// be redundant + confusing.
|
||||
if (isPtt && !_isTouchOnlyPttHost)
|
||||
// Still shown on touch-only mobile hosts because iOS P0
|
||||
// acceptance requires an explicit `L0Focused` badge and
|
||||
// explanation that global hotkeys are not available in
|
||||
// the iOS sandbox.
|
||||
if (isPtt)
|
||||
PttCapabilityBadge(
|
||||
level: pttLevel,
|
||||
backendId: pttBackendId,
|
||||
|
||||
@@ -256,7 +256,7 @@ class _VoicePttButtonState extends State<VoicePttButton> {
|
||||
/// 3. Release-tail slider (PTT-only).
|
||||
/// 4. Mic level meter.
|
||||
/// 5. TX / RX frame counts.
|
||||
/// 6. PTT capability badge (desktop only).
|
||||
/// 6. PTT capability badge.
|
||||
///
|
||||
/// Mode + release-tail are inlined directly here instead of being
|
||||
/// hidden behind an "Adjust" button → nested dialog. Single-screen
|
||||
@@ -355,8 +355,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
|
||||
// Route picker only meaningful on iOS + Android where the OS
|
||||
// owns audio routing. Desktop hosts skip the tile entirely.
|
||||
final showRoutePicker =
|
||||
!kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||
final showRoutePicker = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
@@ -365,10 +364,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.voiceSheetTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 1) Audio output route picker tile (mobile only).
|
||||
@@ -428,9 +424,9 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _tail.toDouble().clamp(0, 1000),
|
||||
value: _tail.toDouble().clamp(0, 500),
|
||||
min: 0,
|
||||
max: 1000,
|
||||
max: 500,
|
||||
divisions: 20,
|
||||
label: '$_tail ms',
|
||||
onChanged: _setTail,
|
||||
@@ -470,8 +466,12 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
|
||||
// 5) PTT capability badge \u2014 desktop-only.
|
||||
if (isPtt && !widget.isTouchOnly) ...[
|
||||
// 5) 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,
|
||||
@@ -506,8 +506,8 @@ class _ModeRow extends StatelessWidget {
|
||||
final color = disabled
|
||||
? theme.colorScheme.onSurfaceVariant.withAlpha(120)
|
||||
: selected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface;
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -604,8 +604,11 @@ class _AudioOutputTileState extends State<_AudioOutputTile> {
|
||||
}
|
||||
}
|
||||
|
||||
static String _portLabel(AVAudioSessionPort? type, String fallback,
|
||||
AppL10n l10n) {
|
||||
static String _portLabel(
|
||||
AVAudioSessionPort? type,
|
||||
String fallback,
|
||||
AppL10n l10n,
|
||||
) {
|
||||
switch (type) {
|
||||
case AVAudioSessionPort.builtInSpeaker:
|
||||
return l10n.audioRouteSpeaker;
|
||||
@@ -783,8 +786,9 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
// mic by default, or BT/wired if connected and selected
|
||||
// elsewhere). For consistency, only switch input when the
|
||||
// user explicitly picks a non-speaker input row.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.speaker);
|
||||
await AVAudioSession().overrideOutputAudioPort(
|
||||
AVAudioSessionPortOverride.speaker,
|
||||
);
|
||||
debugPrint('chanora: audio output -> speakerphone (override applied)');
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectSpeaker FAILED: $e\n$st');
|
||||
@@ -800,8 +804,9 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
// which is the built-in receiver. Calling setPreferredInput
|
||||
// explicitly here is redundant and risks the same recalc
|
||||
// race that broke _selectSpeaker before.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
|
||||
await AVAudioSession().overrideOutputAudioPort(
|
||||
AVAudioSessionPortOverride.none,
|
||||
);
|
||||
debugPrint('chanora: audio output -> receiver (override cleared)');
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectReceiver FAILED: $e\n$st');
|
||||
@@ -817,11 +822,13 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
// OWN output (the user hears audio through the same device
|
||||
// they speak into), so .none + setPreferredInput is the
|
||||
// correct combo here.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
|
||||
await AVAudioSession().overrideOutputAudioPort(
|
||||
AVAudioSessionPortOverride.none,
|
||||
);
|
||||
await AVAudioSession().setPreferredInput(port);
|
||||
debugPrint(
|
||||
'chanora: audio output -> ${port.portName} (${port.portType})');
|
||||
'chanora: audio output -> ${port.portName} (${port.portType})',
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectInput FAILED: $e\n$st');
|
||||
}
|
||||
@@ -843,10 +850,12 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
final currentOutputType =
|
||||
_route?.outputs.isNotEmpty == true ? _route!.outputs.first.portType : null;
|
||||
final currentInputUid =
|
||||
_route?.inputs.isNotEmpty == true ? _route!.inputs.first.uid : null;
|
||||
final currentOutputType = _route?.outputs.isNotEmpty == true
|
||||
? _route!.outputs.first.portType
|
||||
: null;
|
||||
final currentInputUid = _route?.inputs.isNotEmpty == true
|
||||
? _route!.inputs.first.uid
|
||||
: null;
|
||||
|
||||
// Whether the active route is the speakerphone override (built-in
|
||||
// speaker is the output but the actual session category isn't
|
||||
@@ -868,10 +877,7 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.audioOutputLabel,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
Text(l10n.audioOutputLabel, style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
_PickerRow(
|
||||
icon: Icons.volume_up,
|
||||
@@ -889,7 +895,10 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
_PickerRow(
|
||||
icon: _AudioOutputTileState._portIcon(port.portType),
|
||||
label: _AudioOutputTileState._portLabel(
|
||||
port.portType, port.portName, l10n),
|
||||
port.portType,
|
||||
port.portName,
|
||||
l10n,
|
||||
),
|
||||
selected: !isSpeaker && port.uid == currentInputUid,
|
||||
onTap: () => _selectInput(port),
|
||||
),
|
||||
@@ -916,8 +925,9 @@ class _PickerRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final color =
|
||||
selected ? theme.colorScheme.primary : theme.colorScheme.onSurface;
|
||||
final color = selected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -933,8 +943,7 @@ class _PickerRow extends StatelessWidget {
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
if (selected)
|
||||
Icon(Icons.check, color: theme.colorScheme.primary),
|
||||
if (selected) Icon(Icons.check, color: theme.colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
06E1AA7E1FB968C1D78DA8DE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -393,10 +393,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
@@ -578,11 +582,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 349G7M4TQQ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
@@ -728,11 +733,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 349G7M4TQQ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
|
||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
|
||||
@@ -763,11 +769,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = "";
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 349G7M4TQQ;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
Reference in New Issue
Block a user