1006 lines
38 KiB
Dart
1006 lines
38 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../../core/responsive/responsive_layout.dart';
|
|
import '../../core/responsive/viewport_size.dart';
|
|
import '../../core/theme/theme_extensions.dart';
|
|
import '../../services/auth/auth_providers.dart';
|
|
import '../../services/social/rider_profile_models.dart';
|
|
import '../../services/social/social_providers.dart';
|
|
import 'widgets/avatar_badge.dart';
|
|
|
|
/// Screen managing friends, requests, and rider discovery.
|
|
class FriendsScreen extends ConsumerStatefulWidget {
|
|
const FriendsScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<FriendsScreen> createState() => _FriendsScreenState();
|
|
}
|
|
|
|
class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
late TabController _tabController;
|
|
final _searchController = TextEditingController();
|
|
bool _isSearching = false;
|
|
RiderSearchResult? _searchResult;
|
|
String? _searchError;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(length: 3, vsync: this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
_searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _performSearch() async {
|
|
final query = _searchController.text.trim();
|
|
if (query.isEmpty) return;
|
|
|
|
final user = ref.read(currentUserProvider);
|
|
if (user == null || user.isAnonymous) return;
|
|
|
|
setState(() {
|
|
_isSearching = true;
|
|
_searchError = null;
|
|
_searchResult = null;
|
|
});
|
|
|
|
try {
|
|
final searchService = ref.read(riderSearchServiceProvider);
|
|
final result = await searchService.searchByHandle(
|
|
rawHandle: query,
|
|
callerUid: user.uid,
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isSearching = false;
|
|
_searchResult = result;
|
|
if (result == null) {
|
|
_searchError = 'No rider found with handle "$query"';
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isSearching = false;
|
|
_searchError = 'Search error: $e';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _sendFriendRequest(RiderSearchResult target) async {
|
|
final user = ref.read(currentUserProvider);
|
|
final myProfileAsync = ref.read(myRiderProfileProvider);
|
|
final myProfile = myProfileAsync.value;
|
|
|
|
if (user == null || user.isAnonymous || myProfile == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Please create your rider profile first.'),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final friendshipService = ref.read(friendshipServiceProvider);
|
|
await friendshipService.sendFriendRequest(
|
|
sender: myProfile,
|
|
receiverUid: target.uid,
|
|
);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Friend request sent to @${target.handle}')),
|
|
);
|
|
}
|
|
await _performSearch();
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _acceptFriendRequest(FriendRequest request) async {
|
|
final myProfile = ref.read(myRiderProfileProvider).value;
|
|
if (myProfile == null) return;
|
|
|
|
try {
|
|
final friendshipService = ref.read(friendshipServiceProvider);
|
|
await friendshipService.acceptFriendRequest(
|
|
me: myProfile,
|
|
request: request,
|
|
);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Now friends with @${request.senderHandle}!'),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Error accepting request: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _declineFriendRequest(FriendRequest request) async {
|
|
final user = ref.read(currentUserProvider);
|
|
if (user == null) return;
|
|
|
|
try {
|
|
final friendshipService = ref.read(friendshipServiceProvider);
|
|
await friendshipService.declineFriendRequest(
|
|
myUid: user.uid,
|
|
senderUid: request.senderUid,
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Error declining request: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _cancelFriendRequest(String receiverUid) async {
|
|
final user = ref.read(currentUserProvider);
|
|
if (user == null) return;
|
|
|
|
try {
|
|
final friendshipService = ref.read(friendshipServiceProvider);
|
|
await friendshipService.cancelFriendRequest(
|
|
myUid: user.uid,
|
|
receiverUid: receiverUid,
|
|
);
|
|
if (_searchResult?.uid == receiverUid) {
|
|
await _performSearch();
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Error canceling request: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _removeFriend(FriendSummary friend) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text('Remove @${friend.handle}?'),
|
|
content: Text(
|
|
'Are you sure you want to remove ${friend.displayName} from your friends list?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('CANCEL'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
child: const Text('REMOVE'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed != true) return;
|
|
|
|
final user = ref.read(currentUserProvider);
|
|
if (user == null) return;
|
|
|
|
try {
|
|
final friendshipService = ref.read(friendshipServiceProvider);
|
|
await friendshipService.removeFriend(
|
|
myUid: user.uid,
|
|
friendUid: friend.uid,
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Error removing friend: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _blockRider(String targetUid, String handle, String name) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: Text('Block @$handle?'),
|
|
content: Text(
|
|
'Are you sure you want to block $name? They will not be able to find your profile, send requests, or see your activity.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('CANCEL'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
child: const Text('BLOCK'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed != true) return;
|
|
|
|
final user = ref.read(currentUserProvider);
|
|
if (user == null) return;
|
|
|
|
try {
|
|
final friendshipService = ref.read(friendshipServiceProvider);
|
|
await friendshipService.blockRider(
|
|
myUid: user.uid,
|
|
targetUid: targetUid,
|
|
targetHandle: handle,
|
|
targetDisplayName: name,
|
|
);
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Blocked @$handle')),
|
|
);
|
|
setState(() {
|
|
_searchResult = null;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Error blocking rider: $e')),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = context.motorColors;
|
|
final authState = ref.watch(authNotifierProvider);
|
|
final isAuthed = authState.isAuthenticated;
|
|
|
|
if (!isAuthed) {
|
|
return Scaffold(
|
|
backgroundColor: colors.surfaceContainer,
|
|
appBar: AppBar(
|
|
backgroundColor: colors.surfaceContainerHigh,
|
|
title: const Text(
|
|
'Friends & Riders',
|
|
style: TextStyle(fontWeight: FontWeight.w800),
|
|
),
|
|
),
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.group_outlined,
|
|
size: 64,
|
|
color: colors.cyberCyan,
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'MOTORCLOUD ACCOUNT REQUIRED',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 16,
|
|
color: Colors.white,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Sign in to add riding buddies, share meetups, and coordinate future group rides.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 13, color: Colors.white70),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
icon: const Icon(Icons.login),
|
|
label: const Text('SIGN IN OR REGISTER'),
|
|
onPressed: () => context.push('/settings/account'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final friendsAsync = ref.watch(myFriendsProvider);
|
|
final incomingAsync = ref.watch(myIncomingRequestsProvider);
|
|
final outgoingAsync = ref.watch(myOutgoingRequestsProvider);
|
|
|
|
final incomingCount = incomingAsync.value?.length ?? 0;
|
|
final friendsCount = friendsAsync.value?.length ?? 0;
|
|
|
|
return Scaffold(
|
|
backgroundColor: colors.surfaceContainer,
|
|
appBar: AppBar(
|
|
backgroundColor: colors.surfaceContainerHigh,
|
|
title: const Text(
|
|
'Friends & Riders',
|
|
style: TextStyle(fontWeight: FontWeight.w800),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.person_outline),
|
|
tooltip: 'My Profile',
|
|
onPressed: () => context.push('/settings/profile'),
|
|
),
|
|
],
|
|
bottom: TabBar(
|
|
controller: _tabController,
|
|
isScrollable: true,
|
|
tabAlignment: TabAlignment.start,
|
|
indicatorColor: colors.cyberCyan,
|
|
labelColor: colors.cyberCyan,
|
|
unselectedLabelColor: Colors.white60,
|
|
tabs: [
|
|
Tab(text: 'FRIENDS ($friendsCount)'),
|
|
Tab(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('REQUESTS'),
|
|
if (incomingCount > 0) ...[
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: colors.warningAmber,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
'$incomingCount',
|
|
style: const TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const Tab(text: 'FIND RIDERS'),
|
|
],
|
|
),
|
|
),
|
|
body: TabBarView(
|
|
controller: _tabController,
|
|
children: [
|
|
// TAB 1: FRIENDS LIST
|
|
friendsAsync.when(
|
|
loading: () => Center(
|
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
|
),
|
|
error: (err, _) => Center(
|
|
child: Text(
|
|
'Error: $err',
|
|
style: TextStyle(color: colors.raceRed),
|
|
),
|
|
),
|
|
data: (friends) {
|
|
if (friends.isEmpty) {
|
|
return SingleChildScrollView(
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.people_alt_outlined,
|
|
size: 48,
|
|
color: Colors.white24,
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'NO FRIENDS ADDED YET',
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 14,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
const Text(
|
|
'Switch to "Find Riders" to search for riding buddies by @handle.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.white38, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
icon: const Icon(Icons.search),
|
|
label: const Text('FIND RIDERS'),
|
|
onPressed: () => _tabController.animateTo(2),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ResponsiveLayout(
|
|
builder: (context, snapshot) {
|
|
final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
|
|
(snapshot.isLandscape && snapshot.width >= 720);
|
|
|
|
if (isWide) {
|
|
final crossAxisCount = snapshot.width >= 1100 ? 3 : 2;
|
|
return GridView.builder(
|
|
padding: const EdgeInsets.all(20),
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: crossAxisCount,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
mainAxisExtent: 88,
|
|
),
|
|
itemCount: friends.length,
|
|
itemBuilder: (context, index) {
|
|
final friend = friends[index];
|
|
return Card(
|
|
color: colors.surfaceContainerHigh,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
side: BorderSide(color: colors.outlineVariant),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 8,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
AvatarBadge(
|
|
avatarPreset: friend.avatarPreset,
|
|
photoUrl: friend.avatarUrl,
|
|
displayName: friend.displayName,
|
|
radius: 24,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
friend.displayName,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
friend.displayHandle,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: colors.cyberCyan,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_buildFriendMenu(friend, colors),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(16.0),
|
|
itemCount: friends.length,
|
|
separatorBuilder: (context, index) =>
|
|
Divider(color: colors.outlineVariant),
|
|
itemBuilder: (context, index) {
|
|
final friend = friends[index];
|
|
return ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: AvatarBadge(
|
|
avatarPreset: friend.avatarPreset,
|
|
photoUrl: friend.avatarUrl,
|
|
displayName: friend.displayName,
|
|
radius: 24,
|
|
),
|
|
title: Text(
|
|
friend.displayName,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
friend.displayHandle,
|
|
style: TextStyle(
|
|
color: colors.cyberCyan,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
trailing: _buildFriendMenu(friend, colors),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
|
|
// TAB 2: REQUESTS
|
|
ListView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
children: [
|
|
// Incoming Section
|
|
Text(
|
|
'INCOMING REQUESTS',
|
|
style: TextStyle(
|
|
color: colors.cyberCyan,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
incomingAsync.when(
|
|
loading: () => Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
|
),
|
|
),
|
|
error: (err, _) => Text(
|
|
'Error: $err',
|
|
style: TextStyle(color: colors.raceRed),
|
|
),
|
|
data: (requests) {
|
|
if (requests.isEmpty) {
|
|
return const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Text(
|
|
'No pending incoming requests',
|
|
style: TextStyle(color: Colors.white38, fontSize: 12),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: requests.map((req) {
|
|
return Card(
|
|
color: colors.surfaceContainerHigh,
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
side: BorderSide(color: colors.outlineVariant),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Row(
|
|
children: [
|
|
AvatarBadge(
|
|
avatarPreset: req.senderAvatarPreset,
|
|
photoUrl: req.senderAvatarUrl,
|
|
displayName: req.senderDisplayName,
|
|
radius: 20,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
req.senderDisplayName,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
'@${req.senderHandle}',
|
|
style: TextStyle(
|
|
color: colors.cyberCyan,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.check_circle_outline,
|
|
color: colors.launchGreen,
|
|
),
|
|
tooltip: 'Accept',
|
|
onPressed: () => _acceptFriendRequest(req),
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.cancel_outlined,
|
|
color: colors.raceRed,
|
|
),
|
|
tooltip: 'Decline',
|
|
onPressed: () => _declineFriendRequest(req),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Outgoing Section
|
|
const Text(
|
|
'SENT REQUESTS',
|
|
style: TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
outgoingAsync.when(
|
|
loading: () => const SizedBox.shrink(),
|
|
error: (err, _) => Text(
|
|
'Error: $err',
|
|
style: TextStyle(color: colors.raceRed),
|
|
),
|
|
data: (requests) {
|
|
if (requests.isEmpty) {
|
|
return const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Text(
|
|
'No pending sent requests',
|
|
style: TextStyle(color: Colors.white38, fontSize: 12),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
children: requests.map((req) {
|
|
return ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: const Icon(
|
|
Icons.outgoing_mail,
|
|
color: Colors.white54,
|
|
),
|
|
title: Text(
|
|
'To Rider (${req.receiverUid.substring(0, req.receiverUid.length.clamp(0, 8))}...)',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
'Sent ${req.sentAt.toLocal().toString().split('.')[0]}',
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.white38,
|
|
),
|
|
),
|
|
trailing: TextButton(
|
|
onPressed: () =>
|
|
_cancelFriendRequest(req.receiverUid),
|
|
child: const Text('CANCEL'),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
|
|
// TAB 3: FIND RIDERS
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchController,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter @handle (e.g. speed_demon)',
|
|
prefixIcon: const Icon(Icons.search),
|
|
border: const OutlineInputBorder(),
|
|
filled: true,
|
|
fillColor: colors.surfaceContainerHigh,
|
|
),
|
|
onSubmitted: (_) => _performSearch(),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 16,
|
|
),
|
|
),
|
|
onPressed: _isSearching ? null : _performSearch,
|
|
child: _isSearching
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.black,
|
|
),
|
|
)
|
|
: const Text('SEARCH'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
if (_searchError != null)
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Text(
|
|
_searchError!,
|
|
style: const TextStyle(
|
|
color: Colors.white60,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
if (_searchResult != null) ...[
|
|
Card(
|
|
color: colors.surfaceContainerHigh,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
side: BorderSide(color: colors.outlineVariant),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
AvatarBadge(
|
|
avatarPreset: _searchResult!.avatarPreset,
|
|
photoUrl: _searchResult!.avatarUrl,
|
|
displayName: _searchResult!.displayName,
|
|
radius: 28,
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_searchResult!.displayName,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 16,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
Text(
|
|
_searchResult!.displayHandle,
|
|
style: TextStyle(
|
|
color: colors.cyberCyan,
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
if (_searchResult!.region.isNotEmpty) ...[
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
_searchResult!.region,
|
|
style: const TextStyle(
|
|
color: Colors.white60,
|
|
fontSize: 11,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (_searchResult!.bio.isNotEmpty) ...[
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_searchResult!.bio,
|
|
style: const TextStyle(
|
|
color: Colors.white70,
|
|
fontSize: 12,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
Divider(height: 24, color: colors.outlineVariant),
|
|
|
|
// Relationship Action
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
if (_searchResult!.relationship ==
|
|
FriendshipStatus.self)
|
|
const Text(
|
|
'This is your profile',
|
|
style: TextStyle(
|
|
color: Colors.white38,
|
|
fontSize: 12,
|
|
),
|
|
)
|
|
else if (_searchResult!.relationship ==
|
|
FriendshipStatus.friend)
|
|
Chip(
|
|
avatar: Icon(Icons.check,
|
|
size: 16, color: colors.cyberCyan),
|
|
label: const Text('Friends'),
|
|
backgroundColor:
|
|
colors.cyberCyan.withValues(alpha: 0.15),
|
|
labelStyle: TextStyle(
|
|
color: colors.cyberCyan,
|
|
),
|
|
)
|
|
else if (_searchResult!.relationship ==
|
|
FriendshipStatus.requestOutgoing)
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor:
|
|
colors.surfaceContainerHighest,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
icon: const Icon(Icons.cancel_outlined,
|
|
size: 16),
|
|
label: const Text('CANCEL REQUEST'),
|
|
onPressed: () =>
|
|
_cancelFriendRequest(_searchResult!.uid),
|
|
)
|
|
else if (_searchResult!.relationship ==
|
|
FriendshipStatus.requestIncoming)
|
|
Text(
|
|
'Check Requests tab to respond',
|
|
style: TextStyle(
|
|
color: colors.warningAmber,
|
|
fontSize: 12,
|
|
),
|
|
)
|
|
else if (!_searchResult!.allowFriendRequests)
|
|
const Text(
|
|
'Not accepting friend requests',
|
|
style: TextStyle(
|
|
color: Colors.white38,
|
|
fontSize: 12,
|
|
),
|
|
)
|
|
else
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: colors.cyberCyan,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
icon: const Icon(Icons.person_add, size: 16),
|
|
label: const Text('ADD FRIEND'),
|
|
onPressed: () =>
|
|
_sendFriendRequest(_searchResult!),
|
|
),
|
|
|
|
if (_searchResult!.relationship !=
|
|
FriendshipStatus.self)
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.block,
|
|
color: Colors.white38,
|
|
size: 18,
|
|
),
|
|
tooltip: 'Block Rider',
|
|
onPressed: () => _blockRider(
|
|
_searchResult!.uid,
|
|
_searchResult!.handle,
|
|
_searchResult!.displayName,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFriendMenu(
|
|
FriendSummary friend, MotorSignalThemeColors colors) {
|
|
return PopupMenuButton<String>(
|
|
icon: const Icon(Icons.more_vert, color: Colors.white60),
|
|
color: colors.surfaceContainerHighest,
|
|
onSelected: (val) {
|
|
if (val == 'remove') {
|
|
_removeFriend(friend);
|
|
} else if (val == 'block') {
|
|
_blockRider(friend.uid, friend.handle, friend.displayName);
|
|
}
|
|
},
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(
|
|
value: 'remove',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.person_remove_outlined, size: 18),
|
|
SizedBox(width: 8),
|
|
Text('Remove Friend'),
|
|
],
|
|
),
|
|
),
|
|
PopupMenuItem(
|
|
value: 'block',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.block, size: 18, color: colors.raceRed),
|
|
const SizedBox(width: 8),
|
|
Text('Block Rider', style: TextStyle(color: colors.raceRed)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|