борт управления самолетом

This commit is contained in:
Jganenokk
2026-08-22 14:09:18 +07:00
parent fc66ba84c7
commit c3027a82fb
15 changed files with 1066 additions and 13 deletions
@@ -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),
);
}
}
+30 -6
View File
@@ -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