feat: promote linux native audio path
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -6,10 +8,17 @@ import '../services/channel_spacer.dart';
|
||||
import '../services/link_trust_service.dart';
|
||||
import '../services/snapshot_state_mapper.dart';
|
||||
import '../services/ts3_server_link.dart';
|
||||
import '../services/ui_preferences_service.dart';
|
||||
import '../src/rust/api.dart' as rust;
|
||||
import 'bbcode_text.dart';
|
||||
import 'talk_power_warning.dart';
|
||||
|
||||
typedef ClientPlaybackPreferenceChanged =
|
||||
Future<void> Function(
|
||||
rust.BridgeClient client,
|
||||
ClientPlaybackPreference preference,
|
||||
);
|
||||
|
||||
/// Connected-server snapshot with welcome text, channels, and clients.
|
||||
class SnapshotView extends StatefulWidget {
|
||||
/// Construct a snapshot view.
|
||||
@@ -25,6 +34,8 @@ class SnapshotView extends StatefulWidget {
|
||||
required this.canJoinVoiceChannel,
|
||||
required this.onJoinChannel,
|
||||
required this.onJoinChannelWithPassword,
|
||||
this.clientPlaybackPreferences = const {},
|
||||
this.onClientPlaybackPreferenceChanged,
|
||||
this.onTs3ServerLink,
|
||||
});
|
||||
|
||||
@@ -58,6 +69,12 @@ class SnapshotView extends StatefulWidget {
|
||||
/// Join a password-protected channel.
|
||||
final ValueChanged<rust.BridgeChannel> onJoinChannelWithPassword;
|
||||
|
||||
/// Persisted per-client playback preferences keyed by TeamSpeak UID.
|
||||
final Map<String, ClientPlaybackPreference> clientPlaybackPreferences;
|
||||
|
||||
/// Apply an updated per-client playback preference.
|
||||
final ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged;
|
||||
|
||||
/// Handle TeamSpeak server links embedded in server-provided text.
|
||||
final Ts3ServerLinkHandler? onTs3ServerLink;
|
||||
|
||||
@@ -293,21 +310,41 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
],
|
||||
)
|
||||
: null;
|
||||
final canAdjustPlayback = _canAdjustClientPlayback(client);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: channelIndent + _userRowStartIndent,
|
||||
right: 8,
|
||||
),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
decoration: decoration,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
leading: status.icon,
|
||||
title: Text(client.name, style: nameStyle),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: canAdjustPlayback
|
||||
? () =>
|
||||
unawaited(_showClientPlaybackSheet(client, withHaptic: true))
|
||||
: null,
|
||||
onSecondaryTap: canAdjustPlayback
|
||||
? () =>
|
||||
unawaited(_showClientPlaybackSheet(client, withHaptic: true))
|
||||
: null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
curve: Curves.easeOut,
|
||||
decoration: decoration,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
leading: status.icon,
|
||||
title: Text(client.name, style: nameStyle),
|
||||
trailing: canAdjustPlayback
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
tooltip: 'Playback options',
|
||||
onPressed: () =>
|
||||
unawaited(_showClientPlaybackSheet(client)),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -352,6 +389,100 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
});
|
||||
}
|
||||
|
||||
bool _canAdjustClientPlayback(rust.BridgeClient client) {
|
||||
return widget.onClientPlaybackPreferenceChanged != null &&
|
||||
client.id != widget.snapshot.ownClientId &&
|
||||
!client.isServerQuery &&
|
||||
client.uid.isNotEmpty;
|
||||
}
|
||||
|
||||
ClientPlaybackPreference _clientPlaybackPreference(rust.BridgeClient client) {
|
||||
return widget.clientPlaybackPreferences[client.uid] ??
|
||||
const ClientPlaybackPreference();
|
||||
}
|
||||
|
||||
Future<void> _showClientPlaybackSheet(
|
||||
rust.BridgeClient client, {
|
||||
bool withHaptic = false,
|
||||
}) async {
|
||||
if (!_canAdjustClientPlayback(client)) return;
|
||||
|
||||
final onChanged = widget.onClientPlaybackPreferenceChanged;
|
||||
if (onChanged == null) return;
|
||||
|
||||
if (withHaptic) {
|
||||
unawaited(HapticFeedback.selectionClick().catchError((_) {}));
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
var preference = _clientPlaybackPreference(client);
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
void updatePreference(ClientPlaybackPreference next) {
|
||||
setModalState(() => preference = next);
|
||||
unawaited(onChanged(client, next));
|
||||
}
|
||||
|
||||
final volumePercent = (preference.volume * 100).round();
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
client.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Per-user playback',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile.adaptive(
|
||||
key: const Key('client-playback-mute-tile'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Mute playback'),
|
||||
subtitle: Text(
|
||||
preference.muted
|
||||
? 'Audio from this user is muted on this server.'
|
||||
: 'Audio from this user plays at $volumePercent%.',
|
||||
),
|
||||
value: preference.muted,
|
||||
onChanged: (muted) =>
|
||||
updatePreference(preference.copyWith(muted: muted)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Volume $volumePercent%',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
Slider(
|
||||
key: const Key('client-playback-volume-slider'),
|
||||
min: 0.0,
|
||||
max: 4.0,
|
||||
divisions: 40,
|
||||
label: '$volumePercent%',
|
||||
value: preference.volume,
|
||||
onChanged: (value) =>
|
||||
updatePreference(preference.copyWith(volume: value)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
({Widget icon, bool isSpeaking}) _clientVoiceStatusIcon(
|
||||
ThemeData theme,
|
||||
rust.BridgeClient client,
|
||||
|
||||
Reference in New Issue
Block a user