448 lines
14 KiB
Dart
448 lines
14 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 'garage_providers.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 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(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white10,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.cyanAccent, width: 2),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Icon(Icons.two_wheeler, size: 64, color: Colors.cyanAccent),
|
|
const SizedBox(height: 12),
|
|
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)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|