Feedback-Funktion für Spieler

main
joschy2002 2025-05-21 12:38:42 +02:00
parent 402c9a8835
commit 34e959e5ca
11 changed files with 443 additions and 46 deletions

View File

@ -1,6 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:label="trainerbox"
android:label="TrainerBox"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Trainerbox</string>
<string>TrainerBox</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@ -24,6 +24,10 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Diese App benötigt Zugriff auf die Galerie, um Bilder für Trainings hochzuladen.</string>
<key>NSCameraUsageDescription</key>
<string>Diese App benötigt Zugriff auf die Kamera, um Bilder für Trainings aufzunehmen.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'training_detail_screen.dart';
class SearchTab extends StatefulWidget {
@ -296,10 +299,36 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
String? _title;
String? _description;
int? _duration;
String? _picture;
double? _rating;
String? _year;
bool _loading = false;
File? _imageFile;
final _picker = ImagePicker();
Future<void> _pickImage() async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() {
_imageFile = File(pickedFile.path);
});
}
}
Future<String?> _uploadImage() async {
if (_imageFile == null) return null;
final storageRef = FirebaseStorage.instance
.ref()
.child('training_images')
.child('${DateTime.now().millisecondsSinceEpoch}.jpg');
try {
final uploadTask = await storageRef.putFile(_imageFile!);
return await uploadTask.ref.getDownloadURL();
} catch (e) {
print('Error uploading image: $e');
return null;
}
}
@override
Widget build(BuildContext context) {
@ -311,6 +340,24 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_imageFile != null)
Container(
height: 200,
width: double.infinity,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
image: DecorationImage(
image: FileImage(_imageFile!),
fit: BoxFit.cover,
),
),
),
ElevatedButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.image),
label: Text(_imageFile == null ? 'Bild auswählen' : 'Bild ändern'),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _category,
items: widget.categories
@ -337,20 +384,6 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
onChanged: (v) => _duration = int.tryParse(v),
validator: (v) => v == null || int.tryParse(v) == null ? 'Zahl angeben' : null,
),
TextFormField(
decoration: const InputDecoration(labelText: 'Bild-URL (optional)'),
onChanged: (v) => _picture = v,
),
TextFormField(
decoration: const InputDecoration(labelText: 'Bewertung (0-5)'),
keyboardType: TextInputType.number,
onChanged: (v) => _rating = double.tryParse(v),
validator: (v) {
final d = double.tryParse(v ?? '');
if (d == null || d < 0 || d > 5) return '0-5 angeben';
return null;
},
),
TextFormField(
decoration: const InputDecoration(labelText: 'Schwierigkeitslevel'),
onChanged: (v) => _year = v,
@ -371,16 +404,26 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
: () async {
if (_formKey.currentState!.validate()) {
setState(() => _loading = true);
try {
final imageUrl = await _uploadImage();
await FirebaseFirestore.instance.collection('Training').add({
'category': _category,
'title': _title,
'description': _description,
'duration': _duration,
'picture': _picture,
'rating overall': _rating,
'picture': imageUrl,
'rating overall': 0.0,
'year': _year,
'ratings': [], // Array für einzelne Bewertungen
});
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Erstellen: $e')),
);
} finally {
setState(() => _loading = false);
}
}
},
child: _loading ? const CircularProgressIndicator() : const Text('Erstellen'),

View File

@ -1,11 +1,111 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class TrainingDetailScreen extends StatelessWidget {
class TrainingDetailScreen extends StatefulWidget {
final String trainingId;
const TrainingDetailScreen({super.key, required this.trainingId});
@override
State<TrainingDetailScreen> createState() => _TrainingDetailScreenState();
}
class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
double? _userRating;
bool _isLoading = false;
bool _isPlayer = false;
bool _userRoleChecked = false;
@override
void initState() {
super.initState();
_checkUserRole();
}
Future<void> _checkUserRole() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
try {
final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
if (userDoc.exists) {
setState(() {
_isPlayer = userDoc.data()?['role'] == 'player';
_userRoleChecked = true;
});
}
} catch (e) {
print('Error checking user role: $e');
}
}
Future<void> _submitRating(double rating) async {
if (!_isPlayer) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Nur Spieler können Übungen bewerten')),
);
return;
}
setState(() => _isLoading = true);
try {
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
throw Exception('Nicht eingeloggt');
}
final trainingRef = FirebaseFirestore.instance.collection('Training').doc(widget.trainingId);
final trainingDoc = await trainingRef.get();
if (!trainingDoc.exists) {
throw Exception('Training nicht gefunden');
}
final data = trainingDoc.data() as Map<String, dynamic>;
List<dynamic> ratings = List<dynamic>.from(data['ratings'] ?? []);
// Entferne alte Bewertung des Users falls vorhanden
ratings.removeWhere((r) => r['userId'] == user.uid);
// Füge neue Bewertung hinzu
ratings.add({
'userId': user.uid,
'rating': rating,
'timestamp': DateTime.now().toIso8601String(), // Verwende ISO-String statt FieldValue
});
// Berechne neue Gesamtbewertung
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
await trainingRef.update({
'ratings': ratings,
'rating overall': overallRating,
});
setState(() => _userRating = rating);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bewertung erfolgreich gespeichert')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Speichern der Bewertung: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -13,7 +113,7 @@ class TrainingDetailScreen extends StatelessWidget {
title: const Text('Training Details'),
),
body: FutureBuilder<DocumentSnapshot>(
future: FirebaseFirestore.instance.collection('Training').doc(trainingId).get(),
future: FirebaseFirestore.instance.collection('Training').doc(widget.trainingId).get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
@ -22,6 +122,20 @@ class TrainingDetailScreen extends StatelessWidget {
return const Center(child: Text('Training nicht gefunden'));
}
final data = snapshot.data!.data() as Map<String, dynamic>;
// Hole die Bewertung des aktuellen Users
final user = FirebaseAuth.instance.currentUser;
if (user != null && _userRating == null) {
final ratings = List<dynamic>.from(data['ratings'] ?? []);
final userRating = ratings.firstWhere(
(r) => r['userId'] == user.uid,
orElse: () => {'rating': null},
)['rating'];
if (userRating != null) {
_userRating = (userRating as num).toDouble();
}
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -31,7 +145,12 @@ class TrainingDetailScreen extends StatelessWidget {
width: double.infinity,
height: 200,
color: Colors.grey[300],
child: const Center(
child: (data['picture'] is String && data['picture'] != '')
? Image.network(
data['picture'],
fit: BoxFit.cover,
)
: const Center(
child: Icon(Icons.image, size: 64, color: Colors.grey),
),
),
@ -58,11 +177,50 @@ class TrainingDetailScreen extends StatelessWidget {
'Level: ${data['year'] ?? '-'}',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 16),
Row(
children: [
const Icon(Icons.star, color: Colors.amber),
const SizedBox(width: 8),
Text(
'Durchschnittliche Bewertung: ${(data['rating overall'] ?? 0.0).toStringAsFixed(1)}',
style: const TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 8),
Text(
'Bewertung: ${data['rating overall'] ?? '-'}',
'Anzahl Bewertungen: ${(data['ratings'] ?? []).length}',
style: TextStyle(color: Colors.grey[600]),
),
if (_userRoleChecked && _isPlayer) ...[
const SizedBox(height: 16),
const Text(
'Deine Bewertung:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
index < (_userRating ?? 0) ? Icons.star : Icons.star_border,
color: Colors.amber,
size: 32,
),
onPressed: _isLoading
? null
: () => _submitRating(index + 1.0),
);
}),
),
if (_isLoading)
const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: CircularProgressIndicator()),
),
],
const SizedBox(height: 8),
Text(
'Kategorie: ${data['category'] ?? '-'}',

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -6,11 +6,15 @@ import FlutterMacOS
import Foundation
import cloud_firestore
import file_selector_macos
import firebase_auth
import firebase_core
import firebase_storage
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
}

View File

@ -73,6 +73,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
cupertino_icons:
dependency: "direct main"
description:
@ -89,6 +97,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
firebase_auth:
dependency: "direct main"
description:
@ -137,6 +177,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.17.5"
firebase_storage:
dependency: "direct main"
description:
name: firebase_storage
sha256: "2ae478ceec9f458c1bcbf0ee3e0100e4e909708979e83f16d5d9fba35a5b42c1"
url: "https://pub.dev"
source: hosted
version: "11.7.7"
firebase_storage_platform_interface:
dependency: transitive
description:
name: firebase_storage_platform_interface
sha256: "4e18662e6a66e2e0e181c06f94707de06d5097d70cfe2b5141bf64660c5b5da9"
url: "https://pub.dev"
source: hosted
version: "5.1.22"
firebase_storage_web:
dependency: transitive
description:
name: firebase_storage_web
sha256: "3a44aacd38a372efb159f6fe36bb4a7d79823949383816457fd43d3d47602a53"
url: "https://pub.dev"
source: hosted
version: "3.9.7"
flutter:
dependency: "direct main"
description: flutter
@ -146,10 +210,18 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "2.0.3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_test:
dependency: "direct dev"
description: flutter
@ -160,6 +232,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
http_parser:
dependency: transitive
description:
@ -168,6 +248,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
url: "https://pub.dev"
source: hosted
version: "0.8.12+23"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
intl:
dependency: transitive
description:
@ -204,10 +348,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "2.1.1"
matcher:
dependency: transitive
description:
@ -232,6 +376,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@ -248,6 +408,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "https://pub.dev"
source: hosted
version: "6.1.5"
simple_gesture_detector:
dependency: transitive
description:
@ -350,5 +518,5 @@ packages:
source: hosted
version: "0.5.1"
sdks:
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.27.0"

View File

@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ^3.7.2
sdk: '>=3.2.3 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@ -33,11 +33,14 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
firebase_core: ^2.32.0
cupertino_icons: ^1.0.2
firebase_core: ^2.24.2
table_calendar: ^3.0.9
cloud_firestore: ^4.17.3
firebase_auth: ^4.17.4
cloud_firestore: ^4.13.6
firebase_auth: ^4.15.3
firebase_storage: ^11.5.6
image_picker: ^1.0.7
provider: ^6.1.1
dev_dependencies:
flutter_test:
@ -48,7 +51,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -7,14 +7,20 @@
#include "generated_plugin_registrant.h"
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_auth/firebase_auth_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
#include <firebase_storage/firebase_storage_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
CloudFirestorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseAuthPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
FirebaseStoragePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi"));
}

View File

@ -4,8 +4,10 @@
list(APPEND FLUTTER_PLUGIN_LIST
cloud_firestore
file_selector_windows
firebase_auth
firebase_core
firebase_storage
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST