3 Commits

6 changed files with 290 additions and 224 deletions

View File

@@ -3,10 +3,15 @@ import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/full_width_button.dart'; import 'package:game_tracker/presentation/widgets/full_width_button.dart';
import 'package:game_tracker/presentation/widgets/text_input_field.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:uuid/uuid.dart';
class CreateGroupView extends StatefulWidget { class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key}); const CreateGroupView({super.key});
@@ -61,29 +66,12 @@ class _CreateGroupViewState extends State<CreateGroupView> {
children: [ children: [
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: TextField( child: TextInputField(
controller: _groupNameController, controller: _groupNameController,
hintText: 'Group name',
onChanged: (value) { onChanged: (value) {
setState(() {}); setState(() {});
}, },
decoration: InputDecoration(
filled: true,
fillColor: CustomTheme.boxColor,
hint: Text(
"Group name",
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 18),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
), ),
), ),
Expanded( Expanded(
@@ -104,29 +92,16 @@ class _CreateGroupViewState extends State<CreateGroupView> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SearchBar( CustomSearchBar(
controller: _searchBarController, controller: _searchBarController,
constraints: BoxConstraints(maxHeight: 45, minHeight: 45), constraints: BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: "Search for players", hintText: "Search for players",
hintStyle: WidgetStateProperty.all(
TextStyle(fontSize: 16),
),
leading: Icon(Icons.search),
backgroundColor: WidgetStateProperty.all(
CustomTheme.boxColor,
),
side: WidgetStateProperty.all(
BorderSide(color: CustomTheme.boxBorder),
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
if (value.isEmpty) { if (value.isEmpty) {
suggestedPlayers = [...allPlayers]; suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player);
}).toList();
} else { } else {
suggestedPlayers = allPlayers.where((player) { suggestedPlayers = allPlayers.where((player) {
final bool nameMatches = player.name final bool nameMatches = player.name
@@ -156,40 +131,18 @@ class _CreateGroupViewState extends State<CreateGroupView> {
runSpacing: 8.0, runSpacing: 8.0,
children: <Widget>[ children: <Widget>[
for (var selectedPlayer in selectedPlayers) for (var selectedPlayer in selectedPlayers)
Container( TextIconTile(
padding: EdgeInsets.all(5), text: selectedPlayer.name,
decoration: BoxDecoration( icon: Icons.close,
color: CustomTheme.onBoxColor, onIconTap: () {
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: 12),
Flexible(
child: Text(
selectedPlayer.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(width: 3),
GestureDetector(
child: const Icon(Icons.close, size: 20),
onTap: () {
setState(() { setState(() {
final currentSearch = _searchBarController final currentSearch = _searchBarController.text
.text
.toLowerCase(); .toLowerCase();
selectedPlayers.remove(selectedPlayer); selectedPlayers.remove(selectedPlayer);
if (currentSearch.isEmpty || if (currentSearch.isEmpty ||
selectedPlayer.name selectedPlayer.name.toLowerCase().contains(
.toLowerCase() currentSearch,
.contains(currentSearch)) { )) {
suggestedPlayers.add(selectedPlayer); suggestedPlayers.add(selectedPlayer);
suggestedPlayers.sort( suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name), (a, b) => a.name.compareTo(b.name),
@@ -200,9 +153,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
], ],
), ),
),
],
),
SizedBox(height: 10), SizedBox(height: 10),
Text( Text(
"Alle Spieler:", "Alle Spieler:",
@@ -214,7 +164,11 @@ class _CreateGroupViewState extends State<CreateGroupView> {
SizedBox(height: 10), SizedBox(height: 10),
FutureBuilder( FutureBuilder(
future: _allPlayersFuture, future: _allPlayersFuture,
builder: (BuildContext context, AsyncSnapshot<List<Player>> snapshot) { builder:
(
BuildContext context,
AsyncSnapshot<List<Player>> snapshot,
) {
if (snapshot.hasError) { if (snapshot.hasError) {
return const Center( return const Center(
child: TopCenteredMessage( child: TopCenteredMessage(
@@ -224,7 +178,8 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
); );
} }
if (snapshot.connectionState == ConnectionState.done && if (snapshot.connectionState ==
ConnectionState.done &&
(!snapshot.hasData || (!snapshot.hasData ||
snapshot.data!.isEmpty || snapshot.data!.isEmpty ||
(selectedPlayers.isEmpty && (selectedPlayers.isEmpty &&
@@ -238,7 +193,8 @@ class _CreateGroupViewState extends State<CreateGroupView> {
); );
} }
final bool isLoading = final bool isLoading =
snapshot.connectionState == ConnectionState.waiting; snapshot.connectionState ==
ConnectionState.waiting;
return Expanded( return Expanded(
child: Skeletonizer( child: Skeletonizer(
effect: PulseEffect( effect: PulseEffect(
@@ -248,12 +204,13 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
enabled: isLoading, enabled: isLoading,
enableSwitchAnimation: true, enableSwitchAnimation: true,
switchAnimationConfig: const SwitchAnimationConfig( switchAnimationConfig:
const SwitchAnimationConfig(
duration: Duration(milliseconds: 200), duration: Duration(milliseconds: 200),
switchInCurve: Curves.linear, switchInCurve: Curves.linear,
switchOutCurve: Curves.linear, switchOutCurve: Curves.linear,
transitionBuilder: transitionBuilder: AnimatedSwitcher
AnimatedSwitcher.defaultTransitionBuilder, .defaultTransitionBuilder,
layoutBuilder: layoutBuilder:
AnimatedSwitcher.defaultLayoutBuilder, AnimatedSwitcher.defaultLayoutBuilder,
), ),
@@ -271,41 +228,12 @@ class _CreateGroupViewState extends State<CreateGroupView> {
) )
: ListView.builder( : ListView.builder(
itemCount: suggestedPlayers.length, itemCount: suggestedPlayers.length,
itemBuilder: (BuildContext context, int index) { itemBuilder:
return Container( (BuildContext context, int index) {
margin: const EdgeInsets.symmetric( return IconListTile(
horizontal: 5, text: suggestedPlayers[index]
vertical: 5, .name,
), icon: Icons.add,
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(
color: CustomTheme.boxBorder,
),
borderRadius: BorderRadius.circular(
12,
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: Text(
suggestedPlayers[index].name,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
IconButton(
icon: Icon(Icons.add, size: 20),
onPressed: () { onPressed: () {
setState(() { setState(() {
if (!selectedPlayers.contains( if (!selectedPlayers.contains(
@@ -324,9 +252,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
} }
}); });
}, },
),
],
),
); );
}, },
), ),
@@ -348,11 +273,12 @@ class _CreateGroupViewState extends State<CreateGroupView> {
(_groupNameController.text.isEmpty || selectedPlayers.isEmpty) (_groupNameController.text.isEmpty || selectedPlayers.isEmpty)
? null ? null
: () async { : () async {
String id = "ID_" + _groupNameController.text;
String name = _groupNameController.text;
List<Player> members = selectedPlayers;
bool success = await db.groupDao.addGroup( bool success = await db.groupDao.addGroup(
group: Group(id: id, name: name, members: members), group: Group(
id: Uuid().v4(),
name: _groupNameController.text,
members: selectedPlayers,
),
); );
if (success) { if (success) {
_groupNameController.clear(); _groupNameController.clear();

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class CustomSearchBar extends StatelessWidget {
final TextEditingController controller;
final String hintText;
final ValueChanged<String>? onChanged;
final BoxConstraints? constraints;
const CustomSearchBar({
super.key,
required this.controller,
required this.hintText,
this.onChanged,
this.constraints,
});
@override
Widget build(BuildContext context) {
return SearchBar(
controller: controller,
constraints:
constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: hintText,
onChanged: onChanged,
hintStyle: MaterialStateProperty.all(const TextStyle(fontSize: 16)),
leading: const Icon(Icons.search),
backgroundColor: MaterialStateProperty.all(CustomTheme.boxColor),
side: MaterialStateProperty.all(BorderSide(color: CustomTheme.boxBorder)),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
elevation: MaterialStateProperty.all(0),
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class TextInputField extends StatelessWidget {
final TextEditingController controller;
final ValueChanged<String>? onChanged;
final String hintText;
const TextInputField({
super.key,
required this.controller,
required this.hintText,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
filled: true,
fillColor: CustomTheme.boxColor,
hintText: hintText,
hintStyle: const TextStyle(fontSize: 18),
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),
),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatelessWidget { class GroupTile extends StatelessWidget {
const GroupTile({super.key, required this.group}); const GroupTile({super.key, required this.group});
@@ -56,27 +56,7 @@ class GroupTile extends StatelessWidget {
spacing: 12.0, spacing: 12.0,
runSpacing: 8.0, runSpacing: 8.0,
children: <Widget>[ children: <Widget>[
for (var member in group.members) for (var member in group.members) TextIconTile(text: member.name),
Container(
padding: const EdgeInsets.symmetric(
vertical: 5,
horizontal: 10,
),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Skeleton.ignore(
child: Text(
member.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
], ],
), ),
const SizedBox(height: 2.5), const SizedBox(height: 2.5),

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class IconListTile extends StatelessWidget {
final String text;
final IconData icon;
final VoidCallback onPressed;
const IconListTile({
super.key,
required this.text,
required this.icon,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
),
IconButton(icon: Icon(icon, size: 20), onPressed: onPressed),
],
),
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class TextIconTile extends StatelessWidget {
final String text;
final IconData? icon;
final VoidCallback? onIconTap;
const TextIconTile({
super.key,
required this.text,
this.icon,
this.onIconTap,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) const SizedBox(width: 3),
Flexible(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
),
if (icon != null) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
],
],
),
);
}
}