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

594 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../models/motorcycle_models.dart';
import '../../services/entitlements/entitlement_providers.dart';
import '../../services/entitlements/feature_access_service.dart';
import '../entitlements/widgets/pro_lock_badge.dart';
import 'garage_providers.dart';
import 'widgets/motorcycle_photo.dart';
class AddMotorcycleScreen extends ConsumerStatefulWidget {
const AddMotorcycleScreen({super.key});
@override
ConsumerState<AddMotorcycleScreen> createState() =>
_AddMotorcycleScreenState();
}
class _AddMotorcycleScreenState extends ConsumerState<AddMotorcycleScreen> {
int _step = 1; // 1: Year, 2: Make, 3: Model, 4: Variant, 5: Confirm
int? _selectedYear;
MotorcycleMake? _selectedMake;
MotorcycleModel? _selectedModel;
MotorcycleVariant? _selectedVariant;
MotorcycleSpecification? _previewSpec;
final TextEditingController _nicknameController = TextEditingController();
final TextEditingController _searchController = TextEditingController();
bool _isLoading = false;
List<MotorcycleYear> _years = [];
List<MotorcycleMake> _makes = [];
List<MotorcycleModel> _models = [];
List<MotorcycleVariant> _variants = [];
@override
void initState() {
super.initState();
_loadYears();
}
@override
void dispose() {
_nicknameController.dispose();
_searchController.dispose();
super.dispose();
}
Future<void> _loadYears() async {
setState(() => _isLoading = true);
final repo = ref.read(motorcycleRepositoryProvider);
final years = await repo.getYears();
setState(() {
_years = years;
_isLoading = false;
});
}
Future<void> _selectYear(int year) async {
setState(() {
_selectedYear = year;
_step = 2;
_isLoading = true;
_searchController.clear();
});
final repo = ref.read(motorcycleRepositoryProvider);
final makes = await repo.getMakes(year);
setState(() {
_makes = makes;
_isLoading = false;
});
}
Future<void> _selectMake(MotorcycleMake make) async {
setState(() {
_selectedMake = make;
_step = 3;
_isLoading = true;
_searchController.clear();
});
final repo = ref.read(motorcycleRepositoryProvider);
final models = await repo.getModels(_selectedYear!, make.id);
setState(() {
_models = models;
_isLoading = false;
});
}
Future<void> _selectModel(MotorcycleModel model) async {
setState(() {
_selectedModel = model;
_isLoading = true;
});
final repo = ref.read(motorcycleRepositoryProvider);
final variants = await repo.getVariants(
_selectedYear!,
_selectedMake!.id,
model.id,
);
if (variants.length <= 1) {
_selectedVariant = variants.isNotEmpty ? variants.first : null;
await _loadSpecSummary();
} else {
setState(() {
_variants = variants;
_step = 4;
_isLoading = false;
});
}
}
Future<void> _selectVariant(MotorcycleVariant variant) async {
setState(() => _selectedVariant = variant);
await _loadSpecSummary();
}
Future<void> _loadSpecSummary() async {
setState(() => _isLoading = true);
final selection = MotorcycleSelection(
year: _selectedYear!,
makeId: _selectedMake!.id,
makeName: _selectedMake!.name,
modelId: _selectedModel!.id,
modelName: _selectedModel!.name,
variantId: _selectedVariant?.id,
variantName: _selectedVariant?.name,
);
final repo = ref.read(motorcycleRepositoryProvider);
final spec = await repo.getSpecifications(selection);
setState(() {
_previewSpec = spec;
_step = 5;
_isLoading = false;
});
}
Future<void> _saveMotorcycle() async {
final bikes = ref.read(garageMotorcyclesStreamProvider).value ?? [];
if (bikes.isNotEmpty) {
final canAddMore = ref
.read(featureAccessProvider)
.canAccess(MotorSignalFeature.useMultipleMotorcycles);
if (!canAddMore) {
showProUpgradeSheet(
context,
ref,
feature: MotorSignalFeature.useMultipleMotorcycles,
);
return;
}
}
final selection = MotorcycleSelection(
year: _selectedYear!,
makeId: _selectedMake!.id,
makeName: _selectedMake!.name,
modelId: _selectedModel!.id,
modelName: _selectedModel!.name,
variantId: _selectedVariant?.id,
variantName: _selectedVariant?.name,
);
final controller = ref.read(garageControllerProvider);
await controller.addMotorcycle(
selection,
nickname: _nicknameController.text.trim().isNotEmpty
? _nicknameController.text.trim()
: null,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Added ${selection.displayName} to your Garage!'),
),
);
context.pop();
}
}
void _goBack() {
if (_step > 1) {
setState(() {
_step -= 1;
_searchController.clear();
});
} else {
context.pop();
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: _step == 1,
onPopInvokedWithResult: (didPop, result) {
if (!didPop && _step > 1) _goBack();
},
child: Scaffold(
appBar: AppBar(
title: Text(_getStepTitle()),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _goBack,
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
LinearProgressIndicator(
value: _step / 5.0,
backgroundColor: Colors.white10,
valueColor: const AlwaysStoppedAnimation(Colors.cyanAccent),
),
Expanded(child: _buildStepContent()),
],
),
),
);
}
String _getStepTitle() {
switch (_step) {
case 1:
return 'Select Year';
case 2:
return 'Select Manufacturer';
case 3:
return 'Select Model (${_selectedMake?.name ?? ""})';
case 4:
return 'Select Variant';
case 5:
return 'Confirm Motorcycle';
default:
return 'Add Motorcycle';
}
}
Widget _buildStepContent() {
switch (_step) {
case 1:
return _buildYearStep();
case 2:
return _buildMakeStep();
case 3:
return _buildModelStep();
case 4:
return _buildVariantStep();
case 5:
return _buildConfirmStep();
default:
return const SizedBox();
}
}
Widget _buildYearStep() {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: _years.length,
itemBuilder: (context, index) {
final year = _years[index].year;
return OutlinedButton(
style: OutlinedButton.styleFrom(
side: BorderSide(
color: _selectedYear == year ? Colors.cyanAccent : Colors.white24,
),
backgroundColor: _selectedYear == year
? Colors.cyanAccent.withValues(alpha: 0.2)
: null,
),
onPressed: () => _selectYear(year),
child: Text(
'$year',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.white,
),
),
);
},
);
}
Widget _buildMakeStep() {
final query = _searchController.text.toLowerCase();
final filtered = _makes
.where((m) => m.name.toLowerCase().contains(query))
.toList();
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search Manufacturer...',
prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent),
filled: true,
fillColor: Colors.white10,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
onChanged: (_) => setState(() {}),
),
),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filtered.length,
separatorBuilder: (_, _) =>
const Divider(height: 1, color: Colors.white10),
itemBuilder: (context, index) {
final make = filtered[index];
return ListTile(
title: Text(
make.name,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
trailing: const Icon(
Icons.chevron_right,
color: Colors.white30,
),
onTap: () => _selectMake(make),
);
},
),
),
],
);
}
Widget _buildModelStep() {
final query = _searchController.text.toLowerCase();
final filtered = _models
.where((m) => m.name.toLowerCase().contains(query))
.toList();
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search Model...',
prefixIcon: const Icon(Icons.search, color: Colors.cyanAccent),
filled: true,
fillColor: Colors.white10,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
onChanged: (_) => setState(() {}),
),
),
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: filtered.length,
separatorBuilder: (_, _) =>
const Divider(height: 1, color: Colors.white10),
itemBuilder: (context, index) {
final model = filtered[index];
return ListTile(
title: Text(
model.name,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
subtitle: Text('$_selectedYear ${_selectedMake?.name}'),
trailing: const Icon(
Icons.chevron_right,
color: Colors.white30,
),
onTap: () => _selectModel(model),
);
},
),
),
],
);
}
Widget _buildVariantStep() {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _variants.length,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final variant = _variants[index];
return Card(
child: ListTile(
title: Text(
variant.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
trailing: const Icon(
Icons.check_circle_outline,
color: Colors.cyanAccent,
),
onTap: () => _selectVariant(variant),
),
);
},
);
}
Widget _buildConfirmStep() {
final spec = _previewSpec;
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.cyanAccent, width: 2),
),
child: Column(
children: [
MotorcyclePhoto(
year: _selectedYear!,
make: _selectedMake!.name,
model: _selectedModel!.name,
height: 220,
borderRadius: BorderRadius.circular(10),
),
const SizedBox(height: 14),
Text(
'$_selectedYear ${_selectedMake?.name} ${_selectedModel?.name}',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
if (_selectedVariant?.name != null &&
_selectedVariant!.name != 'Standard') ...[
const SizedBox(height: 4),
Text(
_selectedVariant!.name,
style: const TextStyle(color: Colors.white70, fontSize: 16),
),
],
],
),
),
const SizedBox(height: 24),
if (spec != null) ...[
const Text(
'SPECIFICATION SUMMARY',
style: TextStyle(
color: Colors.cyanAccent,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSpecChip(
'DISPLACEMENT',
spec.displacementCc != null
? '${spec.displacementCc!.toStringAsFixed(0)} cc'
: '—',
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecChip(
'HORSEPOWER',
spec.horsepower != null
? '${spec.horsepower!.toStringAsFixed(0)} HP'
: '—',
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSpecChip(
'TORQUE',
spec.torqueLbFt != null
? '${spec.torqueLbFt!.toStringAsFixed(1)} lb-ft'
: '—',
),
),
const SizedBox(width: 12),
Expanded(
child: _buildSpecChip(
'WET WEIGHT',
spec.wetWeightLbs != null
? '${spec.wetWeightLbs!.toStringAsFixed(0)} lbs'
: '—',
),
),
],
),
const SizedBox(height: 24),
],
TextField(
controller: _nicknameController,
decoration: InputDecoration(
labelText: 'Nickname (Optional)',
hintText: 'e.g., Blue Beast, The Track Bike',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
prefixIcon: const Icon(Icons.edit, color: Colors.cyanAccent),
),
),
const SizedBox(height: 32),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.cyanAccent,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: _saveMotorcycle,
child: const Text(
'ADD TO GARAGE',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
),
],
),
);
}
Widget _buildSpecChip(String label, String value) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 10,
color: Colors.white54,
letterSpacing: 1.0,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
);
}
}