FunyMeeAI/lib/features/profile/profile_screen.dart
2026-04-10 15:36:08 +08:00

335 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../../core/app_env.dart';
import '../../core/ext_config_document_urls.dart';
import '../../core/user/user_state.dart';
import '../../design/pencil_theme.dart';
import '../../widgets/pencil_chrome.dart';
import '../purchase/purchase_screen.dart';
import '../web/app_web_view_screen.dart';
import 'delete_account_flow.dart';
/// `5J8Po` 个人中心。
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key, this.isRootTab = false});
/// When true (e.g. bottom tab), hide close; when pushed, show close.
final bool isRootTab;
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
String _version = '';
@override
void initState() {
super.initState();
PackageInfo.fromPlatform().then((p) {
if (mounted) setState(() => _version = p.version);
});
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: PencilTheme.yellowWhitePageGradient,
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
bottom: false,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
child: SizedBox(
height: 56,
child: widget.isRootTab
? Center(
child: Text(
'Profile',
style: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w600,
color: PencilTheme.ink,
),
),
)
: Align(
alignment: Alignment.centerRight,
child: PencilRoundCloseButton(
onPressed: () => Navigator.of(context).pop(),
),
),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.only(bottom: 28),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: Column(
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(
color: PencilTheme.profileAvatarRing,
width: 2,
),
),
child: ValueListenableBuilder<String?>(
valueListenable: UserState.avatar,
builder: (context, url, _) {
if (url != null && url.isNotEmpty) {
return ClipOval(
child: Image.network(
url,
fit: BoxFit.cover,
width: 100,
height: 100,
errorBuilder: (_, _, _) =>
_avatarFallback(),
),
);
}
return _avatarFallback();
},
),
),
const SizedBox(height: 12),
ValueListenableBuilder<String?>(
valueListenable: UserState.userId,
builder: (context, id, _) {
return Text(
'ID${id ?? ''}',
style: GoogleFonts.inter(
fontSize: 17,
fontWeight: FontWeight.w700,
color: PencilTheme.stone900,
),
);
},
),
const SizedBox(height: 4),
ValueListenableBuilder<int>(
valueListenable: UserState.credits,
builder: (context, c, _) {
return Text(
'Credits · ${_formatCredits(c)}',
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: PencilTheme.profileCredits,
),
);
},
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: _menuCard(context),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const PurchaseScreen(),
),
);
},
child: Text(
'Buy credits',
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
color: PencilTheme.underlineGold,
),
),
),
TextButton(
onPressed: () async {
await UserAccountRefresh.fetchAndNotify(
app: currentBackendAppType(),
userId: UserState.userId.value,
onAccount: (a) {
if (a.credits != null) {
UserState.setCredits(a.credits!);
}
if (a.avatar != null) {
UserState.setAvatar(a.avatar!);
}
if (a.userName != null) {
UserState.setUserName(a.userName!);
}
},
onFailure: (m) {
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(m)));
}
},
);
},
child: Text(
'Refresh',
style: GoogleFonts.inter(
fontWeight: FontWeight.w600,
color: PencilTheme.stone600,
),
),
),
],
),
const SizedBox(height: 24),
],
),
),
],
),
),
),
);
}
Widget _avatarFallback() {
return Icon(Icons.person_rounded,
size: 44, color: PencilTheme.profileAvatarIcon);
}
String _formatCredits(int c) {
final s = c.toString();
if (s.length <= 3) return s;
return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
}
void _openAppWebView(
BuildContext context, {
required String title,
required String? url,
}) {
if (url == null || url.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Link not configured')),
);
return;
}
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => AppWebViewScreen(
title: title,
initialUrl: url.trim(),
),
),
);
}
Widget _menuCard(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: PencilTheme.genHintBorder),
boxShadow: [
BoxShadow(
color: const Color(0x30CA8A04),
blurRadius: 20,
offset: const Offset(0, 6),
),
],
),
child: Column(
children: [
_row(
'Terms of Service',
trailing: Icons.chevron_right_rounded,
onTap: () => _openAppWebView(
context,
title: 'Terms of Service',
url: ExtConfigDocumentUrls.agreementUrl,
),
),
_divider(),
_row(
'Privacy Policy',
trailing: Icons.chevron_right_rounded,
onTap: () => _openAppWebView(
context,
title: 'Privacy Policy',
url: ExtConfigDocumentUrls.privacyUrl,
),
),
_divider(),
_row('Version', value: 'v$_version'),
_divider(),
_row('Delete account',
danger: true,
trailing: Icons.chevron_right_rounded,
onTap: () => _delete(context)),
],
),
);
}
Widget _row(String title,
{IconData? trailing, String? value, bool danger = false, VoidCallback? onTap}) {
return ListTile(
onTap: onTap,
title: Text(
title,
style: GoogleFonts.inter(
fontSize: 15,
fontWeight: FontWeight.w600,
color: danger ? const Color(0xFFDC2626) : PencilTheme.stone700,
),
),
trailing: value != null
? Text(value,
style: GoogleFonts.inter(
fontSize: 14,
color: const Color(0xFF78716C)))
: Icon(trailing,
color: danger ? const Color(0xFFFCA292) : const Color(0xFFA8A29E)),
);
}
Widget _divider() =>
Container(height: 1, color: const Color(0xFFF5F5F4));
Future<void> _delete(BuildContext context) async {
final ok = await showDeleteAccountConfirmationFlow(context);
if (ok != true || !context.mounted) return;
final res = await UserApi.deleteAccount(
app: currentBackendAppType(),
userId: UserState.userId.value,
);
if (!context.mounted) return;
if (res.isSuccess) {
ApiClient.instance.setUserToken(null);
UserState.clear();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Account deleted. Please restart the app and sign in again.')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(res.msg)),
);
}
}
}