init
This commit is contained in:
80
lib/features/tournament/data/models/player_model.dart
Normal file
80
lib/features/tournament/data/models/player_model.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'player_model.g.dart';
|
||||
|
||||
|
||||
@HiveType(typeId: 0)
|
||||
class PlayerModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
String name;
|
||||
|
||||
@HiveField(2)
|
||||
Gender gender;
|
||||
|
||||
@HiveField(3)
|
||||
SkillLevel skillLevel;
|
||||
|
||||
@HiveField(4)
|
||||
List<Position> positions; // Mehrere möglich
|
||||
|
||||
@HiveField(5)
|
||||
int wins = 0;
|
||||
|
||||
@HiveField(6)
|
||||
List<String> lastTeammates = []; // IDs der letzten Teamkollegen (max 5 behalten?)
|
||||
|
||||
@HiveField(7)
|
||||
bool hasSatOut = false; // Schon mal ausgesetzt?
|
||||
|
||||
@HiveField(8, defaultValue: true)
|
||||
bool isActive = true;
|
||||
|
||||
PlayerModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.gender,
|
||||
required this.skillLevel,
|
||||
required this.positions,
|
||||
this.wins = 0,
|
||||
this.lastTeammates = const [],
|
||||
this.hasSatOut = false,
|
||||
this.isActive = true,
|
||||
});
|
||||
}
|
||||
|
||||
@HiveType(typeId: 1) // Jede Enum braucht eine eigene typeId!
|
||||
enum Gender {
|
||||
@HiveField(0)
|
||||
male,
|
||||
@HiveField(1)
|
||||
female,
|
||||
@HiveField(2)
|
||||
diverse,
|
||||
}
|
||||
|
||||
@HiveType(typeId: 2)
|
||||
enum SkillLevel {
|
||||
@HiveField(0)
|
||||
low,
|
||||
@HiveField(1)
|
||||
mid,
|
||||
@HiveField(2)
|
||||
high,
|
||||
}
|
||||
|
||||
@HiveType(typeId: 3)
|
||||
enum Position {
|
||||
@HiveField(0)
|
||||
aussen,
|
||||
@HiveField(1)
|
||||
mitte,
|
||||
@HiveField(2)
|
||||
diagonal,
|
||||
@HiveField(3)
|
||||
zuspieler,
|
||||
@HiveField(4)
|
||||
libero,
|
||||
}
|
||||
207
lib/features/tournament/data/models/player_model.g.dart
Normal file
207
lib/features/tournament/data/models/player_model.g.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'player_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class PlayerModelAdapter extends TypeAdapter<PlayerModel> {
|
||||
@override
|
||||
final int typeId = 0;
|
||||
|
||||
@override
|
||||
PlayerModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PlayerModel(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
gender: fields[2] as Gender,
|
||||
skillLevel: fields[3] as SkillLevel,
|
||||
positions: (fields[4] as List).cast<Position>(),
|
||||
wins: fields[5] as int,
|
||||
lastTeammates: (fields[6] as List).cast<String>(),
|
||||
hasSatOut: fields[7] as bool,
|
||||
isActive: fields[8] == null ? true : fields[8] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PlayerModel obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.gender)
|
||||
..writeByte(3)
|
||||
..write(obj.skillLevel)
|
||||
..writeByte(4)
|
||||
..write(obj.positions)
|
||||
..writeByte(5)
|
||||
..write(obj.wins)
|
||||
..writeByte(6)
|
||||
..write(obj.lastTeammates)
|
||||
..writeByte(7)
|
||||
..write(obj.hasSatOut)
|
||||
..writeByte(8)
|
||||
..write(obj.isActive);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PlayerModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class GenderAdapter extends TypeAdapter<Gender> {
|
||||
@override
|
||||
final int typeId = 1;
|
||||
|
||||
@override
|
||||
Gender read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return Gender.male;
|
||||
case 1:
|
||||
return Gender.female;
|
||||
case 2:
|
||||
return Gender.diverse;
|
||||
default:
|
||||
return Gender.male;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Gender obj) {
|
||||
switch (obj) {
|
||||
case Gender.male:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case Gender.female:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case Gender.diverse:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is GenderAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class SkillLevelAdapter extends TypeAdapter<SkillLevel> {
|
||||
@override
|
||||
final int typeId = 2;
|
||||
|
||||
@override
|
||||
SkillLevel read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return SkillLevel.low;
|
||||
case 1:
|
||||
return SkillLevel.mid;
|
||||
case 2:
|
||||
return SkillLevel.high;
|
||||
default:
|
||||
return SkillLevel.low;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SkillLevel obj) {
|
||||
switch (obj) {
|
||||
case SkillLevel.low:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case SkillLevel.mid:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case SkillLevel.high:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SkillLevelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class PositionAdapter extends TypeAdapter<Position> {
|
||||
@override
|
||||
final int typeId = 3;
|
||||
|
||||
@override
|
||||
Position read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return Position.aussen;
|
||||
case 1:
|
||||
return Position.mitte;
|
||||
case 2:
|
||||
return Position.diagonal;
|
||||
case 3:
|
||||
return Position.zuspieler;
|
||||
case 4:
|
||||
return Position.libero;
|
||||
default:
|
||||
return Position.aussen;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Position obj) {
|
||||
switch (obj) {
|
||||
case Position.aussen:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case Position.mitte:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
case Position.diagonal:
|
||||
writer.writeByte(2);
|
||||
break;
|
||||
case Position.zuspieler:
|
||||
writer.writeByte(3);
|
||||
break;
|
||||
case Position.libero:
|
||||
writer.writeByte(4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PositionAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
166
lib/features/tournament/presentation/screens/fields_screen.dart
Normal file
166
lib/features/tournament/presentation/screens/fields_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user