Commented Code
parent
8c0d3676e9
commit
9a8561e45e
|
@ -1,5 +1,7 @@
|
|||
// main.dart
|
||||
// Einstiegspunkt der App und globale Konfigurationen
|
||||
// Entry point of the app and global configuration.
|
||||
// This file initializes Firebase, sets up localization, and launches the main app widget.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:trainerbox/firebase_options.dart';
|
||||
|
@ -8,15 +10,21 @@ import 'screens/login_screen.dart';
|
|||
import 'screens/search_tab.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
||||
/// Main entry point for the Flutter application.
|
||||
void main() async {
|
||||
// Ensures that widget binding is initialized before using platform channels.
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// Initialize Firebase with platform-specific options.
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
// Initialize date formatting for German locale.
|
||||
await initializeDateFormatting('de_DE', null);
|
||||
// Start the app.
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
/// The root widget of the application.
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
|
@ -24,15 +32,18 @@ class MyApp extends StatefulWidget {
|
|||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
/// State for the main app widget, manages login state and routing.
|
||||
class _MyAppState extends State<MyApp> {
|
||||
bool _loggedIn = false;
|
||||
|
||||
/// Called when login is successful.
|
||||
void _handleLoginSuccess() {
|
||||
setState(() {
|
||||
_loggedIn = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Called when logout is successful.
|
||||
void _handleLogoutSuccess() {
|
||||
setState(() {
|
||||
_loggedIn = false;
|
||||
|
@ -48,9 +59,11 @@ class _MyAppState extends State<MyApp> {
|
|||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
),
|
||||
// Show HomeScreen if logged in, otherwise show LoginScreen.
|
||||
home: _loggedIn
|
||||
? HomeScreen(onLogoutSuccess: _handleLogoutSuccess)
|
||||
: LoginScreen(onLoginSuccess: _handleLoginSuccess),
|
||||
// Define named routes for navigation.
|
||||
routes: {
|
||||
'/search': (context) => SearchTab(
|
||||
selectMode: (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?)?['selectMode'] ?? false,
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
// models/exercise.dart
|
||||
// Data model for a training exercise
|
||||
// Datenmodell für eine Trainingsübung
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Represents a training exercise with a title, category, and icon.
|
||||
/// Stellt eine Trainingsübung mit Titel, Kategorie und Icon dar.
|
||||
class Exercise {
|
||||
final String title; // Name der Übung
|
||||
final String category; // Kategorie der Übung
|
||||
final IconData icon; // Icon zur Darstellung
|
||||
/// The name/title of the exercise.
|
||||
// Name der Übung
|
||||
final String title;
|
||||
/// The category of the exercise.
|
||||
// Kategorie der Übung
|
||||
final String category;
|
||||
/// The icon used to visually represent the exercise.
|
||||
// Icon zur Darstellung
|
||||
final IconData icon;
|
||||
|
||||
/// Creates an Exercise instance.
|
||||
/// Erstellt eine neue Instanz einer Übung.
|
||||
Exercise({
|
||||
required this.title,
|
||||
required this.category,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// calendar_tab.dart
|
||||
// This file contains the CalendarTab widget, which displays a calendar view of trainings and allows users to view, add, and manage training events.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
@ -5,6 +8,7 @@ import 'package:firebase_auth/firebase_auth.dart';
|
|||
import 'training_detail_screen.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Mapping of training categories to colors for calendar display.
|
||||
const Map<String, Color> categoryColors = {
|
||||
'Aufwärmen & Mobilisation': Colors.deepOrange,
|
||||
'Wurf- & Torabschluss': Colors.orange,
|
||||
|
@ -14,6 +18,7 @@ const Map<String, Color> categoryColors = {
|
|||
'Koordination': Colors.teal,
|
||||
};
|
||||
|
||||
/// The CalendarTab displays a calendar with all training events for the user.
|
||||
class CalendarTab extends StatefulWidget {
|
||||
final DateTime? initialDate;
|
||||
const CalendarTab({super.key, this.initialDate});
|
||||
|
@ -22,6 +27,7 @@ class CalendarTab extends StatefulWidget {
|
|||
State<CalendarTab> createState() => _CalendarTabState();
|
||||
}
|
||||
|
||||
/// State for the CalendarTab, manages event loading, calendar state, and user interactions.
|
||||
class _CalendarTabState extends State<CalendarTab> {
|
||||
CalendarFormat _calendarFormat = CalendarFormat.week;
|
||||
late DateTime _focusedDay;
|
||||
|
@ -60,6 +66,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Initializes user data and loads events for the calendar.
|
||||
Future<void> _initializeData() async {
|
||||
if (_currentUserId != null) {
|
||||
final userDoc = await FirebaseFirestore.instance
|
||||
|
@ -76,6 +83,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Loads all training events for the current user (trainer or player).
|
||||
Future<void> _loadEvents() async {
|
||||
if (_userRole == null) return;
|
||||
|
||||
|
@ -84,12 +92,14 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
QuerySnapshot trainersSnapshot;
|
||||
|
||||
if (_userRole == 'trainer') {
|
||||
// Trainer: only their own trainings
|
||||
trainersSnapshot = await FirebaseFirestore.instance
|
||||
.collection('User')
|
||||
.where('role', isEqualTo: 'trainer')
|
||||
.where(FieldPath.documentId, isEqualTo: _currentUserId)
|
||||
.get();
|
||||
} else {
|
||||
// Player: trainings from trainers in their club
|
||||
final userDoc = await FirebaseFirestore.instance
|
||||
.collection('User')
|
||||
.doc(_currentUserId)
|
||||
|
@ -127,7 +137,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
|
||||
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// Lade alle Trainings für das Datum
|
||||
// Load all specific trainings for each date
|
||||
trainings.forEach((dateString, trainingsList) {
|
||||
final date = DateTime.tryParse(dateString);
|
||||
if (date == null) return;
|
||||
|
@ -144,6 +154,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
return sum;
|
||||
});
|
||||
|
||||
// Build the event map for this training
|
||||
final event = {
|
||||
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
|
||||
'time': timeStr,
|
||||
|
@ -165,7 +176,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
}
|
||||
});
|
||||
|
||||
// Füge regelmäßige Trainings hinzu
|
||||
// Add recurring weekly trainings for the year
|
||||
final now = DateTime.now();
|
||||
final yearStart = DateTime(now.year, 1, 1);
|
||||
final yearEnd = DateTime(now.year + 1, 1, 1);
|
||||
|
@ -179,7 +190,7 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
'Sonntag': 7,
|
||||
};
|
||||
|
||||
// Erstelle eine Set von gecancelten Daten für schnelleren Zugriff
|
||||
// Create a set of cancelled dates for fast lookup
|
||||
final cancelledDates = cancelledTrainings
|
||||
.where((cancelled) => cancelled is Map<String, dynamic>)
|
||||
.map((cancelled) => DateTime.parse((cancelled as Map<String, dynamic>)['date'] as String))
|
||||
|
@ -195,23 +206,23 @@ class _CalendarTabState extends State<CalendarTab> {
|
|||
final duration = trainingDurations[day] ?? 60;
|
||||
final targetWeekday = weekdays[day] ?? 1;
|
||||
|
||||
// Finde das erste gewünschte Wochentags-Datum ab Jahresanfang
|
||||
// Find the first desired weekday date from the start of the year
|
||||
DateTime firstDate = yearStart;
|
||||
while (firstDate.weekday != targetWeekday) {
|
||||
firstDate = firstDate.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
// Generiere Trainings für das gesamte Jahr
|
||||
// Generate weekly trainings for the whole year
|
||||
while (firstDate.isBefore(yearEnd)) {
|
||||
final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day);
|
||||
final dateString = normalizedDate.toIso8601String();
|
||||
|
||||
// Prüfe, ob das Training für dieses Datum gecancelt ist
|
||||
// Check if the training for this date is cancelled
|
||||
final isCancelled = cancelledDates.any((cancelledDate) =>
|
||||
isSameDay(cancelledDate, normalizedDate)
|
||||
);
|
||||
|
||||
// Prüfe, ob bereits ein spezifisches Training für dieses Datum existiert
|
||||
// Check if a specific training already exists for this date
|
||||
final hasSpecificTraining = trainings.containsKey(dateString);
|
||||
|
||||
if (!isCancelled && !hasSpecificTraining) {
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
// favorites_tab.dart
|
||||
// This file contains the FavoritesTab widget, which displays the user's favorite exercises and allows filtering by category.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'training_detail_screen.dart';
|
||||
|
||||
/// The FavoritesTab displays the user's favorite exercises and allows filtering by category.
|
||||
class FavoritesTab extends StatefulWidget {
|
||||
final String? categoryFilter;
|
||||
const FavoritesTab({super.key, this.categoryFilter});
|
||||
|
@ -11,6 +15,7 @@ class FavoritesTab extends StatefulWidget {
|
|||
State<FavoritesTab> createState() => _FavoritesTabState();
|
||||
}
|
||||
|
||||
/// State for the FavoritesTab, manages category selection and favorite loading.
|
||||
class _FavoritesTabState extends State<FavoritesTab> {
|
||||
static const List<String> _categories = [
|
||||
'Aufwärmen & Mobilisation',
|
||||
|
@ -42,7 +47,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
|
|||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Filter-Chip-Leiste
|
||||
// Category filter chips
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Wrap(
|
||||
|
@ -84,7 +89,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
|
|||
return const Center(child: Text('Keine Favoriten gefunden'));
|
||||
}
|
||||
|
||||
// Lade alle Favoriten-Dokumente auf einmal
|
||||
// Load all favorite exercise documents at once
|
||||
return FutureBuilder<List<DocumentSnapshot>>(
|
||||
future: Future.wait(allFavorites.map((id) =>
|
||||
FirebaseFirestore.instance.collection('Training').doc(id).get()
|
||||
|
@ -97,7 +102,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
|
|||
return const Center(child: Text('Favoriten konnten nicht geladen werden'));
|
||||
}
|
||||
|
||||
// Filtere die Favoriten basierend auf der Kategorie
|
||||
// Filter favorites by selected category
|
||||
final filteredFavorites = multiSnapshot.data!.where((doc) {
|
||||
if (!doc.exists) return false;
|
||||
if (_selectedCategory == null) return true;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// screens/home_screen.dart
|
||||
// Enthält die BottomNavigationBar-Logik und Navigation zwischen den Hauptscreens
|
||||
// This file contains the main HomeScreen widget, which manages the bottom navigation bar and navigation between the main screens of the app.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
@ -10,6 +11,7 @@ import 'calendar_tab.dart';
|
|||
import 'profile_tab.dart';
|
||||
import 'training_detail_screen.dart';
|
||||
|
||||
/// The main home screen of the app, containing the bottom navigation bar and all main tabs.
|
||||
class HomeScreen extends StatefulWidget {
|
||||
final VoidCallback? onLogoutSuccess;
|
||||
const HomeScreen({super.key, this.onLogoutSuccess});
|
||||
|
@ -18,6 +20,7 @@ class HomeScreen extends StatefulWidget {
|
|||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
/// State for the HomeScreen, manages navigation and data loading for the home tab.
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _selectedIndex = 0;
|
||||
Map<String, dynamic>? _nextTraining;
|
||||
|
@ -35,6 +38,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
_loadSuggestions();
|
||||
}
|
||||
|
||||
/// Loads exercise suggestions for the user based on unused and highly rated exercises.
|
||||
Future<void> _loadSuggestions() async {
|
||||
setState(() {
|
||||
_isLoadingSuggestions = true;
|
||||
|
@ -48,7 +52,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
return;
|
||||
}
|
||||
|
||||
// 1. Hole alle vom User genutzten Übungs-IDs
|
||||
// 1. Get all exercise IDs used by the user
|
||||
final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
|
||||
final Set<String> usedExerciseIds = {};
|
||||
if (userDoc.exists) {
|
||||
|
@ -67,14 +71,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
// 2. Hole alle Übungen
|
||||
// 2. Get all exercises
|
||||
final exercisesSnapshot = await FirebaseFirestore.instance.collection('Training').get();
|
||||
|
||||
final allExercises = exercisesSnapshot.docs.map((doc) {
|
||||
return {'id': doc.id, ...doc.data() as Map<String, dynamic>};
|
||||
}).toList();
|
||||
|
||||
// 3. Filtere nach "nicht genutzt" und "gut bewertet"
|
||||
// 3. Filter for unused and highly rated exercises
|
||||
final suggestions = allExercises.where((exercise) {
|
||||
final isUsed = usedExerciseIds.contains(exercise['id']);
|
||||
final rating = (exercise['rating overall'] as num?)?.toDouble() ?? 0.0;
|
||||
|
@ -91,11 +95,12 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingSuggestions = false;
|
||||
_suggestionsError = 'Fehler beim Laden.';
|
||||
_suggestionsError = 'Error loading suggestions.';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the next training event for the user, depending on their role and club.
|
||||
Future<void> _loadNextTraining() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
|
@ -115,14 +120,14 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
|
||||
QuerySnapshot trainersSnapshot;
|
||||
if (userRole == 'trainer') {
|
||||
// Trainer sieht nur seine eigenen Trainings
|
||||
// Trainer sees only their own trainings
|
||||
trainersSnapshot = await FirebaseFirestore.instance
|
||||
.collection('User')
|
||||
.where('role', isEqualTo: 'trainer')
|
||||
.where(FieldPath.documentId, isEqualTo: user.uid)
|
||||
.get();
|
||||
} else {
|
||||
// Spieler sieht nur Trainings von Trainern seines Vereins
|
||||
// Player sees only trainings from trainers in their club
|
||||
if (userClub == null || userClub.isEmpty) {
|
||||
setState(() {
|
||||
_nextTraining = null;
|
||||
|
@ -160,7 +165,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
final daysUntilNext = _getDaysUntilNext(day, now.weekday);
|
||||
final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute);
|
||||
|
||||
// Prüfe die nächsten 4 Wochen
|
||||
// Check the next 4 weeks
|
||||
for (var i = 0; i < 4; i++) {
|
||||
final date = eventDate.add(Duration(days: i * 7));
|
||||
final normalizedDate = DateTime(date.year, date.month, date.day);
|
||||
|
@ -180,7 +185,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
'date': dateString,
|
||||
'time': timeStr,
|
||||
'duration': trainingDurations[day] ?? 60,
|
||||
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
|
||||
'trainerName': trainerData['name'] ?? 'Unknown Trainer',
|
||||
'day': day,
|
||||
};
|
||||
}
|
||||
|
@ -200,6 +205,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the number of days until the next occurrence of a given weekday.
|
||||
int _getDaysUntilNext(String day, int currentWeekday) {
|
||||
final weekdays = {
|
||||
'Montag': 1,
|
||||
|
@ -219,22 +225,25 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
return daysUntilNext;
|
||||
}
|
||||
|
||||
/// Checks if two DateTime objects represent the same day.
|
||||
bool isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||
}
|
||||
|
||||
/// Handles navigation bar item taps and reloads suggestions if needed.
|
||||
void _onItemTapped(int index) {
|
||||
if (_selectedIndex != index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
// Wenn zum Home-Tab gewechselt wird, lade die Vorschläge neu
|
||||
// Reload suggestions when switching to the Home tab
|
||||
if (index == 0) {
|
||||
_loadSuggestions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of main screens for the bottom navigation bar.
|
||||
List<Widget> get _screens => [
|
||||
_buildHomeTab(),
|
||||
const SearchTab(),
|
||||
|
@ -271,6 +280,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
/// Builds the Home tab content, including next training, favorites, and suggestions.
|
||||
Widget _buildHomeTab() {
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
|
@ -440,6 +450,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
/// Builds a favorite category circle with icon and label.
|
||||
Widget _buildFavoriteCircle(String label, IconData icon, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
|
@ -474,6 +485,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
/// Builds a suggestion card for an exercise.
|
||||
Widget _buildSuggestionCard(Map<String, dynamic> exercise) {
|
||||
final category = exercise['category'] as String? ?? 'Sonstiges';
|
||||
final title = exercise['title'] as String? ?? 'Unbekannte Übung';
|
||||
|
@ -553,6 +565,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Mapping of category names to icons.
|
||||
const Map<String, IconData> categoryIcons = {
|
||||
'Aufwärmen & Mobilisation': Icons.directions_run,
|
||||
'Wurf- & Torabschluss': Icons.sports_handball,
|
||||
|
@ -562,6 +575,7 @@ const Map<String, IconData> categoryIcons = {
|
|||
'Koordination': Icons.directions_walk,
|
||||
};
|
||||
|
||||
/// Mapping of category names to colors.
|
||||
const Map<String, Color> categoryColors = {
|
||||
'Aufwärmen & Mobilisation': Colors.deepOrange,
|
||||
'Wurf- & Torabschluss': Colors.orange,
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
// home_tab.dart
|
||||
// This file contains the HomeTab widget, which displays a welcome message, a featured image, favorite categories, and exercise suggestions.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/category_circle.dart';
|
||||
import '../widgets/exercise_card.dart';
|
||||
|
||||
/// The HomeTab displays a welcome message, featured image, favorite categories, and exercise suggestions.
|
||||
class HomeTab extends StatelessWidget {
|
||||
const HomeTab({super.key});
|
||||
|
||||
|
@ -13,7 +17,7 @@ class HomeTab extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Begrüßung
|
||||
// Welcome message
|
||||
const Center(
|
||||
child: Text(
|
||||
'Hallo Trainer!',
|
||||
|
@ -25,7 +29,7 @@ class HomeTab extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Bild mit Titel
|
||||
// Featured image with title overlay
|
||||
Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
|
@ -52,7 +56,7 @@ class HomeTab extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Favoriten Kategorien
|
||||
// Favorite categories section
|
||||
const Text(
|
||||
'Favoriten',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
|
@ -77,7 +81,7 @@ class HomeTab extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Vorschläge
|
||||
// Suggestions section
|
||||
const Text(
|
||||
'Vorschläge',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
|
@ -88,6 +92,7 @@ class HomeTab extends StatelessWidget {
|
|||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// PageView with exercise suggestion cards
|
||||
PageView(
|
||||
children: const [
|
||||
ExerciseCard(
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
// login_screen.dart
|
||||
// This file contains the LoginScreen widget, which handles user authentication (login and registration) and role selection.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
/// The LoginScreen allows users to log in or register, and select their role (trainer or player).
|
||||
class LoginScreen extends StatefulWidget {
|
||||
final void Function() onLoginSuccess;
|
||||
const LoginScreen({super.key, required this.onLoginSuccess});
|
||||
|
@ -10,6 +14,7 @@ class LoginScreen extends StatefulWidget {
|
|||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
/// State for the LoginScreen, manages form state, authentication, and error handling.
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
|
@ -20,6 +25,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
bool _isLogin = true;
|
||||
bool _isTrainer = false;
|
||||
|
||||
/// Handles login or registration when the form is submitted.
|
||||
Future<void> _submit() async {
|
||||
setState(() { _loading = true; _error = null; });
|
||||
try {
|
||||
|
@ -31,31 +37,29 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
// Firestore-Check
|
||||
// Check if user profile exists in Firestore
|
||||
final uid = cred.user!.uid;
|
||||
|
||||
final userDoc = await FirebaseFirestore.instance.collection('User').doc(uid).get();
|
||||
|
||||
if (userDoc.exists) {
|
||||
widget.onLoginSuccess();
|
||||
} else {
|
||||
setState(() { _error = 'Kein Benutzerprofil in der Datenbank gefunden!'; });
|
||||
setState(() { _error = 'No user profile found in the database!'; });
|
||||
await FirebaseAuth.instance.signOut();
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
// Registrierung
|
||||
// Registration
|
||||
try {
|
||||
UserCredential cred = await FirebaseAuth.instance.createUserWithEmailAndPassword(
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text.trim(),
|
||||
);
|
||||
|
||||
// User-Datensatz in Firestore anlegen
|
||||
// Create user document in Firestore
|
||||
final uid = cred.user!.uid;
|
||||
|
||||
await FirebaseFirestore.instance.collection('User').doc(uid).set({
|
||||
'email': _emailController.text.trim(),
|
||||
'name': _nameController.text.trim(),
|
||||
|
@ -69,6 +73,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
}
|
||||
}
|
||||
} on FirebaseAuthException catch (e) {
|
||||
// Handle Firebase authentication errors
|
||||
String errorMessage;
|
||||
switch (e.code) {
|
||||
case 'user-not-found':
|
||||
|
@ -81,7 +86,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
errorMessage = 'Ungültige E-Mail-Adresse.';
|
||||
break;
|
||||
case 'user-disabled':
|
||||
errorMessage = 'Dieser Account wurde deaktiviert.';
|
||||
errorMessage = 'Dieses Konto wurde deaktiviert.';
|
||||
break;
|
||||
case 'email-already-in-use':
|
||||
errorMessage = 'Diese E-Mail-Adresse wird bereits verwendet.';
|
||||
|
@ -90,14 +95,14 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
errorMessage = 'Das Passwort ist zu schwach.';
|
||||
break;
|
||||
case 'operation-not-allowed':
|
||||
errorMessage = 'Diese Operation ist nicht erlaubt.';
|
||||
errorMessage = 'Diese Aktion ist nicht erlaubt.';
|
||||
break;
|
||||
default:
|
||||
errorMessage = 'Ein Fehler ist aufgetreten: ${e.message}';
|
||||
}
|
||||
setState(() { _error = errorMessage; });
|
||||
} catch (e) {
|
||||
setState(() { _error = 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.'; });
|
||||
setState(() { _error = 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.'; });
|
||||
} finally {
|
||||
setState(() { _loading = false; });
|
||||
}
|
||||
|
@ -114,15 +119,18 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_isLogin ? 'Login' : 'Registrieren', style: Theme.of(context).textTheme.headlineMedium),
|
||||
// Title
|
||||
Text(_isLogin ? 'Anmeldung' : 'Registrieren', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: 32),
|
||||
if (!_isLogin) ...[
|
||||
// Name field for registration
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
validator: (v) => v != null && v.trim().isNotEmpty ? null : 'Name angeben',
|
||||
validator: (v) => v != null && v.trim().isNotEmpty ? null : 'Bitte gib deinen Namen ein',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Role selection
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
|
@ -144,24 +152,28 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// Email field
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(labelText: 'E-Mail'),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (v) => v != null && v.contains('@') ? null : 'Gib eine gültige E-Mail ein',
|
||||
validator: (v) => v != null && v.contains('@') ? null : 'Bitte gib eine gültige E-Mail ein',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Password field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(labelText: 'Passwort'),
|
||||
obscureText: true,
|
||||
validator: (v) => v != null && v.length >= 6 ? null : 'Mind. 6 Zeichen',
|
||||
validator: (v) => v != null && v.length >= 6 ? null : 'Mindestens 6 Zeichen',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_error != null) ...[
|
||||
// Error message
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
// Submit button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
|
@ -174,10 +186,11 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
},
|
||||
child: _loading
|
||||
? const CircularProgressIndicator()
|
||||
: Text(_isLogin ? 'Login' : 'Registrieren'),
|
||||
: Text(_isLogin ? 'Anmelden' : 'Registrieren'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Switch between login and registration
|
||||
TextButton(
|
||||
onPressed: _loading
|
||||
? null
|
||||
|
@ -187,7 +200,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
_error = null;
|
||||
});
|
||||
},
|
||||
child: Text(_isLogin ? 'Noch keinen Account? Jetzt registrieren!' : 'Schon registriert? Jetzt einloggen!'),
|
||||
child: Text(_isLogin ? 'Noch kein Konto? Jetzt registrieren!' : 'Schon registriert? Jetzt anmelden!'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -3,7 +3,12 @@ import 'package:firebase_auth/firebase_auth.dart';
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// profile_tab.dart
|
||||
// This file contains the ProfileTab widget, which allows users to view and edit their profile information, including training times and durations. Trainers have additional options for managing training schedules.
|
||||
|
||||
/// The ProfileTab displays and allows editing of user profile information.
|
||||
class ProfileTab extends StatefulWidget {
|
||||
/// Callback for when the user logs out successfully.
|
||||
final VoidCallback? onLogoutSuccess;
|
||||
const ProfileTab({super.key, this.onLogoutSuccess});
|
||||
|
||||
|
@ -12,23 +17,35 @@ class ProfileTab extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ProfileTabState extends State<ProfileTab> {
|
||||
// Form key for validating the profile form.
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
// Indicates if the user is a trainer.
|
||||
bool _isTrainer = false;
|
||||
// Indicates if data is currently loading.
|
||||
bool _isLoading = false;
|
||||
// Stores training times for each day.
|
||||
Map<String, TimeOfDay?> _trainingTimes = {};
|
||||
// Stores training durations for each day.
|
||||
Map<String, int> _trainingDurations = {};
|
||||
// User's name.
|
||||
String _name = '';
|
||||
// User's email.
|
||||
String _email = '';
|
||||
// User's club.
|
||||
String _club = '';
|
||||
// User's role (trainer or player).
|
||||
String? _userRole;
|
||||
// Date the user joined.
|
||||
DateTime? _joinDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Load user data when the widget is initialized.
|
||||
_loadUserData();
|
||||
}
|
||||
|
||||
/// Loads user data from Firebase and updates the state.
|
||||
Future<void> _loadUserData() async {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
@ -41,6 +58,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
|
||||
final trainingDurations = data['trainingDurations'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// Convert training times from string to TimeOfDay.
|
||||
final convertedTrainingTimes = <String, TimeOfDay?>{};
|
||||
trainingTimes.forEach((key, value) {
|
||||
if (value != null) {
|
||||
|
@ -79,6 +97,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Saves the training time and duration for a specific day to Firebase.
|
||||
Future<void> _saveTrainingTime(String day, TimeOfDay? time, int? duration) async {
|
||||
if (time == null || duration == null) return;
|
||||
|
||||
|
@ -89,13 +108,13 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
|
||||
final timeString = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
// Update training times and durations
|
||||
// Update training times and durations in Firestore.
|
||||
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
|
||||
'trainingTimes.$day': timeString,
|
||||
'trainingDurations.$day': duration,
|
||||
});
|
||||
|
||||
// Trainings ab 1. Januar des aktuellen Jahres bis 52 Wochen in die Zukunft anlegen
|
||||
// Create training entries from the start of the year for 52 weeks ahead.
|
||||
final now = DateTime.now();
|
||||
final yearStart = DateTime(now.year, 1, 1);
|
||||
final weekdays = {
|
||||
|
@ -109,7 +128,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
};
|
||||
final targetWeekday = weekdays[day] ?? 1;
|
||||
|
||||
// Finde das erste gewünschte Wochentags-Datum ab Jahresanfang
|
||||
// Find the first occurrence of the target weekday from the start of the year.
|
||||
DateTime firstDate = yearStart;
|
||||
while (firstDate.weekday != targetWeekday) {
|
||||
firstDate = firstDate.add(const Duration(days: 1));
|
||||
|
@ -123,20 +142,20 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
newTrainingDates.add(dateString);
|
||||
}
|
||||
|
||||
// Load existing trainingExercises and cancelledTrainings
|
||||
// Load existing trainingExercises and cancelledTrainings from Firestore.
|
||||
final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
|
||||
final data = userDoc.data() ?? {};
|
||||
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
|
||||
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
|
||||
|
||||
// Add empty training only if not already present
|
||||
// Add empty training only if not already present.
|
||||
for (final dateString in newTrainingDates) {
|
||||
if (!trainingExercises.containsKey(dateString)) {
|
||||
trainingExercises[dateString] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove cancelledTrainings for these dates
|
||||
// Remove cancelledTrainings for these dates.
|
||||
cancelledTrainings.removeWhere((cancelled) =>
|
||||
cancelled is Map<String, dynamic> &&
|
||||
cancelled.containsKey('date') &&
|
||||
|
@ -171,6 +190,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Removes the training time and duration for a specific day from Firebase.
|
||||
Future<void> _removeTrainingTime(String day) async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
|
@ -205,6 +225,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Opens a time picker dialog for selecting a training time, then a dialog for duration.
|
||||
Future<void> _selectTime(BuildContext context, String day) async {
|
||||
final TimeOfDay? picked = await showTimePicker(
|
||||
context: context,
|
||||
|
@ -223,6 +244,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Saves the user's profile data (name, club, training times, durations) to Firebase.
|
||||
Future<void> _saveUserData() async {
|
||||
if (FirebaseAuth.instance.currentUser == null) return;
|
||||
|
||||
|
@ -268,6 +290,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
Widget build(BuildContext context) {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) {
|
||||
// Show message if user is not logged in.
|
||||
return const Center(child: Text('Nicht eingeloggt'));
|
||||
}
|
||||
|
||||
|
@ -278,6 +301,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: () async {
|
||||
// Log out the user and call the callback if provided.
|
||||
await FirebaseAuth.instance.signOut();
|
||||
if (widget.onLogoutSuccess != null) {
|
||||
widget.onLogoutSuccess!();
|
||||
|
@ -297,6 +321,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Card for personal information fields.
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
|
@ -312,6 +337,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Editable name field.
|
||||
TextField(
|
||||
controller: TextEditingController(text: _name),
|
||||
decoration: const InputDecoration(
|
||||
|
@ -322,6 +348,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
onChanged: (value) => _name = value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Non-editable email field.
|
||||
TextField(
|
||||
controller: TextEditingController(text: _email),
|
||||
decoration: const InputDecoration(
|
||||
|
@ -332,6 +359,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
enabled: false,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Editable club field.
|
||||
TextField(
|
||||
controller: TextEditingController(text: _club),
|
||||
decoration: const InputDecoration(
|
||||
|
@ -343,7 +371,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
onChanged: (value) => _club = value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Rolle
|
||||
// Non-editable role field.
|
||||
TextField(
|
||||
controller: TextEditingController(text: _userRole == 'trainer' ? 'Trainer' : 'Spieler'),
|
||||
decoration: const InputDecoration(
|
||||
|
@ -355,6 +383,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
),
|
||||
if (_joinDate != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
// Non-editable join date field.
|
||||
TextField(
|
||||
controller: TextEditingController(text: DateFormat('dd.MM.yyyy').format(_joinDate!)),
|
||||
decoration: const InputDecoration(
|
||||
|
@ -371,6 +400,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
),
|
||||
if (_userRole == 'trainer') ...[
|
||||
const SizedBox(height: 24),
|
||||
// Card for managing training times (visible only to trainers).
|
||||
Card(
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
|
@ -386,6 +416,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// List of training days with time and duration controls.
|
||||
...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
|
||||
.map((day) => Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
|
@ -400,6 +431,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Edit or add training time button.
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_trainingTimes[day] != null
|
||||
|
@ -411,6 +443,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
: () => _selectTime(context, day),
|
||||
),
|
||||
if (_trainingTimes[day] != null)
|
||||
// Remove training time button.
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: _isLoading
|
||||
|
@ -434,6 +467,7 @@ class _ProfileTabState extends State<ProfileTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Dialog for selecting the duration of a training session.
|
||||
class _DurationDialog extends StatefulWidget {
|
||||
final int initialDuration;
|
||||
|
||||
|
@ -449,6 +483,7 @@ class _DurationDialogState extends State<_DurationDialog> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize duration with the provided initial value.
|
||||
_duration = widget.initialDuration;
|
||||
}
|
||||
|
||||
|
@ -464,6 +499,7 @@ class _DurationDialogState extends State<_DurationDialog> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Decrement duration button (minimum 15 minutes).
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
|
@ -476,6 +512,7 @@ class _DurationDialogState extends State<_DurationDialog> {
|
|||
'$_duration Minuten',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
// Increment duration button (maximum 300 minutes).
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: _duration < 300
|
||||
|
@ -487,10 +524,12 @@ class _DurationDialogState extends State<_DurationDialog> {
|
|||
],
|
||||
),
|
||||
actions: [
|
||||
// Cancel button.
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
// Confirm button.
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, _duration),
|
||||
child: const Text('Bestätigen'),
|
||||
|
|
|
@ -6,8 +6,11 @@ import 'package:image_picker/image_picker.dart';
|
|||
import 'dart:io';
|
||||
import 'training_detail_screen.dart';
|
||||
|
||||
/// The SearchTab displays a searchable and filterable list of training exercises.
|
||||
class SearchTab extends StatefulWidget {
|
||||
/// If true, enables selection mode for choosing exercises.
|
||||
final bool selectMode;
|
||||
/// Remaining time for selection mode (optional).
|
||||
final int? remainingTime;
|
||||
|
||||
const SearchTab({
|
||||
|
@ -21,7 +24,9 @@ class SearchTab extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SearchTabState extends State<SearchTab> {
|
||||
// Controller for the search input field.
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
// List of available exercise categories.
|
||||
final List<String> _categories = [
|
||||
'Aufwärmen & Mobilisation',
|
||||
'Wurf- & Torabschluss',
|
||||
|
@ -30,15 +35,21 @@ class _SearchTabState extends State<SearchTab> {
|
|||
'Pass',
|
||||
'Koordination',
|
||||
];
|
||||
// Currently selected category for filtering.
|
||||
String? _selectedCategory;
|
||||
// Current search term entered by the user.
|
||||
String _searchTerm = '';
|
||||
// Indicates if the user is a trainer.
|
||||
bool _isTrainer = false;
|
||||
// Indicates if the trainer check has completed.
|
||||
bool _trainerChecked = false;
|
||||
// Set of favorite exercise IDs.
|
||||
Set<String> _favorites = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Listen for changes in the search field and update the search term.
|
||||
_searchController.addListener(() {
|
||||
setState(() {
|
||||
_searchTerm = _searchController.text.trim();
|
||||
|
@ -48,6 +59,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
_loadFavorites();
|
||||
}
|
||||
|
||||
/// Checks if the current user is a trainer and updates state.
|
||||
Future<void> _checkIfTrainer() async {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
@ -58,6 +70,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Loads the user's favorite exercises from Firestore.
|
||||
Future<void> _loadFavorites() async {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
@ -70,13 +83,15 @@ class _SearchTabState extends State<SearchTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Shows a dialog for creating a new training exercise (trainer only).
|
||||
void _showCreateTrainingDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _CreateTrainingDialog(categories: _categories),
|
||||
).then((_) => setState(() {})); // Refresh nach Hinzufügen
|
||||
).then((_) => setState(() {})); // Refresh after adding
|
||||
}
|
||||
|
||||
/// Toggles the favorite status of an exercise for the current user.
|
||||
Future<void> _toggleFavorite(String trainingId, bool isFavorite) async {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
@ -89,7 +104,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
'favorites': FieldValue.arrayUnion([trainingId]),
|
||||
});
|
||||
}
|
||||
await _loadFavorites(); // Aktualisiere die Favoriten nach dem Toggle
|
||||
await _loadFavorites(); // Update favorites after toggling
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -112,6 +127,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search input field.
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
|
@ -136,6 +152,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
),
|
||||
),
|
||||
),
|
||||
// Category filter chips.
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
|
@ -164,6 +181,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
],
|
||||
),
|
||||
),
|
||||
// Exercise grid view.
|
||||
Expanded(
|
||||
child: FutureBuilder<QuerySnapshot>(
|
||||
future: FirebaseFirestore.instance.collection('Training').get(),
|
||||
|
@ -180,6 +198,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
return const Center(child: Text('Keine Übungen gefunden'));
|
||||
}
|
||||
|
||||
// Filter exercises by search term and category.
|
||||
var exercises = snapshot.data!.docs.where((doc) {
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
final title = data['title']?.toString().toLowerCase() ?? '';
|
||||
|
@ -210,6 +229,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
final doc = exercises[index];
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
final duration = (data['duration'] as num?)?.toInt() ?? 0;
|
||||
// Disable selection if duration exceeds remaining time in select mode.
|
||||
final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0);
|
||||
|
||||
return Card(
|
||||
|
@ -220,6 +240,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
? null
|
||||
: () {
|
||||
if (widget.selectMode) {
|
||||
// Return selected exercise data in select mode.
|
||||
Navigator.pop(context, {
|
||||
'id': doc.id,
|
||||
'title': data['title']?.toString() ?? 'Unbekannte Übung',
|
||||
|
@ -227,6 +248,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
'duration': duration,
|
||||
});
|
||||
} else {
|
||||
// Navigate to exercise detail screen.
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
@ -293,6 +315,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
],
|
||||
),
|
||||
),
|
||||
// Favorite icon button (toggle favorite status).
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
|
@ -324,6 +347,7 @@ class _SearchTabState extends State<SearchTab> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Dialog for creating a new training exercise.
|
||||
class _CreateTrainingDialog extends StatefulWidget {
|
||||
final List<String> categories;
|
||||
const _CreateTrainingDialog({required this.categories});
|
||||
|
@ -333,16 +357,26 @@ class _CreateTrainingDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
|
||||
// Form key for validating the create training form.
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
// Selected category for the new training.
|
||||
String? _category;
|
||||
// Title of the new training.
|
||||
String? _title;
|
||||
// Description of the new training.
|
||||
String? _description;
|
||||
// Duration of the new training.
|
||||
int? _duration;
|
||||
// Difficulty level or year.
|
||||
String? _year;
|
||||
// Indicates if the dialog is loading (creating training).
|
||||
bool _loading = false;
|
||||
// Selected image file for the training.
|
||||
File? _imageFile;
|
||||
// Image picker instance.
|
||||
final _picker = ImagePicker();
|
||||
|
||||
/// Opens the image picker to select an image from the gallery.
|
||||
Future<void> _pickImage() async {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
|
||||
if (pickedFile != null) {
|
||||
|
@ -352,6 +386,7 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Uploads the selected image to Firebase Storage and returns the download URL.
|
||||
Future<String?> _uploadImage() async {
|
||||
if (_imageFile == null) return null;
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
|||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
/// The TrainingDetailScreen displays details and ratings for a specific training exercise.
|
||||
class TrainingDetailScreen extends StatefulWidget {
|
||||
/// The ID of the training exercise to display.
|
||||
final String trainingId;
|
||||
|
||||
const TrainingDetailScreen({super.key, required this.trainingId});
|
||||
|
@ -12,17 +14,23 @@ class TrainingDetailScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
||||
// The current user's rating for this exercise.
|
||||
double? _userRating;
|
||||
// Indicates if a loading operation is in progress.
|
||||
bool _isLoading = false;
|
||||
// Indicates if the current user is a player (not a trainer).
|
||||
bool _isPlayer = false;
|
||||
// Indicates if the user role check has completed.
|
||||
bool _userRoleChecked = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Check the user's role when the widget is initialized.
|
||||
_checkUserRole();
|
||||
}
|
||||
|
||||
/// Checks the current user's role (player or trainer) and updates state.
|
||||
Future<void> _checkUserRole() async {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
@ -40,6 +48,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Submits a rating for the current training exercise by the user.
|
||||
Future<void> _submitRating(double rating) async {
|
||||
if (!_isPlayer) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
@ -65,23 +74,23 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
final data = trainingDoc.data() as Map<String, dynamic>;
|
||||
List<dynamic> ratings = List<dynamic>.from(data['ratings'] ?? []);
|
||||
|
||||
// Entferne alte Bewertung des Users falls vorhanden
|
||||
// Remove old rating from this user if present.
|
||||
ratings.removeWhere((r) => r['userId'] == user.uid);
|
||||
|
||||
// Füge neue Bewertung hinzu
|
||||
// Add new rating.
|
||||
ratings.add({
|
||||
'userId': user.uid,
|
||||
'rating': rating,
|
||||
'timestamp': DateTime.now().toIso8601String(), // Verwende ISO-String statt FieldValue
|
||||
'timestamp': DateTime.now().toIso8601String(), // Use ISO string for timestamp
|
||||
});
|
||||
|
||||
// Berechne neue Gesamtbewertung
|
||||
// Calculate new overall rating.
|
||||
double overallRating = 0;
|
||||
if (ratings.isNotEmpty) {
|
||||
overallRating = ratings.map((r) => (r['rating'] as num).toDouble()).reduce((a, b) => a + b) / ratings.length;
|
||||
}
|
||||
|
||||
// Aktualisiere das Dokument
|
||||
// Update the training document with new ratings and overall rating.
|
||||
await trainingRef.update({
|
||||
'ratings': ratings,
|
||||
'rating overall': overallRating,
|
||||
|
@ -123,7 +132,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
}
|
||||
final data = snapshot.data!.data() as Map<String, dynamic>;
|
||||
|
||||
// Hole die Bewertung des aktuellen Users
|
||||
// Get the current user's rating for this exercise if not already loaded.
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user != null && _userRating == null) {
|
||||
final ratings = List<dynamic>.from(data['ratings'] ?? []);
|
||||
|
@ -141,6 +150,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Display the exercise image if available.
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
|
@ -155,6 +165,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Display the exercise title.
|
||||
Text(
|
||||
data['title'] ?? 'Unbekannt',
|
||||
style: const TextStyle(
|
||||
|
@ -163,21 +174,25 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Display the exercise description.
|
||||
Text(
|
||||
data['description'] ?? 'Keine Beschreibung',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Display the exercise duration.
|
||||
Text(
|
||||
'Dauer: ${data['duration'] ?? '-'} Minuten',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Display the exercise level/year.
|
||||
Text(
|
||||
'Level: ${data['year'] ?? '-'}',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Display the average rating.
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star, color: Colors.amber),
|
||||
|
@ -189,10 +204,12 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Display the number of ratings.
|
||||
Text(
|
||||
'Anzahl Bewertungen: ${(data['ratings'] ?? []).length}',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
// Show rating controls if the user is a player.
|
||||
if (_userRoleChecked && _isPlayer) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
|
@ -222,6 +239,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
|
|||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
// Display the exercise category.
|
||||
Text(
|
||||
'Kategorie: ${data['category'] ?? '-'}',
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
|
|
|
@ -1,28 +1,40 @@
|
|||
// responsive.dart
|
||||
// Utility class for handling responsive design in Flutter apps. Provides helpers for device type checks, scaling, and adaptive sizing.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// The Responsive class provides static methods to help adapt UI to different screen sizes (mobile, tablet, desktop).
|
||||
class Responsive {
|
||||
/// Returns true if the device is considered a mobile (width < 600px).
|
||||
static bool isMobile(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width < 600;
|
||||
|
||||
/// Returns true if the device is considered a tablet (600px <= width < 1200px).
|
||||
static bool isTablet(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width >= 600 &&
|
||||
MediaQuery.of(context).size.width < 1200;
|
||||
|
||||
/// Returns true if the device is considered a desktop (width >= 1200px).
|
||||
static bool isDesktop(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width >= 1200;
|
||||
|
||||
/// Returns the current screen width in logical pixels.
|
||||
static double getWidth(BuildContext context) =>
|
||||
MediaQuery.of(context).size.width;
|
||||
|
||||
/// Returns the current screen height in logical pixels.
|
||||
static double getHeight(BuildContext context) =>
|
||||
MediaQuery.of(context).size.height;
|
||||
|
||||
/// Returns a width scaled by the given percentage of the screen width.
|
||||
static double getScaledWidth(BuildContext context, double percentage) =>
|
||||
getWidth(context) * (percentage / 100);
|
||||
|
||||
/// Returns a height scaled by the given percentage of the screen height.
|
||||
static double getScaledHeight(BuildContext context, double percentage) =>
|
||||
getHeight(context) * (percentage / 100);
|
||||
|
||||
/// Returns appropriate padding based on device type (mobile/tablet/desktop).
|
||||
static EdgeInsets getPadding(BuildContext context) {
|
||||
if (isMobile(context)) {
|
||||
return const EdgeInsets.all(16.0);
|
||||
|
@ -33,6 +45,7 @@ class Responsive {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a font size scaled for the device type.
|
||||
static double getFontSize(BuildContext context, double baseSize) {
|
||||
if (isMobile(context)) {
|
||||
return baseSize;
|
||||
|
@ -43,6 +56,7 @@ class Responsive {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns an icon size scaled for the device type.
|
||||
static double getIconSize(BuildContext context, double baseSize) {
|
||||
if (isMobile(context)) {
|
||||
return baseSize;
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
// widgets/category_circle.dart
|
||||
// Widget for a circular category view
|
||||
// Widget für eine runde Kategorie-Ansicht
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that displays a category as a circle with an icon and a title below.
|
||||
/// Widget, das eine Kategorie als Kreis mit Icon und Titel darunter darstellt.
|
||||
class CategoryCircle extends StatelessWidget {
|
||||
/// The title of the category.
|
||||
final String title;
|
||||
/// The icon representing the category.
|
||||
final IconData icon;
|
||||
|
||||
/// Creates a CategoryCircle widget.
|
||||
const CategoryCircle({super.key, required this.title, required this.icon});
|
||||
|
||||
@override
|
||||
|
@ -14,6 +20,7 @@ class CategoryCircle extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Circle with icon
|
||||
// Kreis mit Icon
|
||||
Container(
|
||||
width: 70,
|
||||
|
@ -29,6 +36,7 @@ class CategoryCircle extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Title below the circle
|
||||
// Titel unter dem Kreis
|
||||
Text(
|
||||
title,
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
// widgets/exercise_card.dart
|
||||
// Widget for a training exercise card view
|
||||
// Widget für eine Trainingskarten-Ansicht
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that displays a card for a training exercise, showing its icon, title, and category.
|
||||
/// Widget, das eine Karte für eine Trainingsübung mit Icon, Titel und Kategorie anzeigt.
|
||||
class ExerciseCard extends StatelessWidget {
|
||||
/// The title of the exercise.
|
||||
final String title;
|
||||
/// The category of the exercise.
|
||||
final String category;
|
||||
/// The icon representing the exercise.
|
||||
final IconData icon;
|
||||
|
||||
/// Creates an ExerciseCard widget.
|
||||
const ExerciseCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
|
@ -26,9 +33,11 @@ class ExerciseCard extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icon for the exercise
|
||||
// Icon für die Übung
|
||||
Icon(icon, size: 60, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
// Title of the exercise
|
||||
// Titel der Übung
|
||||
Text(
|
||||
title,
|
||||
|
@ -36,6 +45,7 @@ class ExerciseCard extends StatelessWidget {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Category badge
|
||||
// Kategorie-Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
|
|
Loading…
Reference in New Issue