422 lines
13 KiB
Dart
422 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../main.dart';
|
|
import '../services/collection_service.dart';
|
|
import '../theme/app_colors.dart';
|
|
import 'garage_screen.dart';
|
|
import 'manage_collection_screen.dart';
|
|
|
|
/// Lists all collections the current user is a member of.
|
|
class CollectionsScreen extends StatefulWidget {
|
|
const CollectionsScreen({super.key});
|
|
|
|
@override
|
|
State<CollectionsScreen> createState() => CollectionsScreenState();
|
|
}
|
|
|
|
class CollectionsScreenState extends State<CollectionsScreen> {
|
|
List<Collection> _collections = [];
|
|
bool _isLoading = true;
|
|
String? _error;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
/// Public so other tabs can trigger a refresh.
|
|
void refresh() => _load();
|
|
|
|
Future<void> _load() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
});
|
|
|
|
try {
|
|
final list = await CollectionService.getMyCollections();
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_collections = list;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_error = e.toString();
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _createCollection() async {
|
|
final formKey = GlobalKey<FormState>();
|
|
final nameCtrl = TextEditingController();
|
|
final descCtrl = TextEditingController();
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => AlertDialog(
|
|
icon: Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: const BoxDecoration(
|
|
gradient: AppColors.brandGradient,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.add, color: Colors.white, size: 28),
|
|
),
|
|
title: const Text('New Collection'),
|
|
content: Form(
|
|
key: formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
controller: nameCtrl,
|
|
autofocus: true,
|
|
maxLength: 50,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Name',
|
|
hintText: 'e.g. Hot Wheels, Matchbox…',
|
|
),
|
|
validator: (value) {
|
|
final trimmed = value?.trim() ?? '';
|
|
if (trimmed.isEmpty) return 'Name is required';
|
|
if (trimmed.length < 2) return 'Name must be at least 2 characters';
|
|
if (trimmed.length > 50) return 'Name must be 50 characters or fewer';
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: descCtrl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Description (optional)',
|
|
hintText: 'What is this collection for?',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
if (formKey.currentState!.validate()) {
|
|
Navigator.pop(context, true);
|
|
}
|
|
},
|
|
child: const Text('Create'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (result != true) return;
|
|
|
|
try {
|
|
await CollectionService.create(
|
|
name: nameCtrl.text.trim(),
|
|
description: descCtrl.text.trim(),
|
|
);
|
|
showGlobalSnackBar('Collection created!');
|
|
_load();
|
|
} catch (e) {
|
|
showGlobalSnackBar('Failed: $e', isError: true);
|
|
}
|
|
}
|
|
|
|
void _openCollection(Collection c) {
|
|
navigatorKey.currentState!.push(
|
|
MaterialPageRoute(
|
|
builder: (_) => GarageScreen(
|
|
collectionId: c.id,
|
|
collectionName: c.name,
|
|
isOwner: c.isOwner,
|
|
),
|
|
),
|
|
).then((_) => _load()); // refresh counts when coming back
|
|
}
|
|
|
|
void _manageCollection(Collection c) {
|
|
navigatorKey.currentState!.push(
|
|
MaterialPageRoute(
|
|
builder: (_) => ManageCollectionScreen(collection: c),
|
|
),
|
|
).then((_) => _load());
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
// ── Header ──
|
|
SliverAppBar(
|
|
expandedHeight: 140,
|
|
pinned: true,
|
|
flexibleSpace: FlexibleSpaceBar(
|
|
titlePadding: const EdgeInsets.only(left: 20, bottom: 16),
|
|
title: Text(
|
|
'Collections',
|
|
style: TextStyle(
|
|
fontFamily: 'Poppins',
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 22,
|
|
color: Colors.white,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.black.withValues(alpha: 0.3),
|
|
blurRadius: 4,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
background: Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: AppColors.brandGradient,
|
|
),
|
|
child: Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(right: 24),
|
|
child: Icon(
|
|
Icons.collections_bookmark,
|
|
size: 72,
|
|
color: Colors.white.withValues(alpha: 0.15),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── Content ──
|
|
if (_isLoading)
|
|
const SliverFillRemaining(
|
|
child: Center(child: CircularProgressIndicator()),
|
|
)
|
|
else if (_error != null)
|
|
SliverFillRemaining(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.error_outline,
|
|
size: 56, color: AppColors.error),
|
|
const SizedBox(height: 16),
|
|
Text(_error!,
|
|
textAlign: TextAlign.center,
|
|
style:
|
|
const TextStyle(color: AppColors.textSecondary)),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: _load,
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
else if (_collections.isEmpty)
|
|
SliverFillRemaining(
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(40),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.collections_bookmark_outlined,
|
|
size: 64, color: AppColors.textHint),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'No collections yet',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'Create your first collection to start tracking!',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: AppColors.textHint),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton.icon(
|
|
onPressed: _createCollection,
|
|
icon: const Icon(Icons.add),
|
|
label: const Text('Create Collection'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 100),
|
|
sliver: SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) {
|
|
final c = _collections[index];
|
|
return _CollectionCard(
|
|
collection: c,
|
|
onTap: () => _openCollection(c),
|
|
onManage: () => _manageCollection(c),
|
|
);
|
|
},
|
|
childCount: _collections.length,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: _collections.isNotEmpty
|
|
? FloatingActionButton(
|
|
onPressed: _createCollection,
|
|
child: const Icon(Icons.add),
|
|
)
|
|
: null,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Collection Card ──────────────────────────────────────────────────
|
|
|
|
class _CollectionCard extends StatelessWidget {
|
|
final Collection collection;
|
|
final VoidCallback onTap;
|
|
final VoidCallback onManage;
|
|
|
|
const _CollectionCard({
|
|
required this.collection,
|
|
required this.onTap,
|
|
required this.onManage,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final c = collection;
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
// Icon
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
gradient: c.isOwner
|
|
? AppColors.brandGradient
|
|
: const LinearGradient(
|
|
colors: [AppColors.navy, AppColors.navyLight],
|
|
),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Icon(
|
|
c.isOwner
|
|
? Icons.collections_bookmark
|
|
: Icons.group,
|
|
color: Colors.white,
|
|
size: 28,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
|
|
// Info
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
c.name,
|
|
style: const TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
_MiniStat(
|
|
icon: Icons.directions_car,
|
|
value: '${c.itemCount}'),
|
|
const SizedBox(width: 16),
|
|
_MiniStat(
|
|
icon: Icons.people,
|
|
value: '${c.memberCount}'),
|
|
const SizedBox(width: 16),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: c.isOwner
|
|
? AppColors.orange.withValues(alpha: 0.15)
|
|
: AppColors.navy.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
c.isOwner ? 'Owner' : 'Member',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: c.isOwner
|
|
? AppColors.orange
|
|
: AppColors.navy,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Manage button
|
|
IconButton(
|
|
icon: const Icon(Icons.settings_outlined,
|
|
color: AppColors.textHint),
|
|
onPressed: onManage,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MiniStat extends StatelessWidget {
|
|
final IconData icon;
|
|
final String value;
|
|
const _MiniStat({required this.icon, required this.value});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 14, color: AppColors.textHint),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|