Add accepted leg edit notification and class leaderboard
All checks were successful
Release / meta (push) Successful in 12s
Release / linux-build (push) Successful in 1m0s
Release / web-build (push) Successful in 2m6s
Release / android-build (push) Successful in 6m8s
Release / release-master (push) Successful in 16s
Release / release-dev (push) Successful in 19s
All checks were successful
Release / meta (push) Successful in 12s
Release / linux-build (push) Successful in 1m0s
Release / web-build (push) Successful in 2m6s
Release / android-build (push) Successful in 6m8s
Release / release-master (push) Successful in 16s
Release / release-dev (push) Successful in 19s
This commit is contained in:
@@ -17,11 +17,13 @@ class UserProfilePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UserProfilePageState extends State<UserProfilePage> {
|
||||
static const int _pageSize = 22;
|
||||
UserProfileDetail? _profile;
|
||||
List<Leg> _legs = const [];
|
||||
bool _loading = false;
|
||||
bool _loadingMore = false;
|
||||
bool _hasMore = false;
|
||||
bool _lastFetchReturnedData = true;
|
||||
Friendship? _friendship;
|
||||
bool _actionsLoading = false;
|
||||
|
||||
@@ -39,9 +41,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
final userId = _userId;
|
||||
if (userId == null || userId.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No user selected.')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No user selected.')));
|
||||
context.pop();
|
||||
}
|
||||
return;
|
||||
@@ -66,7 +68,8 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
setState(() {
|
||||
_profile = profile;
|
||||
_legs = legs;
|
||||
_hasMore = legs.length >= 25;
|
||||
_lastFetchReturnedData = legs.isNotEmpty;
|
||||
_hasMore = _lastFetchReturnedData && _legs.length >= _pageSize;
|
||||
_friendship = friendship;
|
||||
});
|
||||
} finally {
|
||||
@@ -83,11 +86,13 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
final more = await data.fetchUserLegs(
|
||||
userId: userId,
|
||||
offset: _legs.length,
|
||||
limit: _pageSize,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_legs = [..._legs, ...more];
|
||||
_hasMore = more.length >= 25;
|
||||
_lastFetchReturnedData = more.isNotEmpty;
|
||||
_hasMore = _lastFetchReturnedData && _legs.length >= _pageSize;
|
||||
});
|
||||
} finally {
|
||||
if (mounted) setState(() => _loadingMore = false);
|
||||
@@ -110,7 +115,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
final mileage = profile?.mileage;
|
||||
final privacy = profile?.privacyInfo;
|
||||
final mileageHidden =
|
||||
(mileage == null || mileage == 0) && privacy != null && privacy.isNotEmpty;
|
||||
(mileage == null || mileage == 0) &&
|
||||
privacy != null &&
|
||||
privacy.isNotEmpty;
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
@@ -152,9 +159,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final topTen = [...profile.topLocos]
|
||||
..sort(
|
||||
(a, b) => (b.mileage ?? 0).compareTo(a.mileage ?? 0),
|
||||
);
|
||||
..sort((a, b) => (b.mileage ?? 0).compareTo(a.mileage ?? 0));
|
||||
final displayLocos = topTen.take(10).toList();
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -177,15 +182,14 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
final mileage = loco.mileage ?? 0;
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withValues(alpha: 0.1),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
@@ -195,10 +199,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
subtitle: Text(loco.locoClass),
|
||||
trailing: Text(
|
||||
'${mileage.toStringAsFixed(1)} mi',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -211,8 +214,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
|
||||
List<Widget> _buildLegsWithDividers(
|
||||
BuildContext context,
|
||||
List<Leg> legs,
|
||||
) {
|
||||
List<Leg> legs, {
|
||||
required bool showEditButton,
|
||||
}) {
|
||||
final widgets = <Widget>[];
|
||||
String? currentDate;
|
||||
final dayLegs = <Leg>[];
|
||||
@@ -225,15 +229,21 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
date,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
);
|
||||
widgets.add(const Divider());
|
||||
widgets.addAll(
|
||||
dayLegs.map((leg) => LegCard(leg: leg, showDate: false)),
|
||||
dayLegs.map(
|
||||
(leg) => LegCard(
|
||||
leg: leg,
|
||||
showDate: false,
|
||||
showEditButton: showEditButton,
|
||||
),
|
||||
),
|
||||
);
|
||||
dayLegs.clear();
|
||||
}
|
||||
@@ -295,7 +305,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
break;
|
||||
case 'pending':
|
||||
final isRequester = status.requesterId == auth.userId;
|
||||
label = isRequester ? 'Pending (you sent)' : 'Pending (needs your reply)';
|
||||
label = isRequester
|
||||
? 'Pending (you sent)'
|
||||
: 'Pending (needs your reply)';
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case 'blocked':
|
||||
@@ -325,10 +337,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
}
|
||||
|
||||
Widget _buildActions(Friendship status, AuthService auth) {
|
||||
final isSelf = status.addresseeId == auth.userId || status.requesterId == auth.userId;
|
||||
if (isSelf) {
|
||||
return const Text('This is you.');
|
||||
}
|
||||
final targetUserId = _userId;
|
||||
final isSelf = targetUserId != null && targetUserId == auth.userId;
|
||||
if (isSelf) return const Text('This is you.');
|
||||
final isRequester = status.requesterId == auth.userId;
|
||||
final id = status.id;
|
||||
final buttons = <Widget>[];
|
||||
@@ -350,10 +361,12 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
onPressed: _actionsLoading
|
||||
? null
|
||||
: () => run(() async {
|
||||
final updated = await data.requestFriendship(status.addresseeId);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = updated);
|
||||
}),
|
||||
final updated = await data.requestFriendship(
|
||||
status.addresseeId,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = updated);
|
||||
}),
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text('Send friend request'),
|
||||
),
|
||||
@@ -365,10 +378,12 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
onPressed: _actionsLoading || id == null || id.isEmpty
|
||||
? null
|
||||
: () => run(() async {
|
||||
await data.cancelFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = status.copyWith(status: 'none'));
|
||||
}),
|
||||
await data.cancelFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(
|
||||
() => _friendship = status.copyWith(status: 'none'),
|
||||
);
|
||||
}),
|
||||
child: const Text('Cancel request'),
|
||||
),
|
||||
);
|
||||
@@ -378,10 +393,10 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
onPressed: _actionsLoading || id == null || id.isEmpty
|
||||
? null
|
||||
: () => run(() async {
|
||||
final updated = await data.acceptFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = updated);
|
||||
}),
|
||||
final updated = await data.acceptFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = updated);
|
||||
}),
|
||||
child: const Text('Accept'),
|
||||
),
|
||||
);
|
||||
@@ -390,10 +405,10 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
onPressed: _actionsLoading || id == null || id.isEmpty
|
||||
? null
|
||||
: () => run(() async {
|
||||
final updated = await data.rejectFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = updated);
|
||||
}),
|
||||
final updated = await data.rejectFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = updated);
|
||||
}),
|
||||
child: const Text('Reject'),
|
||||
),
|
||||
);
|
||||
@@ -404,10 +419,10 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
onPressed: _actionsLoading || id == null || id.isEmpty
|
||||
? null
|
||||
: () => run(() async {
|
||||
await data.deleteFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = status.copyWith(status: 'none'));
|
||||
}),
|
||||
await data.deleteFriendship(id);
|
||||
if (!mounted) return;
|
||||
setState(() => _friendship = status.copyWith(status: 'none'));
|
||||
}),
|
||||
icon: const Icon(Icons.person_remove),
|
||||
label: const Text('Unfriend'),
|
||||
),
|
||||
@@ -416,17 +431,14 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
|
||||
if (buttons.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: buttons,
|
||||
);
|
||||
return Wrap(spacing: 8, runSpacing: 8, children: buttons);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final auth = context.watch<AuthService>();
|
||||
final theme = Theme.of(context);
|
||||
final canEdit = auth.userId != null && auth.userId == _userId;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('User profile'),
|
||||
@@ -455,10 +467,7 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Entries',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
Text('Entries', style: theme.textTheme.titleMedium),
|
||||
if (_loading)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
@@ -478,13 +487,17 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
else if (_legs.isEmpty)
|
||||
Text(
|
||||
(_profile?.privacyInfo.isNotEmpty ?? false)
|
||||
? 'Legs hidden due to privacy settings.'
|
||||
? 'Hidden due to privacy settings.'
|
||||
: 'No entries found.',
|
||||
)
|
||||
else ...[
|
||||
..._buildLegsWithDividers(context, _legs),
|
||||
..._buildLegsWithDividers(
|
||||
context,
|
||||
_legs,
|
||||
showEditButton: canEdit,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_hasMore || _loadingMore)
|
||||
if ((_hasMore || _loadingMore) && _legs.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: OutlinedButton.icon(
|
||||
@@ -493,8 +506,9 @@ class _UserProfilePageState extends State<UserProfilePage> {
|
||||
? const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child:
|
||||
CircularProgressIndicator(strokeWidth: 2),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.expand_more),
|
||||
label: Text(
|
||||
|
||||
Reference in New Issue
Block a user