Add followers/following screen and comprehensive privacy settings
Features: - Create FollowersFollowingScreen with tabs for followers/following lists - Make follower/following counts tappable on profile to navigate to connections - Create comprehensive PrivacySettingsScreen with sections for: - Account privacy (private account toggle) - Post visibility defaults - Interaction controls (who can message/comment) - Discovery settings (search visibility) - Circle (close friends) management placeholder - Data export and blocked users - Update ProfilePrivacySettings model with additional fields - Connect to new backend API endpoints
This commit is contained in:
parent
9fec6754d9
commit
adeffe691e
|
|
@ -1,31 +1,51 @@
|
||||||
class ProfilePrivacySettings {
|
class ProfilePrivacySettings {
|
||||||
final String userId;
|
String userId;
|
||||||
final String profileVisibility;
|
String profileVisibility;
|
||||||
final String postsVisibility;
|
String postsVisibility;
|
||||||
final String savedVisibility;
|
String savedVisibility;
|
||||||
final String followRequestPolicy;
|
String followRequestPolicy;
|
||||||
final String defaultPostVisibility;
|
String defaultVisibility;
|
||||||
final bool isPrivateProfile;
|
bool isPrivate;
|
||||||
|
bool allowChains;
|
||||||
|
String whoCanMessage;
|
||||||
|
String whoCanComment;
|
||||||
|
bool showActivityStatus;
|
||||||
|
bool showInSearch;
|
||||||
|
bool showInSuggestions;
|
||||||
|
|
||||||
const ProfilePrivacySettings({
|
ProfilePrivacySettings({
|
||||||
required this.userId,
|
this.userId = '',
|
||||||
required this.profileVisibility,
|
this.profileVisibility = 'public',
|
||||||
required this.postsVisibility,
|
this.postsVisibility = 'public',
|
||||||
required this.savedVisibility,
|
this.savedVisibility = 'private',
|
||||||
required this.followRequestPolicy,
|
this.followRequestPolicy = 'everyone',
|
||||||
required this.defaultPostVisibility,
|
this.defaultVisibility = 'public',
|
||||||
required this.isPrivateProfile,
|
this.isPrivate = false,
|
||||||
|
this.allowChains = true,
|
||||||
|
this.whoCanMessage = 'everyone',
|
||||||
|
this.whoCanComment = 'everyone',
|
||||||
|
this.showActivityStatus = true,
|
||||||
|
this.showInSearch = true,
|
||||||
|
this.showInSuggestions = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProfilePrivacySettings.fromJson(Map<String, dynamic> json) {
|
factory ProfilePrivacySettings.fromJson(Map<String, dynamic> json) {
|
||||||
return ProfilePrivacySettings(
|
return ProfilePrivacySettings(
|
||||||
userId: json['user_id'] as String,
|
userId: json['user_id'] as String? ?? '',
|
||||||
profileVisibility: json['profile_visibility'] as String? ?? 'public',
|
profileVisibility: json['profile_visibility'] as String? ?? 'public',
|
||||||
postsVisibility: json['posts_visibility'] as String? ?? 'public',
|
postsVisibility: json['posts_visibility'] as String? ?? 'public',
|
||||||
savedVisibility: json['saved_visibility'] as String? ?? 'private',
|
savedVisibility: json['saved_visibility'] as String? ?? 'private',
|
||||||
followRequestPolicy: json['follow_request_policy'] as String? ?? 'everyone',
|
followRequestPolicy: json['follow_request_policy'] as String? ?? 'everyone',
|
||||||
defaultPostVisibility: json['default_post_visibility'] as String? ?? 'public',
|
defaultVisibility: json['default_post_visibility'] as String? ??
|
||||||
isPrivateProfile: json['is_private_profile'] as bool? ?? false,
|
json['default_visibility'] as String? ?? 'public',
|
||||||
|
isPrivate: json['is_private_profile'] as bool? ??
|
||||||
|
json['is_private'] as bool? ?? false,
|
||||||
|
allowChains: json['allow_chains'] as bool? ?? true,
|
||||||
|
whoCanMessage: json['who_can_message'] as String? ?? 'everyone',
|
||||||
|
whoCanComment: json['who_can_comment'] as String? ?? 'everyone',
|
||||||
|
showActivityStatus: json['show_activity_status'] as bool? ?? true,
|
||||||
|
showInSearch: json['show_in_search'] as bool? ?? true,
|
||||||
|
showInSuggestions: json['show_in_suggestions'] as bool? ?? true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,8 +56,14 @@ class ProfilePrivacySettings {
|
||||||
'posts_visibility': postsVisibility,
|
'posts_visibility': postsVisibility,
|
||||||
'saved_visibility': savedVisibility,
|
'saved_visibility': savedVisibility,
|
||||||
'follow_request_policy': followRequestPolicy,
|
'follow_request_policy': followRequestPolicy,
|
||||||
'default_post_visibility': defaultPostVisibility,
|
'default_post_visibility': defaultVisibility,
|
||||||
'is_private_profile': isPrivateProfile,
|
'is_private_profile': isPrivate,
|
||||||
|
'allow_chains': allowChains,
|
||||||
|
'who_can_message': whoCanMessage,
|
||||||
|
'who_can_comment': whoCanComment,
|
||||||
|
'show_activity_status': showActivityStatus,
|
||||||
|
'show_in_search': showInSearch,
|
||||||
|
'show_in_suggestions': showInSuggestions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,8 +72,14 @@ class ProfilePrivacySettings {
|
||||||
String? postsVisibility,
|
String? postsVisibility,
|
||||||
String? savedVisibility,
|
String? savedVisibility,
|
||||||
String? followRequestPolicy,
|
String? followRequestPolicy,
|
||||||
String? defaultPostVisibility,
|
String? defaultVisibility,
|
||||||
bool? isPrivateProfile,
|
bool? isPrivate,
|
||||||
|
bool? allowChains,
|
||||||
|
String? whoCanMessage,
|
||||||
|
String? whoCanComment,
|
||||||
|
bool? showActivityStatus,
|
||||||
|
bool? showInSearch,
|
||||||
|
bool? showInSuggestions,
|
||||||
}) {
|
}) {
|
||||||
return ProfilePrivacySettings(
|
return ProfilePrivacySettings(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
|
@ -55,8 +87,14 @@ class ProfilePrivacySettings {
|
||||||
postsVisibility: postsVisibility ?? this.postsVisibility,
|
postsVisibility: postsVisibility ?? this.postsVisibility,
|
||||||
savedVisibility: savedVisibility ?? this.savedVisibility,
|
savedVisibility: savedVisibility ?? this.savedVisibility,
|
||||||
followRequestPolicy: followRequestPolicy ?? this.followRequestPolicy,
|
followRequestPolicy: followRequestPolicy ?? this.followRequestPolicy,
|
||||||
defaultPostVisibility: defaultPostVisibility ?? this.defaultPostVisibility,
|
defaultVisibility: defaultVisibility ?? this.defaultVisibility,
|
||||||
isPrivateProfile: isPrivateProfile ?? this.isPrivateProfile,
|
isPrivate: isPrivate ?? this.isPrivate,
|
||||||
|
allowChains: allowChains ?? this.allowChains,
|
||||||
|
whoCanMessage: whoCanMessage ?? this.whoCanMessage,
|
||||||
|
whoCanComment: whoCanComment ?? this.whoCanComment,
|
||||||
|
showActivityStatus: showActivityStatus ?? this.showActivityStatus,
|
||||||
|
showInSearch: showInSearch ?? this.showInSearch,
|
||||||
|
showInSuggestions: showInSuggestions ?? this.showInSuggestions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,8 +105,18 @@ class ProfilePrivacySettings {
|
||||||
postsVisibility: 'public',
|
postsVisibility: 'public',
|
||||||
savedVisibility: 'private',
|
savedVisibility: 'private',
|
||||||
followRequestPolicy: 'everyone',
|
followRequestPolicy: 'everyone',
|
||||||
defaultPostVisibility: 'public',
|
defaultVisibility: 'public',
|
||||||
isPrivateProfile: false,
|
isPrivate: false,
|
||||||
|
allowChains: true,
|
||||||
|
whoCanMessage: 'everyone',
|
||||||
|
whoCanComment: 'everyone',
|
||||||
|
showActivityStatus: true,
|
||||||
|
showInSearch: true,
|
||||||
|
showInSuggestions: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy getters for backwards compatibility
|
||||||
|
String get defaultPostVisibility => defaultVisibility;
|
||||||
|
bool get isPrivateProfile => isPrivate;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
417
sojorn_app/lib/screens/profile/followers_following_screen.dart
Normal file
417
sojorn_app/lib/screens/profile/followers_following_screen.dart
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../theme/app_theme.dart';
|
||||||
|
import '../../widgets/app_scaffold.dart';
|
||||||
|
import '../../widgets/media/signed_media_image.dart';
|
||||||
|
import '../../services/api_service.dart';
|
||||||
|
import 'viewable_profile_screen.dart';
|
||||||
|
|
||||||
|
/// Screen to manage followers and following with tabbed interface
|
||||||
|
class FollowersFollowingScreen extends ConsumerStatefulWidget {
|
||||||
|
final String userId;
|
||||||
|
final int initialTabIndex; // 0 = Followers, 1 = Following
|
||||||
|
|
||||||
|
const FollowersFollowingScreen({
|
||||||
|
super.key,
|
||||||
|
required this.userId,
|
||||||
|
this.initialTabIndex = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<FollowersFollowingScreen> createState() =>
|
||||||
|
_FollowersFollowingScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FollowersFollowingScreenState
|
||||||
|
extends ConsumerState<FollowersFollowingScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
|
||||||
|
List<UserListItem> _followers = [];
|
||||||
|
List<UserListItem> _following = [];
|
||||||
|
bool _isLoadingFollowers = false;
|
||||||
|
bool _isLoadingFollowing = false;
|
||||||
|
String? _followersError;
|
||||||
|
String? _followingError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(
|
||||||
|
length: 2,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: widget.initialTabIndex,
|
||||||
|
);
|
||||||
|
_loadFollowers();
|
||||||
|
_loadFollowing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setStateIfMounted(VoidCallback fn) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFollowers() async {
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_isLoadingFollowers = true;
|
||||||
|
_followersError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
final data = await api.callGoApi(
|
||||||
|
'/users/${widget.userId}/followers',
|
||||||
|
queryParams: {'limit': '50', 'offset': '0'},
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<UserListItem> users = ((data['followers'] ?? []) as List)
|
||||||
|
.map((json) => UserListItem.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_followers = users;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_followersError = e.toString();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_isLoadingFollowers = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFollowing() async {
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_isLoadingFollowing = true;
|
||||||
|
_followingError = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
final data = await api.callGoApi(
|
||||||
|
'/users/${widget.userId}/following',
|
||||||
|
queryParams: {'limit': '50', 'offset': '0'},
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<UserListItem> users = ((data['following'] ?? []) as List)
|
||||||
|
.map((json) => UserListItem.fromJson(json))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_following = users;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_followingError = e.toString();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_isLoadingFollowing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _unfollowUser(String userId) async {
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
await api.callGoApi('/users/$userId/follow', method: 'DELETE');
|
||||||
|
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_following.removeWhere((u) => u.id == userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Unfollowed successfully')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to unfollow: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeFollower(String userId) async {
|
||||||
|
// This would require a backend endpoint to remove a follower
|
||||||
|
// For now, show a placeholder message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Remove follower feature coming soon')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppScaffold(
|
||||||
|
title: 'Connections',
|
||||||
|
showBackButton: true,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// Tab Bar
|
||||||
|
Container(
|
||||||
|
color: AppTheme.surfaceColor,
|
||||||
|
child: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
indicatorColor: AppTheme.primaryColor,
|
||||||
|
labelColor: AppTheme.textPrimary,
|
||||||
|
unselectedLabelColor: AppTheme.textSecondary,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: 'Followers (${_followers.length})'),
|
||||||
|
Tab(text: 'Following (${_following.length})'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tab Views
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children: [
|
||||||
|
_buildUserList(
|
||||||
|
users: _followers,
|
||||||
|
isLoading: _isLoadingFollowers,
|
||||||
|
error: _followersError,
|
||||||
|
onRefresh: _loadFollowers,
|
||||||
|
isFollowersList: true,
|
||||||
|
),
|
||||||
|
_buildUserList(
|
||||||
|
users: _following,
|
||||||
|
isLoading: _isLoadingFollowing,
|
||||||
|
error: _followingError,
|
||||||
|
onRefresh: _loadFollowing,
|
||||||
|
isFollowersList: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserList({
|
||||||
|
required List<UserListItem> users,
|
||||||
|
required bool isLoading,
|
||||||
|
required String? error,
|
||||||
|
required Future<void> Function() onRefresh,
|
||||||
|
required bool isFollowersList,
|
||||||
|
}) {
|
||||||
|
if (isLoading && users.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error != null && users.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, size: 48, color: AppTheme.textSecondary),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Failed to load', style: AppTheme.bodyLarge),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: onRefresh,
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isFollowersList ? Icons.people_outline : Icons.person_add_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: AppTheme.textSecondary.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
isFollowersList ? 'No followers yet' : 'Not following anyone yet',
|
||||||
|
style: AppTheme.bodyLarge.copyWith(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: onRefresh,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: users.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = users[index];
|
||||||
|
return _UserListTile(
|
||||||
|
user: user,
|
||||||
|
isFollowersList: isFollowersList,
|
||||||
|
onTap: () => _navigateToProfile(user),
|
||||||
|
onAction: isFollowersList
|
||||||
|
? () => _removeFollower(user.id)
|
||||||
|
: () => _unfollowUser(user.id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToProfile(UserListItem user) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ViewableProfileScreen(handle: user.handle),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// USER LIST ITEM MODEL
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class UserListItem {
|
||||||
|
final String id;
|
||||||
|
final String handle;
|
||||||
|
final String? displayName;
|
||||||
|
final String? avatarUrl;
|
||||||
|
final int harmonyScore;
|
||||||
|
final String? harmonyTier;
|
||||||
|
final DateTime? followedAt;
|
||||||
|
|
||||||
|
UserListItem({
|
||||||
|
required this.id,
|
||||||
|
required this.handle,
|
||||||
|
this.displayName,
|
||||||
|
this.avatarUrl,
|
||||||
|
this.harmonyScore = 0,
|
||||||
|
this.harmonyTier,
|
||||||
|
this.followedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UserListItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return UserListItem(
|
||||||
|
id: json['id'] ?? '',
|
||||||
|
handle: json['handle'] ?? '',
|
||||||
|
displayName: json['display_name'],
|
||||||
|
avatarUrl: json['avatar_url'],
|
||||||
|
harmonyScore: json['harmony_score'] ?? 0,
|
||||||
|
harmonyTier: json['harmony_tier'],
|
||||||
|
followedAt: json['followed_at'] != null
|
||||||
|
? DateTime.tryParse(json['followed_at'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// USER LIST TILE WIDGET
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class _UserListTile extends StatelessWidget {
|
||||||
|
final UserListItem user;
|
||||||
|
final bool isFollowersList;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final VoidCallback onAction;
|
||||||
|
|
||||||
|
const _UserListTile({
|
||||||
|
required this.user,
|
||||||
|
required this.isFollowersList,
|
||||||
|
required this.onTap,
|
||||||
|
required this.onAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
Color _getTierColor(String? tier) {
|
||||||
|
switch (tier?.toLowerCase()) {
|
||||||
|
case 'gold':
|
||||||
|
return const Color(0xFFFFD700);
|
||||||
|
case 'silver':
|
||||||
|
return const Color(0xFFC0C0C0);
|
||||||
|
case 'bronze':
|
||||||
|
return const Color(0xFFCD7F32);
|
||||||
|
default:
|
||||||
|
return AppTheme.textSecondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: onTap,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
radius: 24,
|
||||||
|
backgroundColor: AppTheme.surfaceColor,
|
||||||
|
child: user.avatarUrl != null
|
||||||
|
? ClipOval(
|
||||||
|
child: SignedMediaImage(
|
||||||
|
imageUrl: user.avatarUrl!,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
(user.displayName ?? user.handle).substring(0, 1).toUpperCase(),
|
||||||
|
style: AppTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
user.displayName ?? user.handle,
|
||||||
|
style: AppTheme.bodyLarge.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (user.harmonyTier != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getTierColor(user.harmonyTier).withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
user.harmonyTier!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _getTierColor(user.harmonyTier),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'@${user.handle}',
|
||||||
|
style: AppTheme.bodySmall.copyWith(color: AppTheme.textSecondary),
|
||||||
|
),
|
||||||
|
trailing: isFollowersList
|
||||||
|
? null // Followers don't have action button for now
|
||||||
|
: TextButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: AppTheme.textSecondary,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
),
|
||||||
|
child: const Text('Unfollow'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import '../../widgets/media/signed_media_image.dart';
|
||||||
import '../compose/compose_screen.dart';
|
import '../compose/compose_screen.dart';
|
||||||
import '../post/post_detail_screen.dart';
|
import '../post/post_detail_screen.dart';
|
||||||
import 'profile_settings_screen.dart';
|
import 'profile_settings_screen.dart';
|
||||||
|
import 'followers_following_screen.dart';
|
||||||
|
|
||||||
enum ProfileFeedType { posts, saved, chained }
|
enum ProfileFeedType { posts, saved, chained }
|
||||||
|
|
||||||
|
|
@ -1086,15 +1087,35 @@ class _ProfileHeader extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
_StatItem(label: 'Posts', value: stats.posts.toString()),
|
_StatItem(label: 'Posts', value: stats.posts.toString()),
|
||||||
const SizedBox(width: AppTheme.spacingMd),
|
const SizedBox(width: AppTheme.spacingMd),
|
||||||
_StatItem(label: 'Followers', value: stats.followers.toString()),
|
_StatItem(
|
||||||
|
label: 'Followers',
|
||||||
|
value: stats.followers.toString(),
|
||||||
|
onTap: () => _navigateToConnections(0),
|
||||||
|
),
|
||||||
const SizedBox(width: AppTheme.spacingMd),
|
const SizedBox(width: AppTheme.spacingMd),
|
||||||
_StatItem(label: 'Following', value: stats.following.toString()),
|
_StatItem(
|
||||||
|
label: 'Following',
|
||||||
|
value: stats.following.toString(),
|
||||||
|
onTap: () => _navigateToConnections(1),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _navigateToConnections(int tabIndex) {
|
||||||
|
if (_profile == null) return;
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => FollowersFollowingScreen(
|
||||||
|
userId: _profile!.id,
|
||||||
|
initialTabIndex: tabIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
LinearGradient _generateGradient(String seed) {
|
LinearGradient _generateGradient(String seed) {
|
||||||
final hash = seed.hashCode.abs();
|
final hash = seed.hashCode.abs();
|
||||||
final hue = (hash % 360).toDouble();
|
final hue = (hash % 360).toDouble();
|
||||||
|
|
@ -1117,15 +1138,17 @@ class _ProfileHeader extends StatelessWidget {
|
||||||
class _StatItem extends StatelessWidget {
|
class _StatItem extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const _StatItem({
|
const _StatItem({
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.value,
|
required this.value,
|
||||||
|
this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
final content = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -1156,6 +1179,14 @@ class _StatItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (onTap != null) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
560
sojorn_app/lib/screens/settings/privacy_settings_screen.dart
Normal file
560
sojorn_app/lib/screens/settings/privacy_settings_screen.dart
Normal file
|
|
@ -0,0 +1,560 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../theme/app_theme.dart';
|
||||||
|
import '../../widgets/app_scaffold.dart';
|
||||||
|
import '../../services/api_service.dart';
|
||||||
|
import '../../models/profile_privacy_settings.dart';
|
||||||
|
|
||||||
|
/// Comprehensive privacy settings screen for managing account privacy
|
||||||
|
class PrivacySettingsScreen extends ConsumerStatefulWidget {
|
||||||
|
final ProfilePrivacySettings? initialSettings;
|
||||||
|
|
||||||
|
const PrivacySettingsScreen({
|
||||||
|
super.key,
|
||||||
|
this.initialSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PrivacySettingsScreen> createState() =>
|
||||||
|
_PrivacySettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrivacySettingsScreenState
|
||||||
|
extends ConsumerState<PrivacySettingsScreen> {
|
||||||
|
late ProfilePrivacySettings _settings;
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _isSaving = false;
|
||||||
|
bool _hasChanges = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_settings = widget.initialSettings ?? ProfilePrivacySettings();
|
||||||
|
if (widget.initialSettings == null) {
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setStateIfMounted(VoidCallback fn) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSettings() async {
|
||||||
|
_setStateIfMounted(() => _isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
final data = await api.callGoApi('/settings/privacy');
|
||||||
|
|
||||||
|
_setStateIfMounted(() {
|
||||||
|
_settings = ProfilePrivacySettings.fromJson(data);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to load settings: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_setStateIfMounted(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSettings() async {
|
||||||
|
_setStateIfMounted(() => _isSaving = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
await api.callGoApi(
|
||||||
|
'/settings/privacy',
|
||||||
|
method: 'PATCH',
|
||||||
|
body: _settings.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_setStateIfMounted(() => _hasChanges = false);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Privacy settings saved'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop(_settings);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to save: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_setStateIfMounted(() => _isSaving = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateSetting(void Function() update) {
|
||||||
|
setState(() {
|
||||||
|
update();
|
||||||
|
_hasChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
if (_hasChanges) {
|
||||||
|
final shouldLeave = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Unsaved Changes'),
|
||||||
|
content: const Text('You have unsaved changes. Discard them?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Discard'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return shouldLeave ?? false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
child: AppScaffold(
|
||||||
|
title: 'Privacy Settings',
|
||||||
|
showBackButton: true,
|
||||||
|
actions: [
|
||||||
|
if (_hasChanges)
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isSaving ? null : _saveSettings,
|
||||||
|
child: _isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Text('Save', style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildSection(
|
||||||
|
title: 'Account Privacy',
|
||||||
|
icon: Icons.lock_outline,
|
||||||
|
children: [
|
||||||
|
_buildPrivateAccountTile(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSection(
|
||||||
|
title: 'Post Visibility',
|
||||||
|
icon: Icons.visibility_outlined,
|
||||||
|
children: [
|
||||||
|
_buildDefaultVisibilityTile(),
|
||||||
|
_buildAllowChainsTile(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSection(
|
||||||
|
title: 'Interactions',
|
||||||
|
icon: Icons.chat_bubble_outline,
|
||||||
|
children: [
|
||||||
|
_buildWhoCanMessageTile(),
|
||||||
|
_buildWhoCanCommentTile(),
|
||||||
|
_buildShowActivityStatusTile(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSection(
|
||||||
|
title: 'Discovery',
|
||||||
|
icon: Icons.search,
|
||||||
|
children: [
|
||||||
|
_buildShowInSearchTile(),
|
||||||
|
_buildShowSuggestedTile(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSection(
|
||||||
|
title: 'Circle (Close Friends)',
|
||||||
|
icon: Icons.favorite_outline,
|
||||||
|
children: [
|
||||||
|
_buildCircleInfoTile(),
|
||||||
|
_buildManageCircleTile(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSection(
|
||||||
|
title: 'Data & Privacy',
|
||||||
|
icon: Icons.shield_outlined,
|
||||||
|
children: [
|
||||||
|
_buildExportDataTile(),
|
||||||
|
_buildBlockedUsersTile(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSection({
|
||||||
|
required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required List<Widget> children,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: AppTheme.primaryColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: AppTheme.headlineSmall.copyWith(
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.surfaceColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPrivateAccountTile() {
|
||||||
|
return SwitchListTile(
|
||||||
|
title: const Text('Private Account'),
|
||||||
|
subtitle: const Text('Only approved followers can see your posts'),
|
||||||
|
value: _settings.isPrivate,
|
||||||
|
onChanged: (value) => _updateSetting(() => _settings.isPrivate = value),
|
||||||
|
activeColor: AppTheme.primaryColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDefaultVisibilityTile() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Default Post Visibility'),
|
||||||
|
subtitle: Text(_getVisibilityLabel(_settings.defaultVisibility)),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _showVisibilityPicker(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAllowChainsTile() {
|
||||||
|
return SwitchListTile(
|
||||||
|
title: const Text('Allow Chains'),
|
||||||
|
subtitle: const Text('Let others add to your posts'),
|
||||||
|
value: _settings.allowChains,
|
||||||
|
onChanged: (value) => _updateSetting(() => _settings.allowChains = value),
|
||||||
|
activeColor: AppTheme.primaryColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWhoCanMessageTile() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Who Can Message Me'),
|
||||||
|
subtitle: Text(_getAudienceLabel(_settings.whoCanMessage)),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _showAudiencePicker(
|
||||||
|
title: 'Who Can Message You',
|
||||||
|
currentValue: _settings.whoCanMessage,
|
||||||
|
onChanged: (value) => _updateSetting(() => _settings.whoCanMessage = value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWhoCanCommentTile() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Who Can Comment'),
|
||||||
|
subtitle: Text(_getAudienceLabel(_settings.whoCanComment)),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _showAudiencePicker(
|
||||||
|
title: 'Who Can Comment',
|
||||||
|
currentValue: _settings.whoCanComment,
|
||||||
|
onChanged: (value) => _updateSetting(() => _settings.whoCanComment = value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShowActivityStatusTile() {
|
||||||
|
return SwitchListTile(
|
||||||
|
title: const Text('Show Activity Status'),
|
||||||
|
subtitle: const Text('Let others see when you\'re online'),
|
||||||
|
value: _settings.showActivityStatus,
|
||||||
|
onChanged: (value) =>
|
||||||
|
_updateSetting(() => _settings.showActivityStatus = value),
|
||||||
|
activeColor: AppTheme.primaryColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShowInSearchTile() {
|
||||||
|
return SwitchListTile(
|
||||||
|
title: const Text('Show in Search'),
|
||||||
|
subtitle: const Text('Allow others to find you in search'),
|
||||||
|
value: _settings.showInSearch,
|
||||||
|
onChanged: (value) =>
|
||||||
|
_updateSetting(() => _settings.showInSearch = value),
|
||||||
|
activeColor: AppTheme.primaryColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShowSuggestedTile() {
|
||||||
|
return SwitchListTile(
|
||||||
|
title: const Text('Show in Suggestions'),
|
||||||
|
subtitle: const Text('Appear in "Suggested for You"'),
|
||||||
|
value: _settings.showInSuggestions,
|
||||||
|
onChanged: (value) =>
|
||||||
|
_updateSetting(() => _settings.showInSuggestions = value),
|
||||||
|
activeColor: AppTheme.primaryColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCircleInfoTile() {
|
||||||
|
return const ListTile(
|
||||||
|
leading: Icon(Icons.info_outline, color: AppTheme.textSecondary),
|
||||||
|
title: Text('About Circle'),
|
||||||
|
subtitle: Text(
|
||||||
|
'Share posts with only your closest friends. '
|
||||||
|
'Circle members see posts marked "Circle" visibility.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManageCircleTile() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Manage Circle Members'),
|
||||||
|
subtitle: const Text('Add or remove close friends'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Navigate to circle management screen
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Circle management coming soon')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildExportDataTile() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Export My Data'),
|
||||||
|
subtitle: const Text('Download your profile, posts, and connections'),
|
||||||
|
trailing: const Icon(Icons.download_outlined),
|
||||||
|
onTap: () => _exportData(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBlockedUsersTile() {
|
||||||
|
return ListTile(
|
||||||
|
title: const Text('Blocked Users'),
|
||||||
|
subtitle: const Text('Manage users you\'ve blocked'),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Navigate to blocked users screen
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Blocked users screen coming soon')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getVisibilityLabel(String visibility) {
|
||||||
|
switch (visibility) {
|
||||||
|
case 'public':
|
||||||
|
return 'Public - Anyone can see';
|
||||||
|
case 'followers':
|
||||||
|
return 'Followers Only';
|
||||||
|
case 'circle':
|
||||||
|
return 'Circle Only';
|
||||||
|
default:
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getAudienceLabel(String audience) {
|
||||||
|
switch (audience) {
|
||||||
|
case 'everyone':
|
||||||
|
return 'Everyone';
|
||||||
|
case 'followers':
|
||||||
|
return 'Followers Only';
|
||||||
|
case 'mutuals':
|
||||||
|
return 'Mutual Follows Only';
|
||||||
|
case 'nobody':
|
||||||
|
return 'Nobody';
|
||||||
|
default:
|
||||||
|
return audience;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showVisibilityPicker() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: AppTheme.surfaceColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Default Post Visibility', style: AppTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildVisibilityOption('public', 'Public', Icons.public),
|
||||||
|
_buildVisibilityOption('followers', 'Followers Only', Icons.people),
|
||||||
|
_buildVisibilityOption('circle', 'Circle Only', Icons.favorite),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVisibilityOption(String value, String label, IconData icon) {
|
||||||
|
final isSelected = _settings.defaultVisibility == value;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon, color: isSelected ? AppTheme.primaryColor : null),
|
||||||
|
title: Text(label),
|
||||||
|
trailing: isSelected ? const Icon(Icons.check, color: AppTheme.primaryColor) : null,
|
||||||
|
onTap: () {
|
||||||
|
_updateSetting(() => _settings.defaultVisibility = value);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAudiencePicker({
|
||||||
|
required String title,
|
||||||
|
required String currentValue,
|
||||||
|
required void Function(String) onChanged,
|
||||||
|
}) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: AppTheme.surfaceColor,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(title, style: AppTheme.headlineSmall),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildAudienceOption('everyone', 'Everyone', Icons.public, currentValue, onChanged),
|
||||||
|
_buildAudienceOption('followers', 'Followers Only', Icons.people, currentValue, onChanged),
|
||||||
|
_buildAudienceOption('mutuals', 'Mutual Follows', Icons.sync_alt, currentValue, onChanged),
|
||||||
|
_buildAudienceOption('nobody', 'Nobody', Icons.block, currentValue, onChanged),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAudienceOption(
|
||||||
|
String value,
|
||||||
|
String label,
|
||||||
|
IconData icon,
|
||||||
|
String currentValue,
|
||||||
|
void Function(String) onChanged,
|
||||||
|
) {
|
||||||
|
final isSelected = currentValue == value;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon, color: isSelected ? AppTheme.primaryColor : null),
|
||||||
|
title: Text(label),
|
||||||
|
trailing: isSelected ? const Icon(Icons.check, color: AppTheme.primaryColor) : null,
|
||||||
|
onTap: () {
|
||||||
|
onChanged(value);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _exportData() async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const AlertDialog(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
Text('Preparing your data...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final api = ref.read(apiServiceProvider);
|
||||||
|
final data = await api.callGoApi('/users/me/export');
|
||||||
|
|
||||||
|
Navigator.pop(context); // Close loading dialog
|
||||||
|
|
||||||
|
// Show success and maybe allow saving
|
||||||
|
if (mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Export Ready'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Your data export includes:'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('• Profile information', style: AppTheme.bodyMedium),
|
||||||
|
Text('• ${data['posts']?.length ?? 0} posts', style: AppTheme.bodyMedium),
|
||||||
|
Text('• ${data['following']?.length ?? 0} connections', style: AppTheme.bodyMedium),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'The data has been prepared. In a production app, '
|
||||||
|
'this would be saved to your device.',
|
||||||
|
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Done'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Navigator.pop(context); // Close loading dialog
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Export failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue