MotorSignal-App/lib/features/garage/garage_screen.dart

340 lines
12 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../database/database.dart';
import '../../models/motorcycle_models.dart';
import '../../core/responsive/responsive_layout.dart';
import '../../core/theme/theme_extensions.dart';
import 'garage_providers.dart';
import 'widgets/motorcycle_photo.dart';
import '../../services/entitlements/entitlement_providers.dart';
import '../../services/entitlements/feature_access_service.dart';
import '../entitlements/widgets/pro_lock_badge.dart';
class GarageScreen extends ConsumerWidget {
const GarageScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bikesAsync = ref.watch(garageMotorcyclesStreamProvider);
final colors = context.motorColors;
return Scaffold(
appBar: AppBar(
title: const Text(
'My Garage',
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
key: const ValueKey('garage_add_motorcycle_appbar_button'),
icon: Icon(
Icons.add_circle_outline,
color: colors.cyberCyan,
),
tooltip: 'Add Motorcycle',
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
),
],
),
body: bikesAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error loading garage: $err')),
data: (bikes) {
if (bikes.isEmpty) return _buildEmptyState(context, colors);
return ResponsiveLayout(
builder: (context, responsive) {
final isMultiColumn = responsive.isExpanded || (responsive.isMedium && responsive.isLandscape);
if (isMultiColumn) {
return GridView.builder(
padding: const EdgeInsets.all(16.0),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 550,
mainAxisExtent: 420,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: bikes.length,
itemBuilder: (context, index) {
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: true);
},
);
}
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: bikes.length,
itemBuilder: (context, index) {
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: false);
},
);
},
);
},
),
floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty
? FloatingActionButton.extended(
key: const ValueKey('garage_add_motorcycle_fab'),
backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black,
icon: const Icon(Icons.two_wheeler),
label: const Text(
'Add Motorcycle',
style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2),
),
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
)
: null,
);
}
void _onAddMotorcycle(
BuildContext context,
WidgetRef ref,
List<GarageMotorcycle>? bikes,
) {
if (bikes != null && bikes.isNotEmpty) {
final canAddMore = ref
.read(featureAccessProvider)
.canAccess(MotorSignalFeature.useMultipleMotorcycles);
if (!canAddMore) {
showProUpgradeSheet(
context,
ref,
feature: MotorSignalFeature.useMultipleMotorcycles,
);
return;
}
}
context.push('/garage/add');
}
Widget _buildEmptyState(BuildContext context, MotorSignalThemeColors colors) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.two_wheeler,
size: 80,
color: theme.colorScheme.onSurface.withValues(alpha: 0.25),
),
const SizedBox(height: 24),
Text(
'Your Garage is Empty',
style: TextStyle(
fontFamily: 'Sora',
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 12),
Text(
'Add your motorcycle to personalize your ride history and performance data.',
textAlign: TextAlign.center,
style: TextStyle(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
height: 1.4,
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
key: const ValueKey('garage_add_motorcycle_empty_button'),
style: ElevatedButton.styleFrom(
backgroundColor: colors.cyberCyan,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 14,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
icon: const Icon(Icons.add),
label: const Text(
'Add Motorcycle',
style: TextStyle(
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
onPressed: () => context.push('/garage/add'),
),
],
),
),
);
}
Widget _buildMotorcycleCard(
BuildContext context,
WidgetRef ref,
GarageMotorcycle bike,
MotorSignalThemeColors colors, {
bool isGrid = false,
}) {
MotorcycleSpecification? spec;
if (bike.specificationJson != null) {
try {
spec = MotorcycleSpecification.fromJson(
json.decode(bike.specificationJson!),
);
} catch (_) {}
}
final displayName = bike.nickname != null && bike.nickname!.isNotEmpty
? '${bike.nickname} (${bike.year} ${bike.makeName} ${bike.modelName})'
: '${bike.year} ${bike.makeName} ${bike.modelName}';
final theme = Theme.of(context);
return Container(
margin: EdgeInsets.only(bottom: isGrid ? 0 : 16),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: bike.isDefault
? colors.cyberCyan.withValues(alpha: 0.6)
: colors.outlineVariant,
width: bike.isDefault ? 1.0 : 0.8,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () => context.push('/garage/${bike.id}'),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MotorcyclePhoto(
year: bike.year,
make: bike.makeName,
model: bike.modelName,
height: 150,
borderRadius: BorderRadius.circular(10),
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
displayName,
style: TextStyle(
fontFamily: 'Sora',
fontSize: 16,
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
),
),
),
if (bike.isDefault)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: colors.cyberCyan.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: colors.cyberCyan.withValues(alpha: 0.4),
width: 0.8,
),
),
child: Text(
'Current',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 10,
fontWeight: FontWeight.w600,
color: colors.cyberCyan,
letterSpacing: 0.3,
),
),
),
],
),
const SizedBox(height: 4),
Text(
spec != null
? '${spec.displacementCc?.toStringAsFixed(0) ?? "—"} cc • ${spec.horsepower?.toStringAsFixed(0) ?? "—"} hp'
: '${bike.year} • ${bike.makeName}',
style: TextStyle(
fontFamily: 'JetBrains Mono',
color: colors.cyberCyan,
fontSize: 12.5,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
runSpacing: 4,
children: [
TextButton.icon(
icon: Icon(Icons.directions_bike, size: 18, color: colors.cyberCyan),
label: Text(
'View Specs & Stats',
style: TextStyle(color: colors.cyberCyan, fontWeight: FontWeight.bold),
),
onPressed: () => context.push('/garage/${bike.id}'),
),
if (!bike.isDefault)
OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
side: BorderSide(color: colors.outlineVariant),
),
onPressed: () {
ref
.read(garageControllerProvider)
.setDefaultMotorcycle(bike.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Set ${bike.makeName} ${bike.modelName} as current motorcycle.',
),
),
);
},
child: const Text('Set as Current'),
),
],
),
],
),
),
),
),
);
}
}