init
This commit is contained in:
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user