борт управления самолетом
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../core/calls/audio_devices.dart';
|
||||
import '../../../core/calls/call_session.dart';
|
||||
import '../../../core/calls/pulse_audio.dart';
|
||||
import '../../../core/config/app_fonts.dart';
|
||||
import '../../../core/config/call_no_mute.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
import '../../widgets/sheet_helpers.dart';
|
||||
|
||||
Future<void> showCallMicrophoneSheet(
|
||||
BuildContext context, {
|
||||
required CallSession session,
|
||||
required ColorScheme scheme,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
backgroundColor: scheme.surfaceContainerHigh,
|
||||
shape: kSheetShape,
|
||||
builder: (_) => Theme(
|
||||
data: Theme.of(context).copyWith(colorScheme: scheme),
|
||||
child: _MicrophoneSheet(session: session),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _MicOption {
|
||||
const _MicOption({
|
||||
required this.id,
|
||||
required this.label,
|
||||
this.detail,
|
||||
this.isMonitor = false,
|
||||
this.isDevice = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String label;
|
||||
final String? detail;
|
||||
final bool isMonitor;
|
||||
final bool isDevice;
|
||||
}
|
||||
|
||||
class _MicrophoneSheet extends StatefulWidget {
|
||||
final CallSession session;
|
||||
|
||||
const _MicrophoneSheet({required this.session});
|
||||
|
||||
@override
|
||||
State<_MicrophoneSheet> createState() => _MicrophoneSheetState();
|
||||
}
|
||||
|
||||
class _MicrophoneSheetState extends State<_MicrophoneSheet> {
|
||||
List<_MicOption>? _options;
|
||||
bool _pulseMode = false;
|
||||
String? _selected;
|
||||
bool _switching = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final pulse = PulseAudio.supported && await PulseAudio.isAvailable()
|
||||
? await PulseAudio.sources()
|
||||
: const <PulseSource>[];
|
||||
final devices = await AudioDevices.microphones();
|
||||
if (!mounted) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final routed = pulse.map((source) => source.name).toSet();
|
||||
setState(() {
|
||||
_pulseMode = pulse.isNotEmpty;
|
||||
_selected = widget.session.pulseSource ?? widget.session.micDeviceId;
|
||||
_options = [
|
||||
for (final source in pulse)
|
||||
_MicOption(
|
||||
id: source.name,
|
||||
label: source.label,
|
||||
detail: source.name,
|
||||
isMonitor: source.isMonitor,
|
||||
),
|
||||
for (var i = 0; i < devices.length; i++)
|
||||
if (!routed.contains(devices[i].id))
|
||||
_MicOption(
|
||||
id: devices[i].id,
|
||||
label: devices[i].label.isNotEmpty
|
||||
? devices[i].label
|
||||
: l10n.callMicrophoneFallback(i + 1),
|
||||
isDevice: true,
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _select(String? id, {required bool viaDevice}) async {
|
||||
if (_switching || id == _selected) return;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
setState(() => _switching = true);
|
||||
try {
|
||||
if (_pulseMode && !viaDevice) {
|
||||
await widget.session.setPulseSource(id);
|
||||
} else {
|
||||
await widget.session.setMicrophone(id);
|
||||
}
|
||||
if (mounted) setState(() => _selected = id);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showCustomNotification(context, l10n.callMicrophoneFailed(e));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _switching = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final options = _options;
|
||||
final inputs = options?.where((o) => !o.isMonitor).toList() ?? const [];
|
||||
final monitors = options?.where((o) => o.isMonitor).toList() ?? const [];
|
||||
|
||||
return SafeArea(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.7,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_header(cs, l10n),
|
||||
if (CallNoMute.enabled) _noMuteHint(cs, l10n),
|
||||
if (options == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
_tile(
|
||||
cs,
|
||||
id: null,
|
||||
label: l10n.callMicrophoneSystem,
|
||||
icon: Symbols.settings_voice,
|
||||
viaDevice: true,
|
||||
),
|
||||
if (options.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
|
||||
child: Text(
|
||||
l10n.callMicrophoneEmpty,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final option in inputs)
|
||||
_tile(
|
||||
cs,
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
detail: option.detail,
|
||||
icon: Symbols.mic,
|
||||
viaDevice: option.isDevice,
|
||||
),
|
||||
if (monitors.isNotEmpty) ...[
|
||||
_group(cs, l10n.callMicrophoneMonitors),
|
||||
for (final option in monitors)
|
||||
_tile(
|
||||
cs,
|
||||
id: option.id,
|
||||
label: option.label,
|
||||
detail: option.detail,
|
||||
icon: Symbols.graphic_eq,
|
||||
viaDevice: option.isDevice,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header(ColorScheme cs, AppLocalizations l10n) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 8, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.callMicrophoneTitle,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: displayFontOf(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() => _options = null);
|
||||
_load();
|
||||
},
|
||||
tooltip: l10n.callMicrophoneRefresh,
|
||||
icon: Icon(Symbols.refresh, color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _noMuteHint(ColorScheme cs, AppLocalizations l10n) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.graphic_eq, size: 18, color: cs.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.callNoMuteHint,
|
||||
style: TextStyle(color: cs.primary, fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _group(ColorScheme cs, String title) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 6),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _tile(
|
||||
ColorScheme cs, {
|
||||
required String? id,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
String? detail,
|
||||
bool viaDevice = false,
|
||||
}) {
|
||||
final selected = _selected == id;
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: selected ? cs.primary : cs.onSurface),
|
||||
title: Text(
|
||||
label,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: selected ? cs.primary : cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
subtitle: detail == null
|
||||
? null
|
||||
: Text(
|
||||
detail,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
||||
),
|
||||
trailing: selected ? Icon(Symbols.check, color: cs.primary) : null,
|
||||
enabled: !_switching,
|
||||
onTap: () => _select(id, viaDevice: viaDevice),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart'
|
||||
show
|
||||
MediaStream,
|
||||
RTCVideoRenderer,
|
||||
RTCVideoValue,
|
||||
RTCVideoViewObjectFit;
|
||||
show MediaStream, RTCVideoRenderer, RTCVideoValue, RTCVideoViewObjectFit;
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
@@ -19,6 +15,7 @@ import '../../../core/calls/call_controller.dart';
|
||||
import '../../../core/calls/call_info.dart';
|
||||
import '../../../core/calls/call_session.dart';
|
||||
import '../../../core/config/app_colors.dart';
|
||||
import '../../../core/config/call_no_mute.dart';
|
||||
import '../../../core/utils/format.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../widgets/call_video_view.dart';
|
||||
@@ -27,6 +24,7 @@ import '../../widgets/glossy_pill.dart';
|
||||
import '../../widgets/animated_slash_icon.dart';
|
||||
import '../../widgets/sheet_helpers.dart';
|
||||
import '../../widgets/small_spinner.dart';
|
||||
import 'call_mic_sheet.dart';
|
||||
import 'call_participants_sheet.dart';
|
||||
import 'komet_hub.dart';
|
||||
import '../../../core/config/app_fonts.dart';
|
||||
@@ -472,6 +470,16 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
|
||||
void _showMicrophones() {
|
||||
final session = _session;
|
||||
if (session == null) return;
|
||||
showCallMicrophoneSheet(
|
||||
context,
|
||||
session: session,
|
||||
scheme: _darkScheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
void _showInfoSheet() {
|
||||
final cs = _darkScheme(context);
|
||||
showModalBottomSheet<void>(
|
||||
@@ -1010,6 +1018,16 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _showMicrophones,
|
||||
tooltip: l10n.callTooltipMicrophone,
|
||||
icon: Icon(
|
||||
Symbols.settings_voice,
|
||||
color: cs.onSurface,
|
||||
weight: 500,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _showInfoSheet,
|
||||
tooltip: l10n.callInfoTitle,
|
||||
@@ -1295,10 +1313,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
icon: Symbols.mic,
|
||||
slashedIcon: Symbols.mic_off,
|
||||
slashed: _isMuted,
|
||||
label: _isMuted ? l10n.callUnmute : l10n.callMute,
|
||||
label: _isMuted
|
||||
? (CallNoMute.enabled ? l10n.callMicStillLive : l10n.callUnmute)
|
||||
: l10n.callMute,
|
||||
background: _isMuted ? cs.primary : cs.surfaceContainerHighest,
|
||||
foreground: _isMuted ? cs.onPrimary : cs.onSurface,
|
||||
onTap: _toggleMute,
|
||||
onLongPress: _showMicrophones,
|
||||
),
|
||||
_CallButton(
|
||||
icon: Symbols.call_end,
|
||||
@@ -1364,6 +1385,7 @@ class _CallButton extends StatelessWidget {
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final bool busy;
|
||||
|
||||
const _CallButton({
|
||||
@@ -1372,6 +1394,7 @@ class _CallButton extends StatelessWidget {
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
this.slashedIcon,
|
||||
this.slashed = false,
|
||||
this.busy = false,
|
||||
@@ -1405,6 +1428,7 @@ class _CallButton extends StatelessWidget {
|
||||
color: background,
|
||||
borderRadius: BorderRadius.circular(31),
|
||||
onTap: busy ? null : onTap,
|
||||
onLongPress: busy ? null : onLongPress,
|
||||
depth: 9,
|
||||
child: Center(
|
||||
child: busy
|
||||
|
||||
Reference in New Issue
Block a user