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