import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/post.dart'; import '../../models/profile.dart'; import '../../models/profile_privacy_settings.dart'; import '../../models/trust_state.dart'; import '../../models/trust_tier.dart'; import '../../providers/api_provider.dart'; import '../../services/auth_service.dart'; import '../../theme/app_theme.dart'; import '../../utils/country_flag.dart'; import '../../widgets/post_item.dart'; import '../../widgets/media/signed_media_image.dart'; import '../compose/compose_screen.dart'; import '../post/post_detail_screen.dart'; import 'profile_settings_screen.dart'; enum ProfileFeedType { posts, saved, chained } /// Premium profile screen with NestedScrollView and SliverAppBar class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @override ConsumerState createState() => _ProfileScreenState(); } String _resolveAvatar(String? url) { if (url == null || url.isEmpty) return ''; if (url.startsWith('http://') || url.startsWith('https://')) return url; return 'https://img.sojorn.net/${url.replaceFirst(RegExp('^/'), '')}'; } class _ProfileScreenState extends ConsumerState with SingleTickerProviderStateMixin { static const int _postsPageSize = 20; StreamSubscription? _authSubscription; Profile? _profile; ProfileStats? _stats; ProfilePrivacySettings? _privacySettings; bool _isPrivacyLoading = false; late TabController _tabController; ProfileFeedType _activeFeed = ProfileFeedType.posts; List _posts = []; bool _isProfileLoading = false; String? _profileError; bool _isCreatingProfile = false; bool _isPostsLoading = false; bool _isPostsLoadingMore = false; bool _hasMorePosts = true; String? _postsError; List _savedPosts = []; bool _isSavedLoading = false; bool _isSavedLoadingMore = false; bool _hasMoreSaved = true; String? _savedError; List _chainedPosts = []; bool _isChainedLoading = false; bool _isChainedLoadingMore = false; bool _hasMoreChained = true; String? _chainedError; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); _tabController.addListener(() { if (!_tabController.indexIsChanging) { setState(() { _activeFeed = ProfileFeedType.values[_tabController.index]; }); _loadActiveFeed(refresh: true); } }); _loadProfile(); _authSubscription = AuthService.instance.authStateChanges.listen((data) { if (data.event == AuthChangeEvent.signedIn || data.event == AuthChangeEvent.tokenRefreshed) { _loadProfile(); } }); } @override void dispose() { _authSubscription?.cancel(); _tabController.dispose(); super.dispose(); } Future _refreshAll() async { await _loadProfile(refreshFeeds: false); await _loadActiveFeed(refresh: true); } Future _loadProfile({bool refreshFeeds = true}) async { setState(() { _isProfileLoading = true; _profileError = null; }); try { final apiService = ref.read(apiServiceProvider); final data = await apiService.getProfile(); final profile = data['profile'] as Profile; final stats = data['stats'] as ProfileStats; if (!mounted) return; setState(() { _profile = profile; _stats = stats; }); await _loadPrivacySettings(); if (refreshFeeds) { await _loadActiveFeed(refresh: true); } } catch (error) { if (!mounted) return; if (_shouldAutoCreateProfile(error)) { await _createProfileIfMissing(); return; } setState(() { _profileError = error.toString().replaceAll('Exception: ', ''); }); } finally { if (mounted) { setState(() { _isProfileLoading = false; }); } } } Future _loadPrivacySettings() async { if (_isPrivacyLoading) return; setState(() { _isPrivacyLoading = true; }); try { final apiService = ref.read(apiServiceProvider); final settings = await apiService.getPrivacySettings(); if (!mounted) return; setState(() { _privacySettings = settings; }); } catch (error) { if (!mounted) return; } finally { if (mounted) { setState(() { _isPrivacyLoading = false; }); } } } bool _shouldAutoCreateProfile(dynamic error) { final errorStr = error.toString().toLowerCase(); return errorStr.contains('profile not found') || errorStr.contains('no profile'); } Future _createProfileIfMissing() async { if (_isCreatingProfile) return; setState(() { _isCreatingProfile = true; _profileError = null; }); try { final apiService = ref.read(apiServiceProvider); final user = AuthService.instance.currentUser; if (user == null) { throw Exception('No authenticated user'); } // Generate a default handle from email or user ID final defaultHandle = user.email?.split('@').first ?? 'user${user.id.substring(0, 8)}'; final defaultDisplayName = user.email?.split('@').first ?? 'User'; await apiService.createProfile( handle: defaultHandle, displayName: defaultDisplayName, ); if (!mounted) return; await _loadProfile(); } catch (error) { if (!mounted) return; setState(() { _profileError = 'Could not create profile: ${error.toString().replaceAll('Exception: ', '')}'; }); } finally { if (mounted) { setState(() { _isCreatingProfile = false; }); } } } Future _loadActiveFeed({bool refresh = false}) async { switch (_activeFeed) { case ProfileFeedType.posts: return _loadPosts(refresh: refresh); case ProfileFeedType.saved: return _loadSaved(refresh: refresh); case ProfileFeedType.chained: return _loadChained(refresh: refresh); } } Future _loadPosts({bool refresh = false}) async { if (_profile == null) return; if (refresh) { setState(() { _posts = []; _hasMorePosts = true; _postsError = null; }); } else if (!_hasMorePosts || _isPostsLoadingMore) { return; } setState(() { if (refresh) { _isPostsLoading = true; } else { _isPostsLoadingMore = true; } if (!refresh) { _postsError = null; } }); try { final apiService = ref.read(apiServiceProvider); final posts = await apiService.getProfilePosts( authorId: _profile!.id, limit: _postsPageSize, offset: refresh ? 0 : _posts.length, ); if (!mounted) return; setState(() { if (refresh) { _posts = posts; } else { _posts.addAll(posts); } _hasMorePosts = posts.length == _postsPageSize; }); } catch (error) { if (!mounted) return; setState(() { _postsError = error.toString().replaceAll('Exception: ', ''); }); } finally { if (mounted) { setState(() { _isPostsLoading = false; _isPostsLoadingMore = false; }); } } } Future _loadSaved({bool refresh = false}) async { if (_profile == null) return; if (refresh) { setState(() { _savedPosts = []; _hasMoreSaved = true; _savedError = null; }); } else if (!_hasMoreSaved || _isSavedLoadingMore) { return; } setState(() { if (refresh) { _isSavedLoading = true; } else { _isSavedLoadingMore = true; } if (!refresh) { _savedError = null; } }); try { final apiService = ref.read(apiServiceProvider); final posts = await apiService.getSavedPosts( userId: _profile!.id, limit: _postsPageSize, offset: refresh ? 0 : _savedPosts.length, ); if (!mounted) return; setState(() { if (refresh) { _savedPosts = posts; } else { _savedPosts.addAll(posts); } _hasMoreSaved = posts.length == _postsPageSize; }); } catch (error) { if (!mounted) return; setState(() { _savedError = error.toString().replaceAll('Exception: ', ''); }); } finally { if (mounted) { setState(() { _isSavedLoading = false; _isSavedLoadingMore = false; }); } } } Future _loadChained({bool refresh = false}) async { if (_profile == null) return; if (refresh) { setState(() { _chainedPosts = []; _hasMoreChained = true; _chainedError = null; }); } else if (!_hasMoreChained || _isChainedLoadingMore) { return; } setState(() { if (refresh) { _isChainedLoading = true; } else { _isChainedLoadingMore = true; } if (!refresh) { _chainedError = null; } }); try { final apiService = ref.read(apiServiceProvider); final posts = await apiService.getChainedPostsForAuthor( authorId: _profile!.id, limit: _postsPageSize, offset: refresh ? 0 : _chainedPosts.length, ); if (!mounted) return; setState(() { if (refresh) { _chainedPosts = posts; } else { _chainedPosts.addAll(posts); } _hasMoreChained = posts.length == _postsPageSize; }); } catch (error) { if (!mounted) return; setState(() { _chainedError = error.toString().replaceAll('Exception: ', ''); }); } finally { if (mounted) { setState(() { _isChainedLoading = false; _isChainedLoadingMore = false; }); } } } Future _openSettings() async { final profile = _profile; if (profile == null) return; final settings = _privacySettings ?? ProfilePrivacySettings.defaults(profile.id); final result = await Navigator.of(context, rootNavigator: true) .push( MaterialPageRoute( builder: (_) => ProfileSettingsScreen( profile: profile, settings: settings, ), ), ); if (result != null && mounted) { setState(() { _profile = result.profile; _privacySettings = result.settings; }); } } Future _openPrivacyMenu() async { final profile = _profile; if (profile == null) return; final apiService = ref.read(apiServiceProvider); final currentSettings = _privacySettings ?? ProfilePrivacySettings.defaults(profile.id); ProfilePrivacySettings draft = currentSettings; final result = await showModalBottomSheet( context: context, useSafeArea: true, isScrollControlled: true, builder: (context) { bool isSaving = false; bool limitOldPosts = false; return StatefulBuilder( builder: (context, setModalState) { Future handleSave() async { if (isSaving) return; setModalState(() => isSaving = true); try { final saved = await apiService.updatePrivacySettings(draft); if (limitOldPosts) { await apiService .updateAllPostVisibility(draft.postsVisibility); } if (!context.mounted) return; Navigator.of(context).pop(saved); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Privacy settings updated.'), ), ); } } catch (error) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( error.toString().replaceAll('Exception: ', ''), ), backgroundColor: AppTheme.error, ), ); } finally { if (context.mounted) { setModalState(() => isSaving = false); } } } return Padding( padding: EdgeInsets.only( left: AppTheme.spacingLg, right: AppTheme.spacingLg, top: AppTheme.spacingLg, bottom: MediaQuery.of(context).viewInsets.bottom + AppTheme.spacingLg, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Privacy', style: AppTheme.headlineSmall), const SizedBox(height: AppTheme.spacingSm), Text( 'Control who can see your profile and posts.', style: AppTheme.bodyMedium.copyWith( color: AppTheme.navyText.withOpacity(0.7), ), ), const SizedBox(height: AppTheme.spacingLg), _PrivacyDropdown( label: 'Profile visibility', value: draft.profileVisibility, onChanged: (value) { setModalState(() { draft = draft.copyWith(profileVisibility: value); }); }, ), const SizedBox(height: AppTheme.spacingMd), _PrivacyDropdown( label: 'Posts visibility', value: draft.postsVisibility, onChanged: (value) { setModalState(() { draft = draft.copyWith(postsVisibility: value); }); }, ), const SizedBox(height: AppTheme.spacingMd), CheckboxListTile( contentPadding: EdgeInsets.zero, value: limitOldPosts, title: const Text('Limit old posts'), subtitle: const Text( 'Apply this posts privacy setting to all existing posts.', ), onChanged: (value) { setModalState(() => limitOldPosts = value ?? false); }, ), const SizedBox(height: AppTheme.spacingLg), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: isSaving ? null : handleSave, child: isSaving ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( AppTheme.white, ), ), ) : const Text('Save'), ), ), const SizedBox(height: AppTheme.spacingSm), ], ), ); }, ); }, ); if (result != null && mounted) { setState(() { _privacySettings = result; }); } } void _showAvatarActions() { final profile = _profile; if (profile == null) return; showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (context) { return Container( padding: const EdgeInsets.all(AppTheme.spacingLg), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.visibility), title: const Text('View profile photo'), onTap: () { Navigator.of(context).pop(); _showAvatarPreview(profile); }, ), ListTile( leading: const Icon(Icons.photo_camera), title: const Text('Change profile photo'), onTap: () { Navigator.of(context).pop(); _openSettings(); }, ), ], ), ); }, ); } void _showAvatarPreview(Profile profile) { showDialog( context: context, builder: (context) { final avatarUrl = _resolveAvatar(profile.avatarUrl); return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.all(AppTheme.spacingLg), child: Container( padding: const EdgeInsets.all(AppTheme.spacingLg), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(20), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Profile Photo', style: AppTheme.headlineSmall, ), const SizedBox(height: AppTheme.spacingLg), CircleAvatar( radius: 72, backgroundColor: AppTheme.queenPink, child: avatarUrl != null && avatarUrl.isNotEmpty ? ClipOval( child: SizedBox( width: 144, height: 144, child: SignedMediaImage( url: avatarUrl, width: 144, height: 144, fit: BoxFit.cover, ), ), ) : Text( profile.displayName.isNotEmpty ? profile.displayName[0].toUpperCase() : '?', style: AppTheme.headlineMedium.copyWith( color: AppTheme.royalPurple, ), ), ), const SizedBox(height: AppTheme.spacingLg), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Close'), ), ], ), ), ); }, ); } void _openPostDetail(Post post) { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute( builder: (_) => PostDetailScreen(post: post), ), ); } void _openChainComposer(Post post) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ComposeScreen(chainParentPost: post), fullscreenDialog: true, ), ); } List _getPostsFor(ProfileFeedType type) { switch (type) { case ProfileFeedType.posts: return _posts; case ProfileFeedType.saved: return _savedPosts; case ProfileFeedType.chained: return _chainedPosts; } } bool _isLoadingFor(ProfileFeedType type) { switch (type) { case ProfileFeedType.posts: return _isPostsLoading; case ProfileFeedType.saved: return _isSavedLoading; case ProfileFeedType.chained: return _isChainedLoading; } } bool _isLoadingMoreFor(ProfileFeedType type) { switch (type) { case ProfileFeedType.posts: return _isPostsLoadingMore; case ProfileFeedType.saved: return _isSavedLoadingMore; case ProfileFeedType.chained: return _isChainedLoadingMore; } } bool _hasMoreFor(ProfileFeedType type) { switch (type) { case ProfileFeedType.posts: return _hasMorePosts; case ProfileFeedType.saved: return _hasMoreSaved; case ProfileFeedType.chained: return _hasMoreChained; } } String? _errorFor(ProfileFeedType type) { switch (type) { case ProfileFeedType.posts: return _postsError; case ProfileFeedType.saved: return _savedError; case ProfileFeedType.chained: return _chainedError; } } @override Widget build(BuildContext context) { final profile = _profile; if (_profileError != null && profile == null && !_isProfileLoading) { return _buildErrorState(); } if (_isProfileLoading && profile == null) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } if (profile == null) { return const Scaffold( body: Center(child: Text('No profile found')), ); } return Scaffold( backgroundColor: AppTheme.scaffoldBg, body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ _buildSliverAppBar(profile), _buildSliverTabBar(), ]; }, body: _buildTabBarView(), ), ); } Widget _buildErrorState() { return Scaffold( body: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppTheme.spacingLg), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _profileError ?? 'Something went wrong', style: AppTheme.bodyMedium.copyWith( color: AppTheme.error, ), textAlign: TextAlign.center, ), const SizedBox(height: AppTheme.spacingMd), ElevatedButton( onPressed: _loadProfile, child: const Text('Retry'), ), ], ), ), ), ); } Widget _buildSliverAppBar(Profile profile) { return SliverAppBar( expandedHeight: 255, pinned: true, toolbarHeight: 0, collapsedHeight: 0, automaticallyImplyLeading: false, backgroundColor: Colors.transparent, elevation: 0, flexibleSpace: FlexibleSpaceBar( background: _ProfileHeader( profile: profile, stats: _stats, onSettingsTap: _openSettings, onPrivacyTap: _openPrivacyMenu, onAvatarTap: _showAvatarActions, ), ), ); } Widget _buildSliverTabBar() { return SliverPersistentHeader( pinned: true, delegate: _SliverTabBarDelegate( TabBar( controller: _tabController, labelColor: AppTheme.navyText, unselectedLabelColor: AppTheme.navyText.withOpacity(0.6), indicatorColor: AppTheme.royalPurple, indicatorWeight: 3, labelStyle: AppTheme.labelMedium, tabs: const [ Tab(text: 'Posts'), Tab(text: 'Saved'), Tab(text: 'Chains'), ], ), ), ); } Widget _buildTabBarView() { final activePosts = _getPostsFor(_activeFeed); final isLoading = _isLoadingFor(_activeFeed); final isLoadingMore = _isLoadingMoreFor(_activeFeed); final hasMore = _hasMoreFor(_activeFeed); final error = _errorFor(_activeFeed); return RefreshIndicator( onRefresh: _refreshAll, child: CustomScrollView( slivers: [ if (error != null) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(AppTheme.spacingLg), child: Text( error, style: AppTheme.bodyMedium.copyWith(color: AppTheme.error), textAlign: TextAlign.center, ), ), ), if (isLoading && activePosts.isEmpty) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric(vertical: AppTheme.spacingLg), child: Center(child: CircularProgressIndicator()), ), ), if (activePosts.isEmpty && !isLoading) SliverFillRemaining( child: Center( child: Text( 'No posts yet', style: AppTheme.bodyMedium.copyWith( color: AppTheme.navyText.withOpacity(0.7), ), ), ), ), if (activePosts.isNotEmpty) SliverPadding( padding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingMd, vertical: AppTheme.spacingMd, ), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final post = activePosts[index]; return Padding( padding: EdgeInsets.only( bottom: index == activePosts.length - 1 ? 0 : AppTheme.spacingSm, ), child: PostItem( post: post, onTap: () => _openPostDetail(post), onChain: () => _openChainComposer(post), ), ); }, childCount: activePosts.length, ), ), ), if (isLoadingMore) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.symmetric(vertical: AppTheme.spacingLg), child: Center(child: CircularProgressIndicator()), ), ), if (!isLoadingMore && hasMore && activePosts.isNotEmpty) SliverToBoxAdapter( child: Center( child: TextButton( onPressed: () => _loadActiveFeed(refresh: false), child: const Text('Load more'), ), ), ), SliverToBoxAdapter( child: SizedBox(height: AppTheme.spacingLg * 2), ), ], ), ); } } // ============================================================================== // PROFILE HEADER WITH HARMONY RING // ============================================================================== class _ProfileHeader extends StatelessWidget { final Profile profile; final ProfileStats? stats; final VoidCallback onSettingsTap; final VoidCallback onPrivacyTap; final VoidCallback onAvatarTap; const _ProfileHeader({ required this.profile, required this.stats, required this.onSettingsTap, required this.onPrivacyTap, required this.onAvatarTap, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( gradient: _generateGradient(profile.handle), ), child: SafeArea( bottom: false, child: LayoutBuilder( builder: (context, constraints) { final isCompact = constraints.maxHeight < 240; final avatarRadius = isCompact ? 36.0 : 44.0; return Padding( padding: EdgeInsets.only( top: 0, bottom: isCompact ? 2 : 6, ), child: Column( mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ Align( alignment: Alignment.topRight, child: Padding( padding: EdgeInsets.only(top: 0), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: onPrivacyTap, icon: Icon(Icons.lock_outline, color: AppTheme.white), tooltip: 'Privacy', ), const SizedBox(width: 4), IconButton( onPressed: onSettingsTap, icon: Icon(Icons.settings_outlined, color: AppTheme.white), tooltip: 'Settings', ), ], ), ), ), InkResponse( onTap: onAvatarTap, radius: 40, child: _HarmonyAvatar( profile: profile, radius: avatarRadius, ), ), SizedBox(height: isCompact ? 4 : 6), Text( profile.displayName, style: AppTheme.headlineMedium.copyWith( color: AppTheme.white.withOpacity(0.95), fontSize: isCompact ? 14 : 16, shadows: [ Shadow( color: Colors.black.withOpacity(0.2), blurRadius: 6, ), ], ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '@${profile.handle}', style: AppTheme.bodyMedium.copyWith( fontSize: 11, color: AppTheme.white.withOpacity(0.85), shadows: [ Shadow( color: Colors.black.withOpacity(0.2), blurRadius: 4, ), ], ), ), if (getCountryFlag(profile.originCountry ?? 'US') != null) ...[ const SizedBox(width: 4), Text( getCountryFlag(profile.originCountry ?? 'US')!, style: const TextStyle(fontSize: 12), ), ], ], ), if (stats != null && !isCompact) ...[ const SizedBox(height: 5), _buildStats(stats!), ], ], ), ); }, ), ), ); } Widget _buildStats(ProfileStats stats) { return FittedBox( fit: BoxFit.scaleDown, child: Container( padding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingMd, vertical: AppTheme.spacingXs, ), decoration: BoxDecoration( color: Colors.black.withOpacity(0.35), borderRadius: BorderRadius.circular(AppTheme.radiusMd), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _StatItem(label: 'Posts', value: stats.posts.toString()), const SizedBox(width: AppTheme.spacingMd), _StatItem(label: 'Followers', value: stats.followers.toString()), const SizedBox(width: AppTheme.spacingMd), _StatItem(label: 'Following', value: stats.following.toString()), ], ), ), ); } LinearGradient _generateGradient(String seed) { final hash = seed.hashCode.abs(); final hue = (hash % 360).toDouble(); return LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ HSLColor.fromAHSL(1.0, hue, 0.6, 0.55).toColor(), HSLColor.fromAHSL(1.0, (hue + 60) % 360, 0.6, 0.45).toColor(), ], ); } } // ============================================================================== // STAT ITEM // ============================================================================== class _StatItem extends StatelessWidget { final String label; final String value; const _StatItem({ required this.label, required this.value, }); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Text( value, style: AppTheme.headlineSmall.copyWith( color: AppTheme.white, fontSize: 16, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), blurRadius: 4, ), ], ), ), Text( label, style: AppTheme.labelSmall.copyWith( color: AppTheme.white.withOpacity(0.8), fontSize: 10, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), blurRadius: 2, ), ], ), ), ], ); } } // ============================================================================== // HARMONY AVATAR WITH RING // ============================================================================== class _HarmonyAvatar extends StatelessWidget { final Profile profile; final double radius; const _HarmonyAvatar({ required this.profile, required this.radius, }); @override Widget build(BuildContext context) { final trustState = profile.trustState; final avatarLetter = profile.handle.isNotEmpty ? profile.handle[0].toUpperCase() : '?'; Color ringColor = AppTheme.egyptianBlue; double ringWidth = 3; if (trustState != null) { final harmonyScore = trustState.harmonyScore / 100.0; if (harmonyScore >= 0.8) { ringColor = const Color(0xFFFFD700); // Gold ringWidth = 5; } else if (harmonyScore >= 0.5) { ringColor = AppTheme.royalPurple; ringWidth = 4; } else if (harmonyScore >= 0.3) { ringColor = AppTheme.egyptianBlue; ringWidth = 3; } else { ringColor = Colors.grey; ringWidth = 2; } } return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(radius * 0.45), border: Border.all( color: ringWidth >= 4 ? ringColor : ringColor.withOpacity(0.8), width: ringWidth, ), boxShadow: [ if (ringWidth >= 4) BoxShadow( color: ringColor.withOpacity(0.5), blurRadius: 12, spreadRadius: 2, ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(radius * 0.4), child: SizedBox( width: radius * 2, height: radius * 2, child: _resolveAvatar(profile.avatarUrl) != null && _resolveAvatar(profile.avatarUrl)!.isNotEmpty ? SignedMediaImage( url: _resolveAvatar(profile.avatarUrl)!, fit: BoxFit.cover, ) : Container( color: AppTheme.queenPink, alignment: Alignment.center, child: Text( avatarLetter, style: AppTheme.headlineMedium.copyWith( fontSize: radius * 0.6, color: AppTheme.royalPurple, ), ), ), ), ), ); } } // ============================================================================== // HARMONY BADGE // ============================================================================== class _HarmonyBadge extends StatelessWidget { final TrustState trustState; const _HarmonyBadge({ required this.trustState, }); @override Widget build(BuildContext context) { final tier = trustState.tier; Color badgeColor; Color textColor; switch (tier) { case TrustTier.established: badgeColor = const Color(0xFFFFD700); // Gold break; case TrustTier.trusted: badgeColor = AppTheme.royalPurple; break; case TrustTier.new_user: badgeColor = AppTheme.egyptianBlue; break; } textColor = tier == TrustTier.new_user ? AppTheme.white : badgeColor; return Container( padding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingMd, vertical: AppTheme.spacingXs, ), decoration: BoxDecoration( color: badgeColor.withOpacity(0.2), border: Border.all(color: badgeColor, width: 1.5), borderRadius: BorderRadius.circular(AppTheme.radiusMd), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _getIconForTier(tier), size: 14, color: badgeColor, ), const SizedBox(width: AppTheme.spacingXs), Text( tier.displayName, style: AppTheme.labelSmall.copyWith( color: textColor, fontWeight: FontWeight.w700, ), ), ], ), ); } IconData _getIconForTier(TrustTier tier) { switch (tier) { case TrustTier.established: return Icons.verified; case TrustTier.trusted: return Icons.check_circle; case TrustTier.new_user: return Icons.fiber_new; } } } // ============================================================================== // HEADER ACTION BUTTON // ============================================================================== class _HeaderActionButton extends StatelessWidget { final IconData icon; final String label; final VoidCallback onPressed; const _HeaderActionButton({ required this.icon, required this.label, required this.onPressed, }); @override Widget build(BuildContext context) { return OutlinedButton.icon( onPressed: onPressed, style: OutlinedButton.styleFrom( foregroundColor: AppTheme.white, side: BorderSide(color: AppTheme.white.withOpacity(0.7)), padding: const EdgeInsets.symmetric( horizontal: AppTheme.spacingMd, vertical: AppTheme.spacingSm, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.radiusMd), ), ), icon: Icon(icon, size: 18), label: Text( label, style: AppTheme.labelMedium.copyWith(color: AppTheme.white), ), ); } } // ============================================================================== // PRIVACY DROPDOWN // ============================================================================== class _PrivacyDropdown extends StatelessWidget { final String label; final String value; final ValueChanged onChanged; const _PrivacyDropdown({ required this.label, required this.value, required this.onChanged, }); static const Map _options = { 'public': 'Public', 'followers': 'Followers only', 'private': 'Private', }; @override Widget build(BuildContext context) { return DropdownButtonFormField( value: value, decoration: InputDecoration(labelText: label), isExpanded: true, items: _options.entries .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) .toList(), onChanged: (v) => v == null ? null : onChanged(v), ); } } // ============================================================================== // SLIVER TAB BAR DELEGATE // ============================================================================== class _SliverTabBarDelegate extends SliverPersistentHeaderDelegate { final TabBar tabBar; _SliverTabBarDelegate(this.tabBar); @override double get minExtent => tabBar.preferredSize.height; @override double get maxExtent => tabBar.preferredSize.height; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent, ) { return Container( color: AppTheme.cardSurface, child: tabBar, ); } @override bool shouldRebuild(_SliverTabBarDelegate oldDelegate) { return false; } }