fix: improve voice and chat controls
This commit is contained in:
@@ -88,6 +88,7 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
static const _channelIconColumnWidth = 24.0;
|
||||
static const _channelTextGap = 8.0;
|
||||
static const _userRowStartIndent = 32.0;
|
||||
static const _clientPlaybackVolumePresets = [0.25, 0.5, 1.0, 2.0, 4.0];
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
final Map<BigInt, bool> _channelExpandedById = {};
|
||||
@@ -311,6 +312,7 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
)
|
||||
: null;
|
||||
final canAdjustPlayback = _canAdjustClientPlayback(client);
|
||||
Offset? secondaryTapPosition;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
@@ -320,12 +322,15 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: canAdjustPlayback
|
||||
? () =>
|
||||
unawaited(_showClientPlaybackSheet(client, withHaptic: true))
|
||||
? () => unawaited(_showClientPlaybackMenu(client, withHaptic: true))
|
||||
: null,
|
||||
onSecondaryTapDown: canAdjustPlayback
|
||||
? (details) => secondaryTapPosition = details.globalPosition
|
||||
: null,
|
||||
onSecondaryTap: canAdjustPlayback
|
||||
? () =>
|
||||
unawaited(_showClientPlaybackSheet(client, withHaptic: true))
|
||||
? () => unawaited(
|
||||
_showClientPlaybackMenu(client, anchor: secondaryTapPosition),
|
||||
)
|
||||
: null,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
@@ -336,14 +341,6 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -401,8 +398,9 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
const ClientPlaybackPreference();
|
||||
}
|
||||
|
||||
Future<void> _showClientPlaybackSheet(
|
||||
Future<void> _showClientPlaybackMenu(
|
||||
rust.BridgeClient client, {
|
||||
Offset? anchor,
|
||||
bool withHaptic = false,
|
||||
}) async {
|
||||
if (!_canAdjustClientPlayback(client)) return;
|
||||
@@ -415,72 +413,52 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
||||
var preference = _clientPlaybackPreference(client);
|
||||
await showModalBottomSheet<void>(
|
||||
final preference = _clientPlaybackPreference(client);
|
||||
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||
final position = anchor ?? overlay.size.center(Offset.zero);
|
||||
final selected = await showMenu<_ClientPlaybackMenuAction>(
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
position: RelativeRect.fromLTRB(
|
||||
position.dx,
|
||||
position.dy,
|
||||
overlay.size.width - position.dx,
|
||||
overlay.size.height - position.dy,
|
||||
),
|
||||
items: [
|
||||
PopupMenuItem<_ClientPlaybackMenuAction>(
|
||||
value: const _ToggleMuteClientPlaybackMenuAction(),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
preference.muted ? Icons.volume_off : Icons.volume_up,
|
||||
),
|
||||
title: Text(preference.muted ? 'Unmute playback' : 'Mute playback'),
|
||||
subtitle: Text(client.name),
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
..._clientPlaybackVolumePresets.map(
|
||||
(preset) => CheckedPopupMenuItem<_ClientPlaybackMenuAction>(
|
||||
value: _VolumeClientPlaybackMenuAction(preset),
|
||||
checked:
|
||||
!preference.muted && (preference.volume - preset).abs() < 0.001,
|
||||
child: Text('Volume ${(preset * 100).round()}%'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (selected == null || !mounted) return;
|
||||
final next = switch (selected) {
|
||||
_ToggleMuteClientPlaybackMenuAction() => preference.copyWith(
|
||||
muted: !preference.muted,
|
||||
),
|
||||
_VolumeClientPlaybackMenuAction(:final volume) => preference.copyWith(
|
||||
volume: volume,
|
||||
muted: false,
|
||||
),
|
||||
};
|
||||
await onChanged(client, next);
|
||||
}
|
||||
|
||||
({Widget icon, bool isSpeaking}) _clientVoiceStatusIcon(
|
||||
@@ -695,6 +673,20 @@ class _ChannelTreeNode {
|
||||
final List<_ChannelTreeNode> children = [];
|
||||
}
|
||||
|
||||
sealed class _ClientPlaybackMenuAction {
|
||||
const _ClientPlaybackMenuAction();
|
||||
}
|
||||
|
||||
class _ToggleMuteClientPlaybackMenuAction extends _ClientPlaybackMenuAction {
|
||||
const _ToggleMuteClientPlaybackMenuAction();
|
||||
}
|
||||
|
||||
class _VolumeClientPlaybackMenuAction extends _ClientPlaybackMenuAction {
|
||||
const _VolumeClientPlaybackMenuAction(this.volume);
|
||||
|
||||
final double volume;
|
||||
}
|
||||
|
||||
_ChannelTree _buildChannelTree(List<rust.BridgeChannel> channels) {
|
||||
final byParent = <BigInt, List<rust.BridgeChannel>>{};
|
||||
final knownIds = {for (final channel in channels) channel.id};
|
||||
|
||||
Reference in New Issue
Block a user