b239ae3e5f
Replaces Firebase with a self-hosted PHP/MySQL API served from winded.prymsolutions.com. Includes full backend (schema, auth, events, teams, brackets, suggestions, stats, media, file upload) and updated Flutter repositories and domain models. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
6.5 KiB
Dart
198 lines
6.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../domain/bracket.dart';
|
|
import 'round_column.dart';
|
|
|
|
/// Renders a single-elimination bracket as a horizontal scrolling tree.
|
|
///
|
|
/// Geometry rules:
|
|
/// * Each round is a [RoundColumn] of width [_columnWidth] (220px card area
|
|
/// + 20px right gap = 240px).
|
|
/// * Round 1 matches are evenly distributed across the available height.
|
|
/// * Each subsequent round's match N is centered between matches 2N and
|
|
/// 2N+1 of the previous round.
|
|
/// * Connector lines are drawn behind the cards by [_ConnectorsPainter]:
|
|
/// a horizontal stub leaves each match's right edge, then a vertical
|
|
/// segment joins to the horizontal stub entering the next round's match.
|
|
class BracketTreeWidget extends StatelessWidget {
|
|
const BracketTreeWidget({super.key, required this.bracket});
|
|
|
|
final Bracket bracket;
|
|
|
|
// Layout constants.
|
|
static const double _cardWidth = 200;
|
|
static const double _cardHeight = 80;
|
|
static const double _columnGap = 40;
|
|
static const double _columnWidth = _cardWidth + _columnGap; // 240
|
|
static const double _matchSlotHeight = 120; // card + status line + spacing
|
|
static const double _verticalPadding = 24;
|
|
static const double _labelHeight = RoundColumn.labelHeight;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final rounds = bracket.rounds;
|
|
if (rounds.isEmpty) {
|
|
return Center(
|
|
child: Text(
|
|
'This bracket has no rounds yet.',
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
);
|
|
}
|
|
|
|
final firstRoundMatches = rounds.first.matches.length.clamp(1, 1024);
|
|
|
|
// Total drawable height inside the column body (below the round label).
|
|
final bodyHeight = _matchSlotHeight * firstRoundMatches;
|
|
final columnHeight = bodyHeight + _labelHeight + _verticalPadding * 2;
|
|
final totalWidth = rounds.length * _columnWidth;
|
|
|
|
// Compute card centers per round, in local column-body coordinates
|
|
// (i.e. y measured from the top of the Stack that holds the cards).
|
|
final centersByRound = <List<double>>[];
|
|
for (var r = 0; r < rounds.length; r++) {
|
|
final matchCount = rounds[r].matches.length;
|
|
if (r == 0) {
|
|
// Evenly distribute round 1.
|
|
final slot = bodyHeight / matchCount;
|
|
centersByRound.add([
|
|
for (var i = 0; i < matchCount; i++)
|
|
_verticalPadding + slot * (i + 0.5),
|
|
]);
|
|
} else {
|
|
// Each match centered between its two feeders from previous round.
|
|
final prev = centersByRound[r - 1];
|
|
final centers = <double>[];
|
|
for (var i = 0; i < matchCount; i++) {
|
|
final a = i * 2;
|
|
final b = a + 1;
|
|
if (b < prev.length) {
|
|
centers.add((prev[a] + prev[b]) / 2);
|
|
} else if (a < prev.length) {
|
|
centers.add(prev[a]);
|
|
} else {
|
|
centers.add(_verticalPadding + bodyHeight / 2);
|
|
}
|
|
}
|
|
centersByRound.add(centers);
|
|
}
|
|
}
|
|
|
|
final connectorColor = Theme.of(context).colorScheme.outlineVariant;
|
|
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: SizedBox(
|
|
width: totalWidth,
|
|
height: columnHeight,
|
|
child: Stack(
|
|
children: [
|
|
// Connector lines drawn first so they sit behind the cards.
|
|
Positioned.fill(
|
|
child: IgnorePointer(
|
|
child: CustomPaint(
|
|
painter: _ConnectorsPainter(
|
|
rounds: rounds,
|
|
centersByRound: centersByRound,
|
|
columnWidth: _columnWidth,
|
|
cardWidth: _cardWidth,
|
|
labelHeight: _labelHeight,
|
|
color: connectorColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
for (var r = 0; r < rounds.length; r++)
|
|
RoundColumn(
|
|
round: rounds[r],
|
|
cardCenters: centersByRound[r],
|
|
columnWidth: _columnWidth,
|
|
cardWidth: _cardWidth,
|
|
cardHeight: _cardHeight,
|
|
columnHeight: columnHeight,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ConnectorsPainter extends CustomPainter {
|
|
_ConnectorsPainter({
|
|
required this.rounds,
|
|
required this.centersByRound,
|
|
required this.columnWidth,
|
|
required this.cardWidth,
|
|
required this.labelHeight,
|
|
required this.color,
|
|
});
|
|
|
|
final List<BracketRound> rounds;
|
|
final List<List<double>> centersByRound;
|
|
final double columnWidth;
|
|
final double cardWidth;
|
|
final double labelHeight;
|
|
final Color color;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = color
|
|
..strokeWidth = 1.5
|
|
..style = PaintingStyle.stroke;
|
|
|
|
// For each pair of adjacent rounds, draw connectors from every match in
|
|
// the earlier round into its corresponding match in the later round.
|
|
for (var r = 0; r < rounds.length - 1; r++) {
|
|
final left = centersByRound[r];
|
|
final right = centersByRound[r + 1];
|
|
|
|
// Card horizontal bounds for this column.
|
|
final colLeftX = r * columnWidth;
|
|
final cardRightX = colLeftX + (columnWidth + cardWidth) / 2;
|
|
|
|
final nextColLeftX = (r + 1) * columnWidth;
|
|
final nextCardLeftX = nextColLeftX + (columnWidth - cardWidth) / 2;
|
|
|
|
final midX = (cardRightX + nextCardLeftX) / 2;
|
|
|
|
for (var i = 0; i < left.length; i++) {
|
|
// Pair index in next round.
|
|
final next = i ~/ 2;
|
|
if (next >= right.length) continue;
|
|
|
|
final fromY = left[i] + labelHeight;
|
|
final toY = right[next] + labelHeight;
|
|
|
|
// Right stub from card.
|
|
canvas.drawLine(Offset(cardRightX, fromY), Offset(midX, fromY), paint);
|
|
// Vertical segment connecting siblings.
|
|
canvas.drawLine(Offset(midX, fromY), Offset(midX, toY), paint);
|
|
// Left stub into next round's card.
|
|
canvas.drawLine(
|
|
Offset(midX, toY),
|
|
Offset(nextCardLeftX, toY),
|
|
paint,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _ConnectorsPainter old) {
|
|
return old.rounds != rounds ||
|
|
old.centersByRound != centersByRound ||
|
|
old.color != color ||
|
|
old.columnWidth != columnWidth ||
|
|
old.cardWidth != cardWidth ||
|
|
old.labelHeight != labelHeight;
|
|
}
|
|
}
|