208 lines
6.1 KiB
Dart
208 lines
6.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:mileograph_flutter/objects/objects.dart';
|
|
import 'package:mileograph_flutter/services/data_service.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class ProfilePage extends StatefulWidget {
|
|
const ProfilePage({super.key});
|
|
|
|
@override
|
|
State<ProfilePage> createState() => _ProfilePageState();
|
|
}
|
|
|
|
class _ProfilePageState extends State<ProfilePage> {
|
|
bool _initialised = false;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_initialised) return;
|
|
_initialised = true;
|
|
_refreshAwards();
|
|
}
|
|
|
|
Future<void> _refreshAwards() {
|
|
return context.read<DataService>().fetchBadgeAwards();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final data = context.watch<DataService>();
|
|
final awards = data.badgeAwards;
|
|
final loading = data.isBadgeAwardsLoading;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Badges'),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
final navigator = Navigator.of(context);
|
|
if (navigator.canPop()) {
|
|
navigator.pop();
|
|
} else {
|
|
context.go('/');
|
|
}
|
|
},
|
|
),
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: _refreshAwards,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
if (loading && awards.isEmpty)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 24.0),
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
)
|
|
else if (awards.isEmpty)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Text('No badges awarded yet.'),
|
|
)
|
|
else
|
|
...awards.map((award) => _buildAwardCard(context, award)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAwardCard(BuildContext context, BadgeAward award) {
|
|
final badgeName = _formatBadgeName(award.badgeCode);
|
|
final tier = award.badgeTier.isNotEmpty
|
|
? award.badgeTier[0].toUpperCase() + award.badgeTier.substring(1)
|
|
: '';
|
|
final tierIcon = _buildTierIcon(award.badgeTier);
|
|
final scope = _scopeToShow(award);
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
if (tierIcon != null) ...[
|
|
tierIcon,
|
|
const SizedBox(width: 8),
|
|
],
|
|
Expanded(
|
|
child: Text(
|
|
'$badgeName • $tier',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
if (award.awardedAt != null)
|
|
Text(
|
|
_formatAwardDate(award.awardedAt!),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
if (scope != null && scope.isNotEmpty) ...[
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
scope,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
if (award.loco != null) ...[
|
|
const SizedBox(height: 8),
|
|
_buildLocoInfo(context, award.loco!),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLocoInfo(BuildContext context, LocoSummary loco) {
|
|
final lines = <String>[];
|
|
final classNum = [
|
|
if (loco.locoClass.isNotEmpty) loco.locoClass,
|
|
if (loco.number.isNotEmpty) loco.number,
|
|
].join(' ');
|
|
if (classNum.isNotEmpty) lines.add(classNum);
|
|
if ((loco.name ?? '').isNotEmpty) lines.add(loco.name!);
|
|
if ((loco.livery ?? '').isNotEmpty) lines.add(loco.livery!);
|
|
if ((loco.location ?? '').isNotEmpty) lines.add(loco.location!);
|
|
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Icon(Icons.train, size: 20),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: lines.map((line) {
|
|
return Text(
|
|
line,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _formatBadgeName(String code) {
|
|
if (code.isEmpty) return 'Badge';
|
|
const known = {
|
|
'class_clearance': 'Class Clearance',
|
|
'loco_clearance': 'Loco Clearance',
|
|
};
|
|
final lower = code.toLowerCase();
|
|
if (known.containsKey(lower)) return known[lower]!;
|
|
final parts = code.split(RegExp(r'[_\\s]+')).where((p) => p.isNotEmpty);
|
|
return parts
|
|
.map((p) => p[0].toUpperCase() + p.substring(1).toLowerCase())
|
|
.join(' ');
|
|
}
|
|
|
|
String _formatAwardDate(DateTime date) {
|
|
final y = date.year.toString().padLeft(4, '0');
|
|
final m = date.month.toString().padLeft(2, '0');
|
|
final d = date.day.toString().padLeft(2, '0');
|
|
return '$y-$m-$d';
|
|
}
|
|
|
|
Widget? _buildTierIcon(String tier) {
|
|
final lower = tier.toLowerCase();
|
|
Color? color;
|
|
switch (lower) {
|
|
case 'bronze':
|
|
color = const Color(0xFFCD7F32);
|
|
break;
|
|
case 'silver':
|
|
color = const Color(0xFFC0C0C0);
|
|
break;
|
|
case 'gold':
|
|
color = const Color(0xFFFFD700);
|
|
break;
|
|
}
|
|
if (color == null) return null;
|
|
return Icon(Icons.emoji_events, color: color);
|
|
}
|
|
|
|
String? _scopeToShow(BadgeAward award) {
|
|
final scope = award.scopeValue?.trim() ?? '';
|
|
if (scope.isEmpty) return null;
|
|
final code = award.badgeCode.toLowerCase();
|
|
if (code == 'loco_clearance') {
|
|
// Hide numeric loco IDs; loco details are shown separately.
|
|
if (int.tryParse(scope) != null) return null;
|
|
}
|
|
return scope;
|
|
}
|
|
}
|