Home auf Favoriten mit Filter added

main
joschy2002 2025-06-19 13:02:17 +02:00
parent 309333424c
commit fb3b9027c5
4 changed files with 249 additions and 167 deletions

View File

@ -3,8 +3,30 @@ import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'training_detail_screen.dart';
class FavoritesTab extends StatelessWidget {
const FavoritesTab({super.key});
class FavoritesTab extends StatefulWidget {
final String? categoryFilter;
const FavoritesTab({super.key, this.categoryFilter});
@override
State<FavoritesTab> createState() => _FavoritesTabState();
}
class _FavoritesTabState extends State<FavoritesTab> {
static const List<String> _categories = [
'Aufwärmen & Mobilisation',
'Wurf- & Torabschluss',
'Torwarttraining',
'Athletik',
'Pass',
'Koordination',
];
String? _selectedCategory;
@override
void initState() {
super.initState();
_selectedCategory = widget.categoryFilter;
}
@override
Widget build(BuildContext context) {
@ -17,118 +39,169 @@ class FavoritesTab extends StatelessWidget {
appBar: AppBar(
title: const Text('Favoriten'),
),
body: StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('User').doc(user.uid).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || !snapshot.data!.exists) {
return const Center(child: Text('Keine Favoriten gefunden'));
}
final data = snapshot.data!.data() as Map<String, dynamic>;
final favorites = List<String>.from(data['favorites'] ?? []);
if (favorites.isEmpty) {
return const Center(child: Text('Keine Favoriten gefunden'));
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
childAspectRatio: 0.75,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Filter-Chip-Leiste
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilterChip(
label: const Text('Alle'),
selected: _selectedCategory == null,
onSelected: (selected) {
setState(() => _selectedCategory = null);
},
),
..._categories.map((cat) => FilterChip(
label: Text(cat),
selected: _selectedCategory == cat,
onSelected: (selected) {
setState(() => _selectedCategory = selected ? cat : null);
},
)),
],
),
itemCount: favorites.length,
itemBuilder: (context, index) {
return FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('Training').doc(favorites[index]).get(),
builder: (context, trainingSnapshot) {
if (!trainingSnapshot.hasData || !trainingSnapshot.data!.exists) {
return const SizedBox.shrink();
),
Expanded(
child: StreamBuilder<DocumentSnapshot>(
stream: FirebaseFirestore.instance.collection('User').doc(user.uid).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || !snapshot.data!.exists) {
return const Center(child: Text('Keine Favoriten gefunden'));
}
final data = snapshot.data!.data() as Map<String, dynamic>;
final allFavorites = List<String>.from(data['favorites'] ?? []);
if (allFavorites.isEmpty) {
return const Center(child: Text('Keine Favoriten gefunden'));
}
// Filtere Favoriten nach Kategorie
Future<List<Map<String, dynamic>>> getFilteredFavorites() async {
List<Map<String, dynamic>> filtered = [];
for (final favId in allFavorites) {
final doc = await FirebaseFirestore.instance.collection('Training').doc(favId).get();
if (!doc.exists) continue;
final trainingData = doc.data() as Map<String, dynamic>;
if (_selectedCategory == null || trainingData['category'] == _selectedCategory) {
filtered.add({'id': favId, 'data': trainingData});
}
}
final trainingData = trainingSnapshot.data!.data() as Map<String, dynamic>;
return Card(
child: Stack(
children: [
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TrainingDetailScreen(trainingId: favorites[index]),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
return filtered;
}
return FutureBuilder<List<Map<String, dynamic>>>(
future: getFilteredFavorites(),
builder: (context, favSnapshot) {
if (!favSnapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final filteredFavorites = favSnapshot.data!;
if (filteredFavorites.isEmpty) {
return const Center(child: Text('Keine Favoriten gefunden'));
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
childAspectRatio: 0.75,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: filteredFavorites.length,
itemBuilder: (context, index) {
final fav = filteredFavorites[index];
final trainingData = fav['data'] as Map<String, dynamic>;
final favId = fav['id'] as String;
return Card(
child: Stack(
children: [
Expanded(
child: Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.fitness_center, size: 50),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TrainingDetailScreen(trainingId: favId),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
trainingData['title'] ?? 'Unbekannt',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 16),
const SizedBox(width: 4),
Text(
(trainingData['rating overall'] ?? 0.0).toStringAsFixed(1),
style: const TextStyle(fontSize: 12),
Expanded(
child: Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.fitness_center, size: 50),
),
],
),
),
const SizedBox(height: 4),
Text(
'Dauer: ${trainingData['duration'] ?? '-'} Minuten',
style: const TextStyle(fontSize: 12, color: Colors.grey),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
trainingData['title'] ?? 'Unbekannt',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 16),
const SizedBox(width: 4),
Text(
(trainingData['rating overall'] ?? 0.0).toStringAsFixed(1),
style: const TextStyle(fontSize: 12),
),
],
),
const SizedBox(height: 4),
Text(
'Dauer: \t${trainingData['duration'] ?? '-'} Minuten',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
],
),
),
Positioned(
top: 4,
right: 4,
child: IconButton(
icon: const Icon(Icons.favorite, color: Colors.red),
onPressed: () async {
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
'favorites': FieldValue.arrayRemove([favId]),
});
},
),
),
],
),
),
Positioned(
top: 4,
right: 4,
child: IconButton(
icon: const Icon(Icons.favorite, color: Colors.red),
onPressed: () async {
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
'favorites': FieldValue.arrayRemove([favorites[index]]),
});
},
),
),
],
),
);
},
);
},
);
},
);
},
);
},
);
},
),
),
],
),
);
}

View File

@ -22,6 +22,7 @@ class _HomeScreenState extends State<HomeScreen> {
Map<String, dynamic>? _nextTraining;
bool _isLoading = true;
DateTime? _calendarInitialDate;
String? _favoriteCategoryFilter;
@override
void initState() {
@ -165,7 +166,7 @@ class _HomeScreenState extends State<HomeScreen> {
List<Widget> get _screens => [
_buildHomeTab(),
const SearchTab(),
const FavoritesTab(),
FavoritesTab(categoryFilter: _favoriteCategoryFilter),
CalendarTab(initialDate: _calendarInitialDate),
ProfileTab(onLogoutSuccess: widget.onLogoutSuccess),
];
@ -349,25 +350,33 @@ class _HomeScreenState extends State<HomeScreen> {
Widget _buildFavoriteCircle(String label, IconData icon, Color color) {
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Column(
children: [
CircleAvatar(
radius: 24,
backgroundColor: color.withOpacity(0.2),
child: Icon(icon, color: color, size: 26),
),
const SizedBox(height: 4),
SizedBox(
width: 60,
child: Text(
label,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
child: GestureDetector(
onTap: () {
setState(() {
_favoriteCategoryFilter = label;
_selectedIndex = 2;
});
},
child: Column(
children: [
CircleAvatar(
radius: 24,
backgroundColor: color.withOpacity(0.2),
child: Icon(icon, color: color, size: 26),
),
),
],
const SizedBox(height: 4),
SizedBox(
width: 60,
child: Text(
label,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}

View File

@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: dda4fd7909a732a014239009aa52537b136f8ce568de23c212587097887e2307
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
url: "https://pub.dev"
source: hosted
version: "1.3.56"
version: "1.3.35"
async:
dependency: transitive
description:
@ -45,26 +45,26 @@ packages:
dependency: "direct main"
description:
name: cloud_firestore
sha256: "80cafe016ea12e4b76737487784dc659720065705445be547d2f47aaaa3f2874"
sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185
url: "https://pub.dev"
source: hosted
version: "5.6.9"
version: "4.17.5"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: "9c5adf444b6e4eca15ecbc497678fba6d9944041fbf22810b756269ddefa75c7"
sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81"
url: "https://pub.dev"
source: hosted
version: "6.6.9"
version: "6.2.5"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: "884c153ac66cdef469ca5e55bf71d979c3b53dc20a4c1662fae5c07bf603d878"
sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9"
url: "https://pub.dev"
source: hosted
version: "4.4.9"
version: "3.12.5"
collection:
dependency: transitive
description:
@ -141,34 +141,34 @@ packages:
dependency: "direct main"
description:
name: firebase_auth
sha256: "10ddd766bd3d3baf1cc8fed544ef83a2c0e6027514a020e56114212ffe1036f2"
sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba
url: "https://pub.dev"
source: hosted
version: "5.6.0"
version: "4.20.0"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: fde52352bfd553f5e38239b868e1e6fe6c1a6af542f5cc547554c61fef30b99b
sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093
url: "https://pub.dev"
source: hosted
version: "7.7.0"
version: "7.3.0"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: "33a0e6521a1c8c476949cc4f2f7b7d9ed8f99514b08dd905fdb9546d2391e5d9"
sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b"
url: "https://pub.dev"
source: hosted
version: "5.15.0"
version: "5.12.0"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "420d9111dcf095341f1ea8fdce926eef750cf7b9745d21f38000de780c94f608"
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
url: "https://pub.dev"
source: hosted
version: "3.14.0"
version: "2.32.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -181,34 +181,34 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e
sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88"
url: "https://pub.dev"
source: hosted
version: "2.23.0"
version: "2.17.5"
firebase_storage:
dependency: "direct main"
description:
name: firebase_storage
sha256: a14390edb4b7a119297ddd09773421233ffddcfa1f8dcb670d9ce71672fdb624
sha256: "2ae478ceec9f458c1bcbf0ee3e0100e4e909708979e83f16d5d9fba35a5b42c1"
url: "https://pub.dev"
source: hosted
version: "12.4.7"
version: "11.7.7"
firebase_storage_platform_interface:
dependency: transitive
description:
name: firebase_storage_platform_interface
sha256: d6c7a32c714cd7d59b61a5c610137695547986b8a5abe71aaf71197d6d255006
sha256: "4e18662e6a66e2e0e181c06f94707de06d5097d70cfe2b5141bf64660c5b5da9"
url: "https://pub.dev"
source: hosted
version: "5.2.7"
version: "5.1.22"
firebase_storage_web:
dependency: transitive
description:
name: firebase_storage_web
sha256: "45ad71124862ef6d3a68770edc0aaefb854c0b4781aa96e9b0ec10400af03861"
sha256: "3a44aacd38a372efb159f6fe36bb4a7d79823949383816457fd43d3d47602a53"
url: "https://pub.dev"
source: hosted
version: "3.10.14"
version: "3.9.7"
fixnum:
dependency: transitive
description:
@ -226,10 +226,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -332,10 +332,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
@ -364,10 +364,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "6.0.0"
version: "5.1.1"
matcher:
dependency: transitive
description:
@ -489,10 +489,10 @@ packages:
dependency: "direct main"
description:
name: table_calendar
sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478"
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.2.0"
term_glyph:
dependency: transitive
description:
@ -545,10 +545,10 @@ packages:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "0.5.1"
sdks:
dart: ">=3.8.0 <4.0.0"
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

View File

@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: '>=3.2.3 <4.0.0'
sdk: '>=3.7.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@ -33,16 +33,16 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
firebase_core: ^3.14.0
table_calendar: ^3.0.9
cloud_firestore: ^5.6.9
firebase_auth: ^5.6.0
firebase_storage: ^12.4.7
image_picker: ^1.0.7
provider: ^6.1.1
uuid: ^4.2.1
intl: ^0.18.0
cupertino_icons: ^1.0.8
firebase_core: ^2.30.1
table_calendar: ^3.1.1
cloud_firestore: ^4.15.8
firebase_auth: ^4.17.8
firebase_storage: ^11.6.6
image_picker: ^1.1.1
provider: ^6.1.2
uuid: ^4.3.3
intl: ^0.20.2
dev_dependencies:
flutter_test:
@ -53,7 +53,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec