Files
sm-emby-share/lib/features/account/account_center_page.dart
T
2026-07-14 11:11:36 +08:00

1046 lines
31 KiB
Dart

import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:forui/forui.dart';
import 'package:go_router/go_router.dart';
import '../../providers/server_provider.dart';
import '../../providers/server_sync_provider.dart';
import '../../providers/sm_account_provider.dart';
import '../../shared/constants/breakpoints.dart';
import '../../shared/theme/app_theme.dart';
import '../../shared/theme/app_theme_scope.dart';
import '../../shared/widgets/app_card.dart';
import '../../shared/widgets/app_dialog.dart';
import '../../shared/widgets/app_inline_alert.dart';
import '../../shared/widgets/app_sliver_header.dart';
import '../../shared/widgets/app_loading_ring.dart';
import '../../shared/widgets/app_snack_bar.dart';
import '../../shared/widgets/page_background.dart';
import 'account_auth_page.dart';
typedef AccountFeedback = void Function(String message, {AppAlertTone tone});
Future<String?> showAccountCenterSheet(BuildContext context) {
final isCompact = Breakpoints.isCompact(MediaQuery.sizeOf(context).width);
if (isCompact) return _showAccountBottomSheet(context);
return _showAccountSideSheet(context);
}
Future<String?> _showAccountBottomSheet(BuildContext context) {
return showModalBottomSheet<String>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
useSafeArea: true,
isDismissible: true,
backgroundColor: Colors.transparent,
barrierColor: Colors.black38,
builder: (_) => const AppThemeScope(child: _AccountBottomSheet()),
);
}
Future<String?> _showAccountSideSheet(BuildContext context) {
return showGeneralDialog<String>(
context: context,
useRootNavigator: true,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black38,
transitionDuration: const Duration(milliseconds: 220),
pageBuilder: (_, _, _) => const AppThemeScope(child: _AccountSideSheet()),
transitionBuilder: (_, animation, _, child) {
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(curved),
child: child,
);
},
);
}
class AccountCenterPage extends ConsumerStatefulWidget {
const AccountCenterPage({super.key});
@override
ConsumerState<AccountCenterPage> createState() => _AccountCenterPageState();
}
class _AccountCenterPageState extends ConsumerState<AccountCenterPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
extendBodyBehindAppBar: true,
body: Stack(
children: [
const Positioned.fill(child: PageBackground()),
CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics(),
),
slivers: [
AppSliverHeader(
title: '账号',
leading: IconButton(
icon: const Icon(Icons.arrow_back_rounded),
onPressed: () => context.pop(),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
sliver: SliverToBoxAdapter(
child: AccountCenterContent(
onAuthRequested: _openAuth,
onFeedback: _pageFeedback,
syncConfirmInPopover: false,
),
),
),
],
),
],
),
);
}
Future<void> _openAuth() async {
final message = await context.push<String>('/account/auth');
if (!mounted || message == null || message.isEmpty) return;
showAppSnackBar(context, message, tone: AppSnackTone.success);
}
void _pageFeedback(String message, {AppAlertTone tone = AppAlertTone.info}) {
showAppSnackBar(
context,
message,
tone: switch (tone) {
AppAlertTone.error => AppSnackTone.error,
AppAlertTone.warning => AppSnackTone.warning,
AppAlertTone.success => AppSnackTone.success,
AppAlertTone.info => AppSnackTone.neutral,
},
);
}
}
class AccountCenterContent extends ConsumerStatefulWidget {
final VoidCallback onAuthRequested;
final AccountFeedback onFeedback;
final bool syncConfirmInPopover;
const AccountCenterContent({
super.key,
required this.onAuthRequested,
required this.onFeedback,
required this.syncConfirmInPopover,
});
@override
ConsumerState<AccountCenterContent> createState() =>
_AccountCenterContentState();
}
class _AccountCenterContentState extends ConsumerState<AccountCenterContent> {
bool _logoutBusy = false;
@override
Widget build(BuildContext context) {
final account = ref.watch(smAccountProvider).value;
final syncState = ref.watch(serverSyncProvider);
final status = account?.status ?? SmAuthStatus.unauthenticated;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildAccountSection(context, account, status),
const SizedBox(height: 24),
_buildSyncSection(context, account, syncState),
],
);
}
Widget _buildAccountSection(
BuildContext context,
SmAccountState? account,
SmAuthStatus status,
) {
return switch (status) {
SmAuthStatus.authenticated => _buildLoggedIn(context, account!),
SmAuthStatus.restoring => _buildRestoring(context, account!),
SmAuthStatus.expired => _buildReauth(
context,
account!,
title: '登录已过期',
subtitle: '请重新登录以继续使用扩展服务',
),
SmAuthStatus.degraded => _buildReauth(
context,
account!,
title: '账号凭据不完整',
subtitle: '请重新登录以继续使用扩展服务',
),
SmAuthStatus.unauthenticated => _buildEntry(context),
};
}
Widget _buildEntry(BuildContext context) {
final theme = Theme.of(context);
return AppCard(
onTap: widget.onAuthRequested,
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: theme.colorScheme.onSurface.withValues(alpha: 0.08),
shape: BoxShape.circle,
),
child: Icon(
Icons.person_outline_rounded,
size: 24,
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'登录 Player 账号',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
const SizedBox(height: 3),
Text(
'登录以使用云同步和 TMDB 等扩展服务',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.58),
),
),
],
),
),
const Icon(Icons.chevron_right_rounded),
],
),
);
}
Widget _buildRestoring(BuildContext context, SmAccountState s) {
final theme = Theme.of(context);
return AppCard(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Icon(
Icons.person_rounded,
size: 24,
color: theme.colorScheme.primary,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.email.isEmpty ? 'Player 账号' : s.email,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
const SizedBox(height: 3),
Text(
'正在恢复登录…',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.58),
),
),
],
),
),
const AppLoadingRing(size: 18),
],
),
);
}
Widget _buildReauth(
BuildContext context,
SmAccountState s, {
required String title,
required String subtitle,
}) {
final theme = Theme.of(context);
return AppCard(
onTap: widget.onAuthRequested,
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFFFAAD14).withValues(alpha: 0.15),
shape: BoxShape.circle,
),
child: const Icon(
Icons.error_outline,
size: 24,
color: Color(0xFFFAAD14),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
const SizedBox(height: 3),
Text(
subtitle,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.58),
),
),
],
),
),
const Icon(Icons.chevron_right_rounded),
],
),
);
}
Widget _buildLoggedIn(BuildContext context, SmAccountState s) {
final theme = Theme.of(context);
return AppCard(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: s.isVip
? const Color(0xFFFAAD14).withValues(alpha: 0.15)
: theme.colorScheme.primary.withValues(alpha: 0.12),
shape: BoxShape.circle,
border: s.isVip
? Border.all(
color: const Color(0xFFFAAD14).withValues(alpha: 0.4),
width: 1.5,
)
: null,
),
child: Icon(
s.isVip
? Icons.workspace_premium_rounded
: Icons.person_rounded,
size: 28,
color: s.isVip
? const Color(0xFFFAAD14)
: theme.colorScheme.primary,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.email.isNotEmpty ? s.email : 'Player 账号',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 17,
),
),
const SizedBox(height: 4),
Text(
_vipLabel(s),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.58,
),
),
),
],
),
),
if (s.isVip)
Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFFAAD14),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'VIP',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700,
),
),
),
],
),
const Divider(height: 32),
InkWell(
onTap: _logoutBusy ? null : _logout,
child: Row(
children: [
Icon(
Icons.logout,
size: 18,
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'退出登录',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
),
if (_logoutBusy) const AppLoadingRing(size: 18),
],
),
),
],
),
);
}
Widget _buildSyncSection(
BuildContext context,
SmAccountState? account,
ServerSyncState syncState,
) {
final theme = Theme.of(context);
final hasVip = account?.hasVip ?? false;
final busy = syncState.busy;
final localCount = ref.watch(serverListProvider).value?.length ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
child: Text(
'云同步',
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.55),
fontWeight: FontWeight.w600,
),
),
),
AppCard(
padding: EdgeInsets.zero,
child: Column(
children: [
_buildSyncStatus(context, syncState, hasVip),
_divider(context),
_SyncActionRow(
icon: Icons.cloud_upload_outlined,
label: '上传到云端',
enabled: !busy && hasVip,
busy: busy,
usePopover: widget.syncConfirmInPopover,
confirmTitle: '上传到云端?',
confirmMessage: '这将把本地 $localCount 个服务器及对应登录态整包上传到云端,覆盖云端已有备份。',
confirmLabel: '确认上传',
onConfirm: () => _cloudUpload(localCount),
),
_divider(context),
_SyncActionRow(
icon: Icons.cloud_download_outlined,
label: '从云端拉取',
enabled: !busy && hasVip,
busy: busy,
usePopover: widget.syncConfirmInPopover,
destructive: true,
confirmTitle: '从云端拉取?',
confirmMessage:
'这将用云端备份整包替换本地当前的 $localCount 个服务器及对应登录态;弹幕源按 URL 合并(云端为权威)。本地未上传到云端的改动会丢失。',
confirmLabel: '替换并拉取',
onConfirm: () => _cloudPull(localCount),
),
],
),
),
if (!hasVip)
Padding(
padding: const EdgeInsets.fromLTRB(4, 8, 4, 0),
child: Text(
'云同步需要 VIP 会员',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.45),
),
),
),
],
);
}
Widget _buildSyncStatus(
BuildContext context,
ServerSyncState syncState,
bool hasVip,
) {
final theme = Theme.of(context);
String statusText;
Color? statusColor;
if (syncState.lastSyncError != null) {
statusText = '同步失败';
statusColor = const Color(0xFFEF4444);
} else if (syncState.lastSyncAt != null) {
statusText = '上次同步:${_formatTime(syncState.lastSyncAt!)}';
statusColor = null;
} else if (!hasVip) {
statusText = '未启用';
statusColor = null;
} else {
statusText = '未同步';
statusColor = null;
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
child: Row(
children: [
Icon(
syncState.lastSyncError != null
? Icons.error_outline
: Icons.cloud_done_outlined,
size: 20,
color:
statusColor ??
theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
const SizedBox(width: 14),
Text(
statusText,
style: theme.textTheme.bodyMedium?.copyWith(
color:
statusColor ??
theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
);
}
Widget _divider(BuildContext context) {
return Divider(
height: 1,
indent: 50,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.08),
);
}
String _vipLabel(SmAccountState s) {
if (s.vipPermanent) return '永久 VIP';
if (s.isVip && s.vipExpiresAt != null) {
final d = DateTime.fromMillisecondsSinceEpoch(s.vipExpiresAt!);
String two(int v) => v.toString().padLeft(2, '0');
return 'VIP 有效期至 ${d.year}-${two(d.month)}-${two(d.day)}';
}
return '已登录';
}
String _formatTime(DateTime t) {
String two(int v) => v.toString().padLeft(2, '0');
final now = DateTime.now();
if (t.year == now.year && t.month == now.month && t.day == now.day) {
return '今天 ${two(t.hour)}:${two(t.minute)}';
}
return '${t.month}/${t.day} ${two(t.hour)}:${two(t.minute)}';
}
Future<void> _logout() async {
setState(() => _logoutBusy = true);
await ref.read(smAccountProvider.notifier).logout();
if (!mounted) return;
setState(() => _logoutBusy = false);
widget.onFeedback('已退出登录', tone: AppAlertTone.success);
}
Future<void> _cloudUpload(int localCount) async {
if (!widget.syncConfirmInPopover) {
final confirmed = await showAppConfirmDialog(
context,
title: '上传到云端',
body: Text('这将把本地 $localCount 个服务器及对应登录态整包上传到云端,覆盖云端已有的备份。确定继续?'),
confirmLabel: '上传',
);
if (!confirmed || !mounted) return;
}
final outcome = await ref.read(serverSyncProvider.notifier).upload();
if (!mounted) return;
widget.onFeedback(
outcome.message,
tone: outcome.isError ? AppAlertTone.error : AppAlertTone.info,
);
}
Future<void> _cloudPull(int localCount) async {
if (!widget.syncConfirmInPopover) {
final confirmed = await showAppConfirmDialog(
context,
title: '从云端拉取',
body: Text(
'这将用云端备份整包替换本地当前的 $localCount 个服务器及对应登录态;'
'弹幕源按 URL 合并(云端为权威)。'
'本地未上传到云端的改动会丢失。确定继续?',
),
confirmLabel: '替换并拉取',
destructive: true,
);
if (!confirmed || !mounted) return;
}
final outcome = await ref.read(serverSyncProvider.notifier).pull();
if (!mounted) return;
widget.onFeedback(
outcome.message,
tone: outcome.isError ? AppAlertTone.error : AppAlertTone.info,
);
}
}
class _SyncActionRow extends StatelessWidget {
final IconData icon;
final String label;
final bool enabled;
final bool busy;
final bool usePopover;
final String confirmTitle;
final String confirmMessage;
final String confirmLabel;
final bool destructive;
final Future<void> Function() onConfirm;
const _SyncActionRow({
required this.icon,
required this.label,
required this.enabled,
required this.busy,
required this.usePopover,
required this.confirmTitle,
required this.confirmMessage,
required this.confirmLabel,
required this.onConfirm,
this.destructive = false,
});
@override
Widget build(BuildContext context) {
final child = _SyncActionRowContent(
icon: icon,
label: label,
enabled: enabled,
busy: busy,
);
if (!usePopover) {
return InkWell(
onTap: enabled ? () => unawaited(onConfirm()) : null,
child: child,
);
}
return FPopover(
constraints: const FPortalConstraints(maxWidth: 300),
popoverAnchor: Alignment.topRight,
childAnchor: Alignment.bottomRight,
popoverBuilder: (_, controller) => _SyncConfirmBubble(
title: confirmTitle,
message: confirmMessage,
confirmLabel: confirmLabel,
destructive: destructive,
onCancel: () => unawaited(controller.hide()),
onConfirm: () {
unawaited(controller.hide());
unawaited(onConfirm());
},
),
builder: (_, controller, child) =>
InkWell(onTap: enabled ? controller.toggle : null, child: child),
child: child,
);
}
}
class _SyncActionRowContent extends StatelessWidget {
final IconData icon;
final String label;
final bool enabled;
final bool busy;
const _SyncActionRowContent({
required this.icon,
required this.label,
required this.enabled,
required this.busy,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
child: Row(
children: [
Icon(
icon,
size: 20,
color: enabled
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.3),
),
const SizedBox(width: 14),
Expanded(
child: Text(
label,
style: theme.textTheme.bodyMedium?.copyWith(
color: enabled
? null
: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
),
),
if (busy) const AppLoadingRing(size: 18),
],
),
);
}
}
class _SyncConfirmBubble extends StatelessWidget {
final String title;
final String message;
final String confirmLabel;
final bool destructive;
final VoidCallback onCancel;
final VoidCallback onConfirm;
const _SyncConfirmBubble({
required this.title,
required this.message,
required this.confirmLabel,
required this.destructive,
required this.onCancel,
required this.onConfirm,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final tokens = context.appTokens;
return SizedBox(
width: 280,
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 6),
Text(
message,
style: theme.textTheme.bodySmall?.copyWith(
color: tokens.mutedForeground,
height: 1.35,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FButton(
onPress: onCancel,
variant: FButtonVariant.outline,
mainAxisSize: MainAxisSize.min,
child: const Text('取消'),
),
const SizedBox(width: 8),
FButton(
onPress: onConfirm,
variant: destructive
? FButtonVariant.destructive
: FButtonVariant.primary,
mainAxisSize: MainAxisSize.min,
child: Text(confirmLabel),
),
],
),
],
),
),
);
}
}
class _AccountSideSheet extends StatelessWidget {
const _AccountSideSheet();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.sizeOf(context).width;
final width = math.min(
math.min(480.0, math.max(400.0, screenWidth * 0.32)),
screenWidth * 0.92,
);
return Align(
alignment: Alignment.centerRight,
child: SizedBox(
width: width,
height: double.infinity,
child: Material(
color: theme.colorScheme.surface,
clipBehavior: Clip.antiAlias,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(24),
),
child: const _AccountSheetFrame(isSideSheet: true),
),
),
);
}
}
class _AccountBottomSheet extends StatelessWidget {
const _AccountBottomSheet();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.sizeOf(context);
final viewInsets = MediaQuery.viewInsetsOf(context);
final sheet = Material(
color: theme.colorScheme.surface,
clipBehavior: Clip.antiAlias,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: const SafeArea(
top: false,
child: _AccountSheetFrame(isSideSheet: false),
),
);
return AnimatedPadding(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
padding: EdgeInsets.only(bottom: viewInsets.bottom),
child: Align(
alignment: Alignment.bottomCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: size.height * 0.92),
child: sheet,
),
),
);
}
}
class _AccountSheetFrame extends StatefulWidget {
final bool isSideSheet;
const _AccountSheetFrame({required this.isSideSheet});
@override
State<_AccountSheetFrame> createState() => _AccountSheetFrameState();
}
class _AccountSheetFrameState extends State<_AccountSheetFrame> {
bool _authMode = false;
@override
Widget build(BuildContext context) {
Widget body = AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: _authMode ? _authBody(context) : _overviewBody(context),
);
body = widget.isSideSheet
? Expanded(child: body)
: Flexible(fit: FlexFit.loose, child: body);
return Column(
mainAxisSize: widget.isSideSheet ? MainAxisSize.max : MainAxisSize.min,
children: [
if (widget.isSideSheet)
SizedBox(height: MediaQuery.paddingOf(context).top),
_AccountSheetTitleBar(
showBack: _authMode,
isSideSheet: widget.isSideSheet,
onBack: () => setState(() => _authMode = false),
onClose: () => Navigator.of(context).pop(),
),
Divider(
height: 1,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.08),
),
body,
],
);
}
Widget _overviewBody(BuildContext context) {
return SingleChildScrollView(
key: const ValueKey('overview'),
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: AccountCenterContent(
onAuthRequested: () => setState(() => _authMode = true),
onFeedback: (message, {tone = AppAlertTone.info}) {
showAppToast(context, message, tone: tone);
},
syncConfirmInPopover: true,
),
);
}
Widget _authBody(BuildContext context) {
return SingleChildScrollView(
key: const ValueKey('auth'),
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(24, 18, 24, 28),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: AccountAuthForm(
onSuccess: (message) => Navigator.of(context).pop(message),
),
),
),
);
}
}
class _AccountSheetTitleBar extends StatelessWidget {
final bool showBack;
final bool isSideSheet;
final VoidCallback onBack;
final VoidCallback onClose;
const _AccountSheetTitleBar({
required this.showBack,
required this.isSideSheet,
required this.onBack,
required this.onClose,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final reservedTrailing = isSideSheet
? MediaQuery.paddingOf(context).right
: 0.0;
return SizedBox(
height: 56,
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: showBack
? IconButton(
onPressed: onBack,
icon: const Icon(Icons.arrow_back_rounded),
tooltip: '返回',
)
: const SizedBox(width: 48),
),
),
Text(
'账号',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(right: 8 + reservedTrailing),
child: IconButton(
onPressed: onClose,
icon: const Icon(Icons.close_rounded),
tooltip: '关闭',
),
),
),
],
),
);
}
}