From a66b03591fb34375f70f5240964b3478e3096f4b Mon Sep 17 00:00:00 2001 From: joschy2002 Date: Sat, 28 Jun 2025 16:16:05 +0200 Subject: [PATCH] Filter-Optionen erweitert --- .../.firebase/hosting.YnVpbGQvd2Vi.cache | 26 +-- trainerbox/lib/screens/favorites_tab.dart | 220 ++++++++++++++++-- trainerbox/lib/screens/search_tab.dart | 199 +++++++++++++++- 3 files changed, 399 insertions(+), 46 deletions(-) diff --git a/trainerbox/.firebase/hosting.YnVpbGQvd2Vi.cache b/trainerbox/.firebase/hosting.YnVpbGQvd2Vi.cache index 7bb26f5..d2f67a1 100644 --- a/trainerbox/.firebase/hosting.YnVpbGQvd2Vi.cache +++ b/trainerbox/.firebase/hosting.YnVpbGQvd2Vi.cache @@ -20,16 +20,16 @@ assets/images/arrowLinks.png,1745788948322,7c2e6d924c8b21aba8e6b58b5998d964c2ff1 assets/images/prototype/prototype2.png,1745788948328,59863dcfc0eb346a4ddccde3ee5d31cbbc318e57fc1b9c82f34ce924c803f100 assets/images/prototype/prototype1.png,1745788948325,38856e73e8544f42c8550c249ab5d820a0b511a03b2ff5913a5c327e37bf5a23 assets/images/prototype/leancanvas1.png,1745788948324,2f6245b5ee908e682cf917905301510f7da5848d91373eea8f6d020bd2804478 -version.json,1750854384037,beb53905e09aa026fbfd8544475307ac1cc8878b5eb8072f44b5b1b8dfe47caa -index.html,1750854367177,bd7b8ed757c3a9f61eb8e01f3f90724ff4aa03e200c15cd33d8653aa2b0cf31b -assets/AssetManifest.json,1750854384117,7d5ecba60e3a1f05f8dadca2094eb13090f8e9578d01c50037c02ab5c59ecfa4 -flutter_service_worker.js,1750854385402,421d1d7b7930952a7264269cd29d75fb62fac167f509f169e7f6831033228a92 -assets/FontManifest.json,1750854384117,9ea504185602e57d97b7c3517d382b8627a13c0181c490c96a9b55a5d5c8810c -assets/AssetManifest.bin.json,1750854384117,77c62ce3fd7c494bed363e766dde46e5e86faaa47e2b3239137f8cb4eb7c30fb -flutter_bootstrap.js,1750854367174,2f27bebc9fd3fd493f4873c2848645e773e34c481daf746afe71584c49e22da8 -assets/AssetManifest.bin,1750854384117,3838b71b15de7f587ccaac0e708e90e9e5eae28b02e743b4291899f4b688bf44 -assets/shaders/ink_sparkle.frag,1750854384207,80c6e65c75f1de434b1b22dba61e96ad82dba0f2fc5e8b3b59c2def46d794354 -assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1750854385049,12bc6bf55aad4657f62747b6c1c9b5c120a594ed3540db21729b6db3c847340d -assets/fonts/MaterialIcons-Regular.otf,1750854385049,9c76fa5bc2f5c3465076add6ae7d25566055b3dbff99068c5ac137554cb3e609 -assets/NOTICES,1750854384118,2423be738108ad3cecb49273868c0853d6b60024fc565d67e59a66196f6b3a57 -main.dart.js,1750854383651,4dc22e5059fa73f18601ca0ae30c8f55bc50ae61f8901472f5e8832b1772a643 +version.json,1751119914966,beb53905e09aa026fbfd8544475307ac1cc8878b5eb8072f44b5b1b8dfe47caa +index.html,1751119898794,bd7b8ed757c3a9f61eb8e01f3f90724ff4aa03e200c15cd33d8653aa2b0cf31b +flutter_service_worker.js,1751119916329,85798f31ac6b4025d57bfa85f416d5024e79d4f0045bec83082ffbd7daf53e57 +assets/AssetManifest.json,1751119915042,7d5ecba60e3a1f05f8dadca2094eb13090f8e9578d01c50037c02ab5c59ecfa4 +flutter_bootstrap.js,1751119898791,6ae3e6cabe64d6cf6898d9078a97870c9d063e319f676117b733a63fa33f64d4 +assets/FontManifest.json,1751119915042,9ea504185602e57d97b7c3517d382b8627a13c0181c490c96a9b55a5d5c8810c +assets/AssetManifest.bin.json,1751119915042,77c62ce3fd7c494bed363e766dde46e5e86faaa47e2b3239137f8cb4eb7c30fb +assets/AssetManifest.bin,1751119915042,3838b71b15de7f587ccaac0e708e90e9e5eae28b02e743b4291899f4b688bf44 +assets/shaders/ink_sparkle.frag,1751119915133,80c6e65c75f1de434b1b22dba61e96ad82dba0f2fc5e8b3b59c2def46d794354 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1751119915931,12bc6bf55aad4657f62747b6c1c9b5c120a594ed3540db21729b6db3c847340d +assets/fonts/MaterialIcons-Regular.otf,1751119915932,7dbdfa332b0b3a613a4ceb55012089f0e9c332b0e29f3cd23aaeee14d40633ff +assets/NOTICES,1751119915043,2423be738108ad3cecb49273868c0853d6b60024fc565d67e59a66196f6b3a57 +main.dart.js,1751119914745,30f90f85410130f8f9fd74c721fed3f89032dca57deb58d3be42884428292fda diff --git a/trainerbox/lib/screens/favorites_tab.dart b/trainerbox/lib/screens/favorites_tab.dart index 2373f4d..271b938 100644 --- a/trainerbox/lib/screens/favorites_tab.dart +++ b/trainerbox/lib/screens/favorites_tab.dart @@ -20,6 +20,27 @@ class FavoritesTab extends StatefulWidget { class _FavoritesTabState extends State { List get _categories => kTrainingCategories.map((c) => c.name).toList(); String? _selectedCategory; + // Level categories for filter + final List _levelCategories = [ + 'Bambini', + 'F-Jugend', + 'E-Jugend', + 'D-Jugend', + 'C-Jugend', + 'B-Jugend', + 'A-Jugend', + 'Herren', + ]; + String? _selectedLevel; + // Filter state variables for duration and rating + double _minDuration = 0; // Minimum duration filter + double _maxDuration = 120; // Maximum duration filter + double _minRating = 0.0; // Minimum rating filter + // Temporary filter state for modal + double _tempMinDuration = 0; + double _tempMaxDuration = 120; + String? _tempSelectedLevel; + double _tempMinRating = 0.0; @override void initState() { @@ -27,6 +48,129 @@ class _FavoritesTabState extends State { _selectedCategory = widget.categoryFilter; } + /// Opens the filter modal for advanced filtering (duration, level, rating). + void _openFilterModal() async { + _tempMinDuration = _minDuration; + _tempMaxDuration = _maxDuration; + _tempSelectedLevel = _selectedLevel; + _tempMinRating = _minRating; + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + left: 16, + right: 16, + top: 24, + ), + child: StatefulBuilder( + builder: (context, setModalState) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Filter', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + // Duration filter (RangeSlider) + Row( + children: [ + const Text('Dauer:'), + Expanded( + child: RangeSlider( + values: RangeValues(_tempMinDuration, _tempMaxDuration), + min: 0, + max: 300, + divisions: 30, + labels: RangeLabels('${_tempMinDuration.toInt()} min', '${_tempMaxDuration.toInt()} min'), + onChanged: (values) { + setModalState(() { + _tempMinDuration = values.start; + _tempMaxDuration = values.end; + }); + }, + ), + ), + ], + ), + // Level filter (Dropdown) + Row( + children: [ + const Text('Level:'), + const SizedBox(width: 8), + Expanded( + child: DropdownButton( + value: _tempSelectedLevel, + hint: const Text('Alle'), + items: [ + const DropdownMenuItem(value: null, child: Text('Alle')), + ..._levelCategories.map((y) => DropdownMenuItem(value: y, child: Text(y))), + ], + onChanged: (value) { + setModalState(() { + _tempSelectedLevel = value; + }); + }, + ), + ), + ], + ), + // Minimum rating filter (Slider) + Row( + children: [ + const Text('Mindestbewertung:'), + Expanded( + child: Slider( + value: _tempMinRating, + min: 0, + max: 5, + divisions: 10, + label: _tempMinRating.toStringAsFixed(1), + onChanged: (value) { + setModalState(() { + _tempMinRating = value; + }); + }, + ), + ), + Text(_tempMinRating.toStringAsFixed(1)), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Abbrechen'), + ), + ElevatedButton( + onPressed: () { + setState(() { + _minDuration = _tempMinDuration; + _maxDuration = _tempMaxDuration; + _selectedLevel = _tempSelectedLevel; + _minRating = _tempMinRating; + }); + Navigator.pop(context); + }, + child: const Text('Anwenden'), + ), + ], + ), + const SizedBox(height: 8), + ], + ); + }, + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { final user = FirebaseAuth.instance.currentUser; @@ -41,28 +185,54 @@ class _FavoritesTabState extends State { body: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Category filter chips + // Category filter chips and filter button Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 8, - runSpacing: 8, + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - FilterChip( - label: const Text('Alle'), - selected: _selectedCategory == null, - onSelected: (selected) { - setState(() => _selectedCategory = null); - }, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Kategorien', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + // Filter button opens the modal with advanced filters + ElevatedButton.icon( + onPressed: _openFilterModal, + icon: const Icon(Icons.filter_list), + label: const Text('Filter'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + textStyle: const TextStyle(fontSize: 14), + ), + ), + ], + ), + const SizedBox(height: 16), + // Category filter chips + Wrap( + alignment: WrapAlignment.center, + 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); + }, + )), + ], ), - ..._categories.map((cat) => FilterChip( - label: Text(cat), - selected: _selectedCategory == cat, - onSelected: (selected) { - setState(() => _selectedCategory = selected ? cat : null); - }, - )), ], ), ), @@ -96,12 +266,18 @@ class _FavoritesTabState extends State { return const Center(child: Text('Favoriten konnten nicht geladen werden')); } - // Filter favorites by selected category + // Filter favorites by selected category and filter options final filteredFavorites = multiSnapshot.data!.where((doc) { if (!doc.exists) return false; - if (_selectedCategory == null) return true; final trainingData = doc.data() as Map; - return trainingData['category'] == _selectedCategory; + final duration = (trainingData['duration'] as num?)?.toInt() ?? 0; + final rating = (trainingData['rating overall'] as num?)?.toDouble() ?? 0.0; + final matchesCategory = _selectedCategory == null || trainingData['category'] == _selectedCategory; + final matchesLevel = _selectedLevel == null || trainingData['year'] == _selectedLevel; + final matchesDuration = duration >= _minDuration && duration <= _maxDuration; + final matchesRating = rating >= _minRating; + // Only show favorites matching all selected filters + return matchesCategory && matchesLevel && matchesDuration && matchesRating; }).toList(); if (filteredFavorites.isEmpty) { diff --git a/trainerbox/lib/screens/search_tab.dart b/trainerbox/lib/screens/search_tab.dart index 774dc9f..88f16b1 100644 --- a/trainerbox/lib/screens/search_tab.dart +++ b/trainerbox/lib/screens/search_tab.dart @@ -39,6 +39,27 @@ class _SearchTabState extends State { bool _trainerChecked = false; // Set of favorite exercise IDs. Set _favorites = {}; + // Filter state variables + double _minDuration = 0; + double _maxDuration = 120; + String? _selectedYear; + double _minRating = 0.0; + // Temporary filter state for modal + double _tempMinDuration = 0; + double _tempMaxDuration = 120; + String? _tempSelectedYear; + double _tempMinRating = 0.0; + // Level categories for filter + final List _levelCategories = [ + 'Bambini', + 'F-Jugend', + 'E-Jugend', + 'D-Jugend', + 'C-Jugend', + 'B-Jugend', + 'A-Jugend', + 'Herren', + ]; @override void initState() { @@ -101,6 +122,129 @@ class _SearchTabState extends State { await _loadFavorites(); // Update favorites after toggling } + void _openFilterModal() async { + // Set temp values to current filter state + _tempMinDuration = _minDuration; + _tempMaxDuration = _maxDuration; + _tempSelectedYear = _selectedYear; + _tempMinRating = _minRating; + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + left: 16, + right: 16, + top: 24, + ), + child: StatefulBuilder( + builder: (context, setModalState) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Filter', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + // Duration filter + Row( + children: [ + const Text('Dauer:'), + Expanded( + child: RangeSlider( + values: RangeValues(_tempMinDuration, _tempMaxDuration), + min: 0, + max: 300, + divisions: 30, + labels: RangeLabels('${_tempMinDuration.toInt()} min', '${_tempMaxDuration.toInt()} min'), + onChanged: (values) { + setModalState(() { + _tempMinDuration = values.start; + _tempMaxDuration = values.end; + }); + }, + ), + ), + ], + ), + // Year/Level filter + Row( + children: [ + const Text('Level:'), + const SizedBox(width: 8), + Expanded( + child: DropdownButton( + value: _tempSelectedYear, + hint: const Text('Alle'), + items: [ + const DropdownMenuItem(value: null, child: Text('Alle')), + ..._levelCategories.map((y) => DropdownMenuItem(value: y, child: Text(y))), + ], + onChanged: (value) { + setModalState(() { + _tempSelectedYear = value; + }); + }, + ), + ), + ], + ), + // Minimum rating filter + Row( + children: [ + const Text('Mindestbewertung:'), + Expanded( + child: Slider( + value: _tempMinRating, + min: 0, + max: 5, + divisions: 10, + label: _tempMinRating.toStringAsFixed(1), + onChanged: (value) { + setModalState(() { + _tempMinRating = value; + }); + }, + ), + ), + Text(_tempMinRating.toStringAsFixed(1)), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Abbrechen'), + ), + ElevatedButton( + onPressed: () { + setState(() { + _minDuration = _tempMinDuration; + _maxDuration = _tempMaxDuration; + _selectedYear = _tempSelectedYear; + _minRating = _tempMinRating; + }); + Navigator.pop(context); + }, + child: const Text('Anwenden'), + ), + ], + ), + const SizedBox(height: 8), + ], + ); + }, + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -146,15 +290,29 @@ class _SearchTabState extends State { ), ), ), - // Category filter chips. + // Category filter chips and filter button only (no direct filter UI here) Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Kategorien', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Kategorien', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ElevatedButton.icon( + onPressed: _openFilterModal, + icon: const Icon(Icons.filter_list), + label: const Text('Filter'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + textStyle: const TextStyle(fontSize: 14), + ), + ), + ], ), const SizedBox(height: 16), Wrap( @@ -198,12 +356,16 @@ class _SearchTabState extends State { final title = data['title']?.toString().toLowerCase() ?? ''; final description = data['description']?.toString().toLowerCase() ?? ''; final category = data['category']?.toString() ?? ''; + final duration = (data['duration'] as num?)?.toInt() ?? 0; + final year = data['year']?.toString() ?? ''; + final rating = (data['rating overall'] as num?)?.toDouble() ?? 0.0; final searchTerm = _searchTerm.toLowerCase(); - final matchesSearch = title.contains(searchTerm) || description.contains(searchTerm); final matchesCategory = _selectedCategory == null || category == _selectedCategory; - - return matchesSearch && matchesCategory; + final matchesDuration = duration >= _minDuration && duration <= _maxDuration; + final matchesYear = _selectedYear == null || _selectedYear == '' || year == _selectedYear; + final matchesRating = rating >= _minRating; + return matchesSearch && matchesCategory && matchesDuration && matchesYear && matchesRating; }).toList(); if (exercises.isEmpty) { @@ -369,6 +531,17 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> { File? _imageFile; // Image picker instance. final _picker = ImagePicker(); + // Level categories for dropdown + final List _levelCategories = [ + 'Bambini', + 'F-Jugend', + 'E-Jugend', + 'D-Jugend', + 'C-Jugend', + 'B-Jugend', + 'A-Jugend', + 'Herren', + ]; /// Opens the image picker to select an image from the gallery. Future _pickImage() async { @@ -452,10 +625,14 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> { onChanged: (v) => _duration = int.tryParse(v), validator: (v) => v == null || int.tryParse(v) == null ? 'Zahl angeben' : null, ), - TextFormField( - decoration: const InputDecoration(labelText: 'Schwierigkeitslevel'), - onChanged: (v) => _year = v, - validator: (v) => v == null || v.isEmpty ? 'Level angeben' : null, + DropdownButtonFormField( + value: _year, + items: _levelCategories + .map((level) => DropdownMenuItem(value: level, child: Text(level))) + .toList(), + onChanged: (v) => setState(() => _year = v), + decoration: const InputDecoration(labelText: 'Level'), + validator: (v) => v == null ? 'Level angeben' : null, ), ], ),