chore: restore product scaffold to rollback baseline

This commit is contained in:
Edison Jwa
2026-05-29 14:02:04 +09:00
parent 2896f14ec9
commit fe6e07353e
434 changed files with 27278 additions and 63230 deletions
@@ -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,
),
),
],
),