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 createState() => _ProfileScreenState(); } class _ProfileScreenState extends State { 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( 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( 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( 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( 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( MaterialPageRoute( 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 _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)), ); } } }