chore: restore product scaffold to rollback baseline
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -8,17 +6,10 @@ 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.
|
||||
@@ -34,8 +25,10 @@ class SnapshotView extends StatefulWidget {
|
||||
required this.canJoinVoiceChannel,
|
||||
required this.onJoinChannel,
|
||||
required this.onJoinChannelWithPassword,
|
||||
this.clientPlaybackPreferences = const {},
|
||||
this.onClientPlaybackPreferenceChanged,
|
||||
this.enableClientLongPressMenu = false,
|
||||
this.onOpenClientInfo,
|
||||
this.onOpenClientChat,
|
||||
this.onOpenClientPoke,
|
||||
this.onTs3ServerLink,
|
||||
});
|
||||
|
||||
@@ -69,11 +62,17 @@ 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;
|
||||
/// True on touch-only mobile hosts where long-press opens client actions.
|
||||
final bool enableClientLongPressMenu;
|
||||
|
||||
/// Apply an updated per-client playback preference.
|
||||
final ClientPlaybackPreferenceChanged? onClientPlaybackPreferenceChanged;
|
||||
/// Open richer profile details for a non-self client.
|
||||
final ValueChanged<rust.BridgeClient>? onOpenClientInfo;
|
||||
|
||||
/// Open a direct chat with a non-self client.
|
||||
final ValueChanged<rust.BridgeClient>? onOpenClientChat;
|
||||
|
||||
/// Open a poke composer for a non-self client.
|
||||
final ValueChanged<rust.BridgeClient>? onOpenClientPoke;
|
||||
|
||||
/// Handle TeamSpeak server links embedded in server-provided text.
|
||||
final Ts3ServerLinkHandler? onTs3ServerLink;
|
||||
@@ -88,11 +87,10 @@ 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 = {};
|
||||
bool _welcomeExpanded = true;
|
||||
bool _welcomeExpanded = false;
|
||||
double _welcomeHeight = 0;
|
||||
final _welcomeKey = GlobalKey();
|
||||
|
||||
@@ -293,6 +291,12 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
fontWeight: FontWeight.w600,
|
||||
)
|
||||
: null;
|
||||
final isSelf = client.id == widget.snapshot.ownClientId;
|
||||
final canOpenPeerActions =
|
||||
!isSelf &&
|
||||
(widget.onOpenClientChat != null || widget.onOpenClientPoke != null);
|
||||
final canOpenClientMenu =
|
||||
widget.onOpenClientInfo != null || canOpenPeerActions;
|
||||
|
||||
final decoration = status.isSpeaking
|
||||
? BoxDecoration(
|
||||
@@ -311,40 +315,90 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
],
|
||||
)
|
||||
: null;
|
||||
final canAdjustPlayback = _canAdjustClientPlayback(client);
|
||||
Offset? secondaryTapPosition;
|
||||
|
||||
return Padding(
|
||||
final tile = Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: channelIndent + _userRowStartIndent,
|
||||
right: 8,
|
||||
),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: canAdjustPlayback
|
||||
? () => unawaited(_showClientPlaybackMenu(client, withHaptic: true))
|
||||
: null,
|
||||
onSecondaryTapDown: canAdjustPlayback
|
||||
? (details) => secondaryTapPosition = details.globalPosition
|
||||
: null,
|
||||
onSecondaryTap: canAdjustPlayback
|
||||
? () => unawaited(
|
||||
_showClientPlaybackMenu(client, anchor: secondaryTapPosition),
|
||||
)
|
||||
: 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),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!canOpenClientMenu) return tile;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onSecondaryTapDown: (details) =>
|
||||
_showClientMenu(client, details.globalPosition),
|
||||
onLongPressStart: widget.enableClientLongPressMenu
|
||||
? (details) => _showClientMenu(client, details.globalPosition)
|
||||
: null,
|
||||
child: tile,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showClientMenu(
|
||||
rust.BridgeClient client,
|
||||
Offset globalPosition,
|
||||
) async {
|
||||
final overlay = Overlay.of(context).context.findRenderObject();
|
||||
if (overlay is! RenderBox) return;
|
||||
final isSelf = client.id == widget.snapshot.ownClientId;
|
||||
|
||||
final selected = await showMenu<_ClientMenuAction>(
|
||||
context: context,
|
||||
position: RelativeRect.fromRect(
|
||||
Rect.fromLTWH(globalPosition.dx, globalPosition.dy, 0, 0),
|
||||
Offset.zero & overlay.size,
|
||||
),
|
||||
items: [
|
||||
if (widget.onOpenClientInfo != null)
|
||||
PopupMenuItem(
|
||||
value: _ClientMenuAction.info,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: Text(AppL10n.of(context).clientInfoAction),
|
||||
),
|
||||
),
|
||||
if (!isSelf && widget.onOpenClientChat != null)
|
||||
PopupMenuItem(
|
||||
value: _ClientMenuAction.directMessage,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.chat_bubble_outline),
|
||||
title: Text(AppL10n.of(context).chatDirectMessageAction),
|
||||
),
|
||||
),
|
||||
if (!isSelf && widget.onOpenClientPoke != null)
|
||||
PopupMenuItem(
|
||||
value: _ClientMenuAction.poke,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.notifications_active_outlined),
|
||||
title: Text(AppL10n.of(context).chatPokeAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!mounted || selected == null) return;
|
||||
switch (selected) {
|
||||
case _ClientMenuAction.info:
|
||||
widget.onOpenClientInfo?.call(client);
|
||||
case _ClientMenuAction.directMessage:
|
||||
widget.onOpenClientChat?.call(client);
|
||||
case _ClientMenuAction.poke:
|
||||
widget.onOpenClientPoke?.call(client);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _expandButton(
|
||||
@@ -386,81 +440,6 @@ 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> _showClientPlaybackMenu(
|
||||
rust.BridgeClient client, {
|
||||
Offset? anchor,
|
||||
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;
|
||||
|
||||
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,
|
||||
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(
|
||||
ThemeData theme,
|
||||
rust.BridgeClient client,
|
||||
@@ -530,6 +509,8 @@ class _SnapshotViewState extends State<SnapshotView> {
|
||||
}
|
||||
}
|
||||
|
||||
enum _ClientMenuAction { info, directMessage, poke }
|
||||
|
||||
class _SpacerChannelContent extends StatelessWidget {
|
||||
const _SpacerChannelContent({required this.spacer});
|
||||
|
||||
@@ -673,20 +654,6 @@ 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};
|
||||
@@ -757,7 +724,7 @@ class _WelcomeMessageTile extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Server welcome message',
|
||||
AppL10n.of(context).serverWelcomeHeading,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -766,20 +733,22 @@ class _WelcomeMessageTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
firstChild: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
|
||||
child: BbCodeText(
|
||||
welcomeMessage,
|
||||
linkTrust: LinkTrustService.instance,
|
||||
onTs3ServerLink: onTs3ServerLink,
|
||||
),
|
||||
),
|
||||
secondChild: const SizedBox(width: double.infinity),
|
||||
crossFadeState: expanded
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: expanded
|
||||
? Padding(
|
||||
key: const ValueKey('welcome-expanded'),
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
|
||||
child: BbCodeText(
|
||||
welcomeMessage,
|
||||
linkTrust: LinkTrustService.instance,
|
||||
onTs3ServerLink: onTs3ServerLink,
|
||||
),
|
||||
)
|
||||
: const SizedBox(
|
||||
key: ValueKey('welcome-collapsed'),
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user