flutter_bracket 0.1.0
flutter_bracket: ^0.1.0 copied to clipboard
Generic graph-driven elimination brackets with pan, zoom, custom cards and themes.
import 'package:flutter/material.dart';
import 'package:flutter_bracket/flutter_bracket.dart';
import 'sample_brackets.dart';
void main() => runApp(const BracketExampleApp());
const examples = [
'Single 8',
'Single 32',
'Single 128',
'Single 256',
'BYE',
'Incomplete',
'Double',
'Custom Card',
'Themes',
'Single 1024',
'Double 128',
'Double 256',
'Double 1024',
];
class BracketExampleApp extends StatefulWidget {
const BracketExampleApp({super.key});
@override
State<BracketExampleApp> createState() => _BracketExampleAppState();
}
class _BracketExampleAppState extends State<BracketExampleApp> {
bool _dark = false;
@override
Widget build(BuildContext context) => MaterialApp(
title: 'flutter_bracket',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff0f766e)),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xff5eead4),
brightness: Brightness.dark,
),
),
themeMode: _dark ? ThemeMode.dark : ThemeMode.light,
home: ExamplePage(onThemeToggle: () => setState(() => _dark = !_dark)),
);
}
class ExamplePage extends StatefulWidget {
const ExamplePage({super.key, required this.onThemeToggle});
final VoidCallback onThemeToggle;
@override
State<ExamplePage> createState() => _ExamplePageState();
}
class _ExamplePageState extends State<ExamplePage> {
final _controller = BracketViewportController();
int _page = 0;
int _score = 18;
String? _selected;
bool _virtualization = true;
bool _overview = true;
final _stats = ValueNotifier<BracketRenderStats?>(null);
late Bracket _bracket = _createBracket;
void _onStats(BracketRenderStats stats) => _stats.value = stats;
@override
void dispose() {
_controller.dispose();
_stats.dispose();
super.dispose();
}
Bracket get _createBracket => switch (_page) {
9 => single(1024, score: _score),
10 => doubleFor(128),
11 => doubleFor(256),
12 => doubleFor(1024),
1 => single(32, score: _score, status: BracketMatchStatus.running),
2 => single(128, score: _score, status: BracketMatchStatus.running),
3 => single(256, score: _score, status: BracketMatchStatus.running),
4 => single(8, bye: true, score: _score),
5 => Bracket(
id: 'incomplete',
sections: [
BracketSection(
id: 'main',
type: BracketSectionType.main,
rounds: [
BracketRound(
id: 'r1',
order: 0,
matches: [
match('a', winnerTo: target('c')),
match('b', order: 1),
],
),
BracketRound(
id: 'r2',
order: 1,
matches: [
match(
'c',
side1: const BracketSide(source: WinnerOfSource('a')),
),
],
),
],
),
],
),
6 => doubleElimination(),
_ => single(8, score: _score, status: BracketMatchStatus.running),
};
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('flutter_bracket'),
actions: [
IconButton(
tooltip: 'Toggle theme',
onPressed: widget.onThemeToggle,
icon: const Icon(Icons.brightness_6_outlined),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 12,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
DropdownButton<int>(
value: _page,
items: [
for (var i = 0; i < examples.length; i++)
DropdownMenuItem(value: i, child: Text(examples[i])),
],
onChanged: (page) {
if (page == null) return;
setState(() {
_page = page;
_selected = null;
_bracket = _createBracket;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
Future<void>.microtask(() {
if (mounted) _controller.fit();
});
});
},
),
TextButton(
onPressed: _controller.reset,
child: const Text('Reset'),
),
TextButton(onPressed: _controller.fit, child: const Text('Fit')),
IconButton(
tooltip: 'Zoom in',
onPressed: _controller.zoomIn,
icon: const Icon(Icons.zoom_in),
),
IconButton(
tooltip: 'Zoom out',
onPressed: _controller.zoomOut,
icon: const Icon(Icons.zoom_out),
),
TextButton(
onPressed: () => setState(() {
_score = (_score + 1) % 30;
_bracket = updateMatchScore(
_bracket,
_bracket.sections.first.rounds.first.matches.first.id,
_score,
);
}),
child: const Text('Update score'),
),
TextButton(
onPressed: () => _controller.focusMatch(
_page == 6 || _page >= 10
? 'gf1'
: _page == 5
? 'c'
: 'm1',
),
child: const Text('Focus match'),
),
FilterChip(
label: const Text('Virtualization'),
selected: _virtualization,
onSelected: (value) => setState(() => _virtualization = value),
),
FilterChip(
label: const Text('Overview'),
selected: _overview,
onSelected: (value) => setState(() => _overview = value),
),
],
),
),
Expanded(
child: BracketView(
bracket: _bracket,
controller: _controller,
renderConfig: BracketRenderConfig(
virtualizationEnabled: _virtualization,
overviewEnabled: _overview,
),
onRenderStats: _onStats,
selectedMatchId: _selected,
highlightedMatchIds: const {'m1', 'm5'},
onMatchTap: (match) => setState(() => _selected = match.id),
theme: _page == 8
? const BracketTheme(
cardColor: Color(0xffe0f2fe),
borderColor: Color(0xff0284c7),
winnerConnectorColor: Color(0xff0284c7),
loserConnectorColor: Color(0xfff97316),
primaryTextStyle: TextStyle(color: Color(0xff0c4a6e)),
borderRadius: 16,
connectorWidth: 3,
)
: const BracketTheme(),
matchBuilder: _page == 7
? (context, match, state) => Semantics(
label: 'Custom match ${match.id}',
child: DecoratedBox(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xff115e59), Color(0xff0f766e)],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: state.isSelected
? Colors.amber
: Colors.teal.shade200,
width: 2,
),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'MATCH ${match.id.toUpperCase()}',
style: const TextStyle(
color: Colors.white70,
fontSize: 11,
),
),
const Spacer(),
Text(
match.side1.participant?.displayName ??
'Qualifier A',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Text(
match.side2.participant?.displayName ??
'Qualifier B',
style: const TextStyle(color: Colors.white),
),
],
),
),
),
)
: null,
),
),
Padding(
padding: const EdgeInsets.all(12),
child: ValueListenableBuilder<BracketRenderStats?>(
valueListenable: _stats,
builder: (context, stats, _) => Text(
'${stats?.mountedMatchCount ?? 0} cards mounted · ${stats?.isOverview == true ? 'Overview' : 'Detail'} · '
'${_selected == null ? 'Pan or pinch to explore' : 'Selected: $_selected'}',
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
);
}