petsHero-AI/lib/features/profile/profile_screen.dart

352 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../core/api/api_config.dart';
import '../../core/api/services/user_api.dart';
import '../../core/auth/auth_service.dart';
import '../../core/theme/app_colors.dart';
import '../../core/user/user_state.dart';
import '../../core/theme/app_spacing.dart';
import '../../core/theme/app_typography.dart';
/// Profile screen - matches Pencil KXeow
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key, required this.isActive});
final bool isActive;
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
@override
void initState() {
super.initState();
if (widget.isActive) _fetchAccount();
}
@override
void didUpdateWidget(covariant ProfileScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive && !oldWidget.isActive) {
_fetchAccount();
}
}
Future<void> _fetchAccount() async {
final uid = UserState.userId.value;
if (uid == null || uid.isEmpty) return;
try {
await AuthService.loginComplete;
final res = await UserApi.getAccount(
sentinel: ApiConfig.appId,
asset: uid,
);
if (!mounted) return;
if (res.isSuccess && res.data != null) {
final data = res.data as Map<String, dynamic>?;
final credits = data?['reveal'] as int?;
if (credits != null) UserState.setCredits(credits);
final avatarUrl = data?['realm'] as String?;
UserState.setAvatar(
avatarUrl != null && avatarUrl.isNotEmpty ? avatarUrl : null);
final name = data?['terminal'] as String?;
UserState.setUserName(
name != null && name.isNotEmpty ? name : null);
}
} catch (_) {}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.screenPadding,
AppSpacing.xxl,
AppSpacing.screenPadding,
AppSpacing.screenPaddingLarge,
),
child: Column(
children: [
ValueListenableBuilder<String?>(
valueListenable: UserState.avatar,
builder: (_, avatarUrl, __) {
return ValueListenableBuilder<String?>(
valueListenable: UserState.userName,
builder: (_, userName, __) {
return ValueListenableBuilder<String?>(
valueListenable: UserState.userId,
builder: (_, userId, __) {
return _ProfileHeader(
avatarUrl: avatarUrl,
userName: userName,
uid: userId,
);
},
);
},
);
},
),
const SizedBox(height: AppSpacing.xl),
_BalanceCard(
balance: UserCreditsData.of(context)?.creditsDisplay ?? '--',
onRecharge: () => Navigator.of(context).pushNamed('/recharge'),
),
const SizedBox(height: AppSpacing.xxl),
_MenuSection(
items: [
_MenuItem(
title: 'Credit Store',
icon: LucideIcons.chevron_right,
onTap: () => Navigator.of(context).pushNamed('/recharge'),
),
_MenuItem(
title: 'Settings',
icon: LucideIcons.chevron_right,
onTap: () {},
),
],
),
],
),
),
);
}
}
class _ProfileHeader extends StatelessWidget {
const _ProfileHeader({
this.avatarUrl,
this.userName,
this.uid,
});
final String? avatarUrl;
final String? userName;
final String? uid;
@override
Widget build(BuildContext context) {
return Container(
height: 220,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(40),
border: Border.all(
color: AppColors.primaryLight,
width: 2,
),
),
clipBehavior: Clip.antiAlias,
child: avatarUrl != null && avatarUrl!.isNotEmpty
? CachedNetworkImage(
imageUrl: avatarUrl!,
fit: BoxFit.cover,
placeholder: (_, __) => Icon(
LucideIcons.user,
size: 40,
color: AppColors.textSecondary,
),
errorWidget: (_, __, ___) => Icon(
LucideIcons.user,
size: 40,
color: AppColors.textSecondary,
),
)
: Icon(
LucideIcons.user,
size: 40,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppSpacing.lg),
Text(
userName ?? 'Guest',
style: AppTypography.bodyLarge.copyWith(
color: AppColors.textPrimary,
),
),
const SizedBox(height: AppSpacing.lg),
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm,
),
decoration: BoxDecoration(
color: AppColors.surfaceAlt,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.border),
),
child: Text(
uid != null && uid!.isNotEmpty ? 'UID $uid' : 'UID --',
style: AppTypography.caption.copyWith(
color: AppColors.textSecondary,
),
),
),
],
),
);
}
}
class _BalanceCard extends StatelessWidget {
const _BalanceCard({
required this.balance,
required this.onRecharge,
});
final String balance;
final VoidCallback onRecharge;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xxl,
vertical: AppSpacing.xl,
),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.shadowLight,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AVAILABLE BALANCE',
style: AppTypography.label.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppSpacing.sm),
Text(
balance,
style: AppTypography.bodyLarge.copyWith(
fontSize: 24,
fontWeight: FontWeight.w700,
color: AppColors.primary,
),
),
],
),
GestureDetector(
onTap: onRecharge,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: AppSpacing.md,
),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Recharge',
style: AppTypography.bodyRegular.copyWith(
color: AppColors.surface,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
}
}
class _MenuSection extends StatelessWidget {
const _MenuSection({required this.items});
final List<_MenuItem> items;
@override
Widget build(BuildContext context) {
return Column(
children: items
.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.md),
child: _MenuItem(
title: item.title,
icon: item.icon,
onTap: item.onTap,
),
),
)
.toList(),
);
}
}
class _MenuItem extends StatelessWidget {
const _MenuItem({
required this.title,
required this.icon,
required this.onTap,
});
final String title;
final IconData icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl,
vertical: 14,
),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: AppColors.shadowLight,
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: AppTypography.bodyRegular.copyWith(
color: AppColors.textPrimary,
),
),
Icon(icon, size: 20, color: AppColors.textMuted),
],
),
),
);
}
}