This commit is contained in:
MarcWieland
2025-12-10 22:51:28 +01:00
commit e636f282ee
87 changed files with 4263 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/models/player_model.dart';
import 'player_provider.dart';
final activePlayersProvider = Provider<List<PlayerModel>>((ref) {
final allPlayers = ref.watch(playerListProvider);
return allPlayers.where((p) => p.isActive).toList();
});

View File

@@ -0,0 +1,71 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../../data/models/player_model.dart';
import 'package:uuid/uuid.dart';
// Provider für die Hive-Box
final playerBoxProvider = Provider<Box<PlayerModel>>((ref) {
return Hive.box<PlayerModel>('players');
});
// Provider für die Liste aller Players (live aktualisiert)
final playerListProvider = StateNotifierProvider<PlayerNotifier, List<PlayerModel>>((ref) {
final box = ref.watch(playerBoxProvider);
return PlayerNotifier(box);
});
class PlayerNotifier extends StateNotifier<List<PlayerModel>> {
PlayerNotifier(this.box) : super(box.values.toList()) {
// Listener für automatische Updates, falls von außen geändert
box.watch().listen((_) {
state = box.values.toList();
});
}
final Box<PlayerModel> box;
// Spieler hinzufügen
Future<void> addPlayer({
required String name,
required Gender gender,
required SkillLevel skillLevel,
required List<Position> positions,
}) async {
final newPlayer = PlayerModel(
id: const Uuid().v4(),
name: name,
gender: gender,
skillLevel: skillLevel,
positions: positions,
);
await box.put(newPlayer.id, newPlayer);
state = box.values.toList(); // Update state
}
// Spieler aktualisieren
Future<void> updatePlayer(PlayerModel updatedPlayer) async {
await updatedPlayer.save();
state = box.values.toList();
}
// Spieler löschen
Future<void> deletePlayer(PlayerModel player) async {
await player.delete();
state = box.values.toList();
}
// Alle löschen (z.B. für neues Turnier)
Future<void> clearAll() async {
await box.clear();
state = [];
}
void awardWinToPlayers(List<PlayerModel> winners){
for(final player in winners){
player.wins += 1;
player.save();
}
state = box.values.toList();
}
}

View File

@@ -0,0 +1,89 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:just_audio/just_audio.dart';
final timerProvider = StateNotifierProvider<TimerNotifier, TimerState>((ref) {
return TimerNotifier();
});
class TimerState {
final Duration remaining;
final bool isRunning;
final int selectedMinutes;
TimerState({
this.remaining = Duration.zero,
this.isRunning = false,
this.selectedMinutes = 15, // Default
});
TimerState copyWith({
Duration? remaining,
bool? isRunning,
int? selectedMinutes,
}) {
return TimerState(
remaining: remaining ?? this.remaining,
isRunning: isRunning ?? this.isRunning,
selectedMinutes: selectedMinutes ?? this.selectedMinutes,
);
}
}
class TimerNotifier extends StateNotifier<TimerState> {
TimerNotifier() : super(TimerState());
DateTime? _endTime;
final AudioPlayer _audioPlayer = AudioPlayer();
void setMinutes(int minutes) {
state = state.copyWith(selectedMinutes: minutes);
}
void start() {
final duration = Duration(minutes: state.selectedMinutes);
_endTime = DateTime.now().add(duration);
state = state.copyWith(remaining: duration, isRunning: true);
WakelockPlus.enable();
// Sound vorbereiten (später Asset hinzufügen)
_audioPlayer.setAsset('assets/sounds/alarm.mp3').catchError((_) {});
}
void pause() {
state = state.copyWith(isRunning: false);
}
void stop() {
state = TimerState(selectedMinutes: state.selectedMinutes); // Reset remaining + running
_endTime = null;
WakelockPlus.disable();
_audioPlayer.stop();
}
void tick() {
if (!state.isRunning || _endTime == null) return;
final now = DateTime.now();
if (now.isAfter(_endTime!)) {
state = state.copyWith(remaining: Duration.zero, isRunning: false);
WakelockPlus.disable();
_playAlarm();
return;
}
state = state.copyWith(remaining: _endTime!.difference(now));
}
Future<void> _playAlarm() async {
await _audioPlayer.seek(Duration.zero);
await _audioPlayer.play();
}
@override
void dispose() {
WakelockPlus.disable();
_audioPlayer.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../data/models/player_model.dart';
import '../provider/active_players_provider.dart';
import '../provider/player_provider.dart'; // für awardWinToPlayers
import '../widgets/volleyball_field_widget.dart';
class FieldsScreen extends ConsumerWidget {
const FieldsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final activePlayers = ref.watch(activePlayersProvider);
// Shuffle für Demo (später echter Algorithmus)
final shuffled = List<PlayerModel>.from(activePlayers)..shuffle();
// Maximal 36 Spieler auf Felder, Rest Aussetzer
final playersOnFields = shuffled.length > 36 ? shuffled.sublist(0, 36) : shuffled;
final bench = shuffled.length > 36 ? shuffled.sublist(36) : <PlayerModel>[];
// Felder aufteilen
final field1 = _slice(playersOnFields, 0, 12);
final field2 = _slice(playersOnFields, 12, 24);
final field3 = _slice(playersOnFields, 24, 36);
final fields = [field1, field2, field3];
return Scaffold(
body: Column(
children: [
// Swipe-Galerie
Expanded(
child: PageView.builder(
itemCount: 3,
itemBuilder: (context, index) {
final fieldPlayers = fields[index];
final teamA = fieldPlayers.sublist(0, fieldPlayers.length.clamp(0, 6));
final teamB = fieldPlayers.length > 6
? fieldPlayers.sublist(6, fieldPlayers.length.clamp(6, 12))
: <PlayerModel>[];
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Expanded(child: VolleyballFieldWidget(teamA: teamA, teamB: teamB, fieldNumber: index + 1)),
const SizedBox(height: 20),
// Gewinner-Buttons
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20),
),
),
),
onPressed: teamA.isEmpty
? null
: () {
ref.read(playerListProvider.notifier).awardWinToPlayers(teamA);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Oben gewinnt! +1 Schleifchen für ${teamA.length} Spieler'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
},
child: const Text('OBEN GEWINNT', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
),
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
),
onPressed: teamB.isEmpty
? null
: () {
ref.read(playerListProvider.notifier).awardWinToPlayers(teamB);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Unten gewinnt! +1 Schleifchen für ${teamB.length} Spieler'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
},
child: const Text('UNTEN GEWINNT', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
),
],
),
],
),
);
},
),
),
// Aussetzer
if (bench.isNotEmpty)
Container(
padding: const EdgeInsets.all(16),
color: Colors.orange.shade100,
child: Column(
children: [
const Text('Aussetzer diese Runde:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 8,
children: bench
.map((p) => Chip(
label: Text(p.name),
backgroundColor: Colors.orange.shade300,
))
.toList(),
),
],
),
),
// Shuffle Button unten
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
icon: const Icon(Icons.shuffle),
label: const Text('Neu mischen', style: TextStyle(fontSize: 18)),
onPressed: () {
ref.invalidate(activePlayersProvider);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Neue Teams gemischt!'), duration: Duration(milliseconds: 100)),
);
},
),
),
],
),
);
}
// Hilfsfunktion für sicheres Slicing
List<PlayerModel> _slice(List<PlayerModel> list, int start, int end) {
final actualEnd = end.clamp(0, list.length);
if (start >= actualEnd) return [];
return list.sublist(start, actualEnd);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:schleifchenturnier/features/tournament/presentation/screens/fields_screen.dart';
import 'package:schleifchenturnier/features/tournament/presentation/screens/stats_screen.dart';
import 'package:schleifchenturnier/features/tournament/presentation/screens/timer_setup_screen.dart';
import 'player_management_screen.dart'; // wird gleich erstellt
// Später: timer_screen.dart, fields_screen.dart, stats_screen.dart
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
int _selectedIndex = 0;
static const List<Widget> _screens = [
PlayerManagementScreen(),
TimerSetupScreen(),
FieldsScreen(),
StatsScreen()
];
void _onTabTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Schleifchenturnier'),
centerTitle: true,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
body: _screens[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: _onTabTapped,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.people), label: 'Spieler'),
BottomNavigationBarItem(icon: Icon(Icons.timer), label: 'Timer'),
BottomNavigationBarItem(icon: Icon(Icons.sports_volleyball), label: 'Felder'),
BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: 'Statistik'),
],
),
);
}
}

View File

@@ -0,0 +1,257 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../provider/player_provider.dart';
import '../../data/models/player_model.dart';
class PlayerManagementScreen extends ConsumerWidget {
const PlayerManagementScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final players = ref.watch(playerListProvider);
return Scaffold(
body: players.isEmpty
? const Center(
child: Text(
'Noch keine Spieler hinzugefügt\nTippe auf + um zu starten!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.grey),
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: players.length,
itemBuilder: (context, index) {
final player = players[index];
return Card(
child: ListTile(
title: Text(
player.name,
style: TextStyle(
fontWeight: FontWeight.bold,
decoration: player.isActive ? TextDecoration.none : TextDecoration.lineThrough,
color: player.isActive ? null : Colors.grey,
),
),
subtitle: Text(
'${_genderEmoji(player.gender)} ${player.gender.name}${player.skillLevel.name.toUpperCase()} • Siege: ${player.wins}${player.isActive ? '' : ' (inaktiv)'}',
style: TextStyle(color: player.isActive ? null : Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
ref.read(playerListProvider.notifier).deletePlayer(player);
},
),
Switch(
value: player.isActive,
onChanged: (val) {
player.isActive = val;
ref.read(playerListProvider.notifier).updatePlayer(player);
},
),
],
),
onTap: () {
_showAddEditPlayerDialog(context, ref, player: player);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddEditPlayerDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
String _genderEmoji(Gender gender) {
switch (gender) {
case Gender.male:
return '👨';
case Gender.female:
return '👩';
case Gender.diverse:
return '🧑';
}
}
void _showAddEditPlayerDialog(BuildContext context, WidgetRef ref, {PlayerModel? player}) {
final isEdit = player != null;
final formKey = GlobalKey<FormState>();
// Lokale temporäre Werte auch für isActive!
String name = isEdit ? player!.name : '';
Gender gender = isEdit ? player.gender : Gender.male;
SkillLevel skillLevel = isEdit ? player.skillLevel : SkillLevel.mid;
Set<Position> selectedPositions = isEdit ? player.positions.toSet() : <Position>{};
bool isActive = isEdit ? player.isActive : true; // Standard: aktiv
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(isEdit ? 'Spieler bearbeiten' : 'Neuen Spieler hinzufügen'),
content: SizedBox(
width: 400,
child: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Name
TextFormField(
initialValue: name,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Bitte einen Namen eingeben';
}
return null;
},
onSaved: (value) => name = value!.trim(),
),
const SizedBox(height: 16),
// Geschlecht
DropdownButtonFormField<Gender>(
value: gender,
decoration: const InputDecoration(
labelText: 'Geschlecht',
border: OutlineInputBorder(),
),
items: Gender.values.map((g) {
return DropdownMenuItem(
value: g,
child: Text(g.name[0].toUpperCase() + g.name.substring(1)),
);
}).toList(),
onChanged: (value) => gender = value!,
),
const SizedBox(height: 16),
// Leistungsstufe
const Text('Leistungsstufe', style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
SegmentedButton<SkillLevel>(
segments: SkillLevel.values.map((level) {
return ButtonSegment(
value: level,
label: Text(level.name.toUpperCase()),
);
}).toList(),
selected: {skillLevel},
onSelectionChanged: (newSelection) {
skillLevel = newSelection.first;
(ctx as Element).markNeedsBuild();
},
),
const SizedBox(height: 16),
// Aktiv-Schalter
Row(
children: [
Checkbox(
value: isActive,
onChanged: (val) {
isActive = val!;
(ctx as Element).markNeedsBuild();
},
),
const SizedBox(width: 8),
const Text('Aktiv am Turnier teilnehmen', style: TextStyle(fontSize: 16)),
],
),
const SizedBox(height: 16),
// Positionen
const Text('Positionen (mehrere möglich)', style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: Position.values.map((pos) {
final isSelected = selectedPositions.contains(pos);
return FilterChip(
label: Text(_positionDisplayName(pos)),
selected: isSelected,
onSelected: (selected) {
if (selected) {
selectedPositions.add(pos);
} else {
selectedPositions.remove(pos);
}
(ctx as Element).markNeedsBuild();
},
);
}).toList(),
),
],
),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Abbrechen'),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
if (isEdit) {
// Bearbeiten
player!.name = name;
player.gender = gender;
player.skillLevel = skillLevel;
player.positions = selectedPositions.toList();
player.isActive = isActive;
ref.read(playerListProvider.notifier).updatePlayer(player);
} else {
// Neu anlegen
ref.read(playerListProvider.notifier).addPlayer(
name: name,
gender: gender,
skillLevel: skillLevel,
positions: selectedPositions.toList(),
// Wir erweitern addPlayer später, oder wir holen den neuen Player und setzen isActive
).then((_) {
// Hinweis: Aktuell setzt addPlayer isActive = true. Wenn du es deaktiviert anlegen willst,
// müssen wir addPlayer erweitern. Für 99% der Fälle ist true ok.
// Alternativ: Später einen separaten Toggle-Provider machen.
});
}
Navigator.pop(ctx);
}
},
child: const Text('Speichern'),
),
],
),
);
}
String _positionDisplayName(Position pos) {
switch (pos) {
case Position.aussen:
return 'Außen';
case Position.mitte:
return 'Mitte';
case Position.diagonal:
return 'Diagonal';
case Position.zuspieler:
return 'Zuspieler';
case Position.libero:
return 'Libero';
}
}
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../provider/player_provider.dart';
import '../../data/models/player_model.dart';
class StatsScreen extends ConsumerWidget {
const StatsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final allPlayers = ref.watch(playerListProvider);
// Sortiere absteigend nach Wins
final sortedPlayers = List<PlayerModel>.from(allPlayers)
..sort((a, b) => b.wins.compareTo(a.wins));
return Scaffold(
appBar: AppBar(
title: const Text('Schleifchen-Rangliste'),
centerTitle: true,
),
body: sortedPlayers.isEmpty
? const Center(
child: Text(
'Noch keine Siege verteilt!\nSpielt eine Runde und lasst die Schleifchen fliegen! 🏐🎀',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, color: Colors.grey),
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: sortedPlayers.length,
itemBuilder: (context, index) {
final player = sortedPlayers[index];
final rank = index + 1;
// Podium-Farben
Color? backgroundColor;
Color? medalColor;
IconData? medalIcon;
if (index == 0 && player.wins > 0) {
backgroundColor = Colors.amber.shade100; // Gold
medalColor = Colors.amber.shade600;
medalIcon = Icons.looks_one;
} else if (index == 1 && player.wins > 0 && (sortedPlayers.length < 2 || player.wins < sortedPlayers[0].wins)) {
backgroundColor = Colors.grey.shade300; // Silber
medalColor = Colors.grey;
medalIcon = Icons.looks_two;
} else if (index == 2 && player.wins > 0 && (sortedPlayers.length < 3 || player.wins < sortedPlayers[1].wins)) {
backgroundColor = Colors.brown.shade200; // Bronze
medalColor = Colors.brown.shade700;
medalIcon = Icons.looks_3;
}
return Card(
color: backgroundColor,
elevation: backgroundColor != null ? 8 : 2,
margin: const EdgeInsets.symmetric(vertical: 6),
child: ListTile(
leading: medalIcon != null
? CircleAvatar(
backgroundColor: medalColor,
child: Icon(medalIcon, color: Colors.white, size: 28),
)
: CircleAvatar(
child: Text(
'$rank',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
title: Text(
player.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
subtitle: Text(
'${player.wins} Siege${player.wins == 1 ? '' : 'n'}',
style: TextStyle(
fontSize: 16,
color: player.wins > 0 ? Colors.green.shade700 : Colors.grey,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(player.wins.clamp(0, 5), (_) {
return const Padding(
padding: EdgeInsets.only(left: 4),
child: Icon(Icons.auto_awesome, color: Colors.amber, size: 20),
);
}),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import '../provider/timer_provider.dart';
class TimerRunningScreen extends ConsumerStatefulWidget {
const TimerRunningScreen({super.key});
@override
ConsumerState<TimerRunningScreen> createState() => _TimerRunningScreenState();
}
class _TimerRunningScreenState extends ConsumerState<TimerRunningScreen> with TickerProviderStateMixin {
late Ticker _ticker;
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
_ticker = createTicker((_) {
ref.read(timerProvider.notifier).tick();
});
_ticker.start();
}
@override
void dispose() {
_ticker.dispose();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
ref.read(timerProvider.notifier).stop(); // Wakelock aus
super.dispose();
}
@override
Widget build(BuildContext context) {
final timerState = ref.watch(timerProvider);
final remaining = timerState.remaining;
final minutes = remaining.inMinutes.toString().padLeft(2, '0');
final seconds = (remaining.inSeconds % 60).toString().padLeft(2, '0');
final isLast30 = remaining.inSeconds <= 30 && remaining.inSeconds > 0;
final isFinished = remaining == Duration.zero;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Dunkler Overlay wird transparent in letzten 30s
AnimatedContainer(
duration: const Duration(milliseconds: 800),
color: isLast30 || isFinished ? Colors.transparent : Colors.black.withOpacity(0.75),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 400),
style: TextStyle(
fontSize: isLast30 ? 180 : 140,
fontWeight: FontWeight.bold,
color: isFinished ? Colors.green : (isLast30 ? Colors.red : Colors.white),
shadows: isLast30
? [const Shadow(blurRadius: 30, color: Colors.red)]
: null,
),
child: Text('$minutes:$seconds'),
),
const SizedBox(height: 60),
if (isFinished)
const Text(
'Runde vorbei!',
style: TextStyle(fontSize: 48, color: Colors.green),
)
else if (timerState.isRunning)
ElevatedButton(
onPressed: () {
ref.read(timerProvider.notifier).pause();
},
child: const Text('Pause', style: TextStyle(fontSize: 32)),
),
],
),
),
// Zurück-Button oben links
Positioned(
top: 40,
left: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 40),
onPressed: () {
ref.read(timerProvider.notifier).stop();
Navigator.of(context).pop();
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'timer_running_screen.dart';
import '../provider/timer_provider.dart';
class TimerSetupScreen extends ConsumerWidget {
const TimerSetupScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedMinutes = ref.watch(timerProvider.select((s) => s.selectedMinutes));
return Scaffold(
appBar: AppBar(title: const Text('Timer einstellen')),
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Rundendauer wählen',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 60),
// Großer Minuten-Anzeige
Text(
'$selectedMinutes',
style: const TextStyle(fontSize: 120, fontWeight: FontWeight.bold),
),
const Text(
'Minuten',
style: TextStyle(fontSize: 32),
),
const SizedBox(height: 60),
// Slider horizontal
Slider(
min: 5,
max: 30,
divisions: 25,
value: selectedMinutes.toDouble(),
onChanged: (val) {
ref.read(timerProvider.notifier).setMinutes(val.round());
},
),
const SizedBox(height: 80),
// Start Button
ElevatedButton(
onPressed: () {
ref.read(timerProvider.notifier).start();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const TimerRunningScreen()),
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
textStyle: const TextStyle(fontSize: 28),
),
child: const Text('Start'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import '../../data/models/player_model.dart';
class VolleyballFieldWidget extends StatelessWidget {
final List<PlayerModel> teamA; // Obere Hälfte
final List<PlayerModel> teamB; // Untere Hälfte
final int fieldNumber;
const VolleyballFieldWidget({
super.key,
required this.teamA,
required this.teamB,
required this.fieldNumber,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Feldnummer
Text(
'Feld $fieldNumber',
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Das komplette Feld
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade800,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.black, width: 4),
),
child: Column(
children: [
// Obere Hälfte (Team A)
Expanded(child: _buildHalfField(teamA, isTop: true)),
// Netz
Container(
height: 8,
color: Colors.white,
margin: const EdgeInsets.symmetric(horizontal: 32),
),
// Untere Hälfte (Team B) gespiegelt
Expanded(child: _buildHalfField(teamB, isTop: false)),
],
),
),
),
],
);
}
Widget _buildHalfField(List<PlayerModel> team, {required bool isTop}) {
// Reihenfolge der Positionen:
// Index 0-2: Vorne (am Netz)
// Index 3-5: Hinten
// Für untere Hälfte spiegeln wir die Reihenfolge horizontal
final frontRow = team.length > 3 ? team.sublist(0, 3) : team.sublist(0, team.length.clamp(0, 3));
final backRow = team.length > 3 ? team.sublist(3, team.length.clamp(3, 6)) : <PlayerModel>[];
// Spiegelung für untere Hälfte
if (!isTop) {
frontRow.reversed.toList();
backRow.reversed.toList();
}
return Column(
children: [
// Vorne (am Netz)
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: _buildPlayerPills(isTop ? frontRow : frontRow.reversed.toList()),
),
),
// Hinten
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: _buildPlayerPills(isTop ? backRow : backRow.reversed.toList()),
),
),
],
);
}
List<Widget> _buildPlayerPills(List<PlayerModel> players) {
return List.generate(3, (i) {
final player = players.length > i ? players[i] : null;
if (player == null) {
return const Expanded(
child: SizedBox(),
);
}
return Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Colors.yellow.shade700,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.black, width: 3),
),
alignment: Alignment.center,
child: Text(
player.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
);
});
}
}