Initial commit: Flutter app + PHP/MySQL backend on Hostinger
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>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user