Skip to content

Commit

Permalink
improve statgraph fields, make achievements fit horizontally, update …
Browse files Browse the repository at this point in the history
…schema, etc
  • Loading branch information
MaxAPCS committed Mar 25, 2024
1 parent cd9757d commit 1b01cc8
Show file tree
Hide file tree
Showing 12 changed files with 649 additions and 575 deletions.
11 changes: 8 additions & 3 deletions lib/interfaces/localstore.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import 'dart:async';
import 'package:localstore/localstore.dart';
import 'package:stock/stock.dart';

import '../pages/matchscout.dart' show MatchScoutInfoSerialized;
import 'bluealliance.dart';

class LocalStoreInterface {
static final _db = Localstore.instance;

static Future<void> addMatch(int season, Map<String, dynamic> data) => _db
static Future<void> addMatch(MatchScoutInfoSerialized key, Map<String, dynamic> fields) => _db
.collection("scout")
.doc("match-$season${data['event']}_${data['match']}-${data['team']}")
.set(data..['season'] = season);
.doc("match-${key.season}${key.event}_${key.match}-${key.team}")
.set(fields
..['season'] = key.season
..['event'] = key.event
..['match'] = key.match
..['team'] = key.team);

static Future<void> addPit(int season, Map<String, dynamic> data) => _db
.collection("scout")
Expand Down
64 changes: 60 additions & 4 deletions lib/interfaces/supabase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import 'package:supabase_flutter/supabase_flutter.dart';

import '../pages/configuration.dart';
import '../pages/matchscout.dart' hide MatchScoutPage;
import 'bluealliance.dart' show MatchInfo, parseMatchInfo;

typedef AggInfo = ({int season, String? event, String? team});

// "Aren't supabase functions all over the code?" Yes, but here are the ones that require big think (and big caching)
class SupabaseInterface {
Expand Down Expand Up @@ -48,9 +51,8 @@ class SupabaseInterface {

static final matchscoutStock = Stock<int, MatchScoutQuestionSchema>(
fetcher: Fetcher.ofFuture((season) => Supabase.instance.client
.rpc('gettableschema', params: {"tablename": "${season}_match"}).then((resp) {
var schema = (Map<String, dynamic>.from(resp)
..removeWhere((key, _) => {"event", "match", "team", "scouter"}.contains(key)))
.rpc('gettableschema', params: {"tablename": "match_data_$season"}).then((resp) {
var schema = (Map<String, dynamic>.from(resp)..remove("id"))
.map((key, value) => MapEntry(key, value["type"]!));
MatchScoutQuestionSchema matchSchema = LinkedHashMap();
for (var MapEntry(key: columnname, value: sqltype) in schema.entries) {
Expand All @@ -72,7 +74,7 @@ class SupabaseInterface {

static final pitscoutStock = Stock<int, Map<String, String>>(
fetcher: Fetcher.ofFuture((season) => Supabase.instance.client
.rpc('gettableschema', params: {"tablename": "${season}_pit"}).then((resp) =>
.rpc('gettableschema', params: {"tablename": "pit_data_$season"}).then((resp) =>
(LinkedHashMap<String, dynamic>.from(resp)
..removeWhere((key, value) =>
{"event", "match", "team", "scouter"}.contains(key) ||
Expand Down Expand Up @@ -104,6 +106,60 @@ class SupabaseInterface {
.then((data) => _achievements = data)
: Future.value(_achievements);
static void clearAchievements() => _achievements = null;

static final aggregateStock = Stock<AggInfo, LinkedHashMap<dynamic, Map<String, num>>>(
sourceOfTruth: CachedSourceOfTruth(),
fetcher: Fetcher.ofFuture((key) async {
assert(key.event != null || key.team != null);
var response = LinkedHashMap<String, dynamic>.from(await Supabase.instance.client.functions
.invoke("match_aggregator_js", body: {
"season": key.season,
if (key.event != null) "event": key.event,
if (key.team != null) "team": key.team
})
.then((resp) =>
resp.status >= 400 ? throw Exception("HTTP Error ${resp.status}") : resp.data)
.catchError((e) => e is FunctionException && e.status == 404 ? {} : throw e))
.map((k, v) => MapEntry(k, Map<String, dynamic>.from(v)));
if (key.event != null && key.team != null) {
return LinkedHashMap<MatchInfo, Map<String, num>>.of(
response.map((k, v) => MapEntry(parseMatchInfo(k)!, Map.from(v[key.team]))));
// match: {scoretype: aggregated_count}
}
if (key.team != null) {
return LinkedHashMap.fromEntries(response.entries
.map((evententry) => Map<String, dynamic>.from(evententry.value).map(
(matchstring, matchscores) => MapEntry(
(event: evententry.key, match: parseMatchInfo(matchstring)!),
Map<String, num>.from(matchscores))))
.expand((e) => e.entries));
}
throw UnimplementedError("No aggregate for that combination");
}));

static final distinctStock = Stock<
AggInfo,
({
Set<String> scouters,
Set<String> eventmatches,
Set<String> events,
Set<String> matches,
Set<String> teams
})>(
sourceOfTruth: CachedSourceOfTruth(),
fetcher: Fetcher.ofFuture((key) {
var q =
Supabase.instance.client.from("match_scouting").select("*").eq("season", key.season);
if (key.event != null) q = q.eq("event", key.event!);
if (key.team != null) q = q.eq("team", key.team!);
return q.withConverter((data) => (
scouters: data.map<String>((e) => e["scouter"]).toSet(),
eventmatches: data.map<String>((e) => e["event"] + e["match"]).toSet(),
events: data.map<String>((e) => e["event"]).toSet(),
matches: data.map<String>((e) => e["match"]).toSet(),
teams: data.map<String>((e) => e["team"]).toSet()
));
}));
}

typedef Achievement = ({
Expand Down
160 changes: 76 additions & 84 deletions lib/pages/achievements.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ class AchievementsPage extends StatelessWidget {
@override
Widget build(BuildContext context) => Column(children: [
AppBar(title: const Text("Achievements")),
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchedText,
decoration: const InputDecoration(
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search_rounded),
hintText: "Search"))),
if (MediaQuery.of(context).size.height > 400)
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _searchedText,
decoration: const InputDecoration(
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.search_rounded),
hintText: "Search"))),
Expanded(
child: FutureBuilder(
future:
Expand Down Expand Up @@ -82,87 +83,78 @@ class AchievementsPage extends StatelessWidget {
.contains(_searchedText.text.toLowerCase()))
.toList(growable: false);
return CarouselSlider(
items: data.map((achdata) {
var autoscrollcontroller = ScrollController();
autoscrollcontroller.addListener(() =>
autoscrollcontroller.offset >=
autoscrollcontroller.position.maxScrollExtent
? autoscrollcontroller.jumpTo(0)
: autoscrollcontroller.position.pixels == 0
? autoscrollcontroller.animateTo(
autoscrollcontroller.position.maxScrollExtent,
duration: const Duration(seconds: 3),
curve: Curves.linear)
: null);
return Card(
clipBehavior: Clip.hardEdge,
child: Stack(fit: StackFit.expand, children: [
if (achdata.approved != null)
ColoredBox(
color: {
AchievementApprovalStatus.approved: Colors.green,
AchievementApprovalStatus.rejected: Colors.red,
AchievementApprovalStatus.pending: Colors.grey
}[achdata.approved]!
.withAlpha(128)),
Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(mainAxisSize: MainAxisSize.max, children: [
Flexible(
flex: 5,
fit: FlexFit.tight,
child: Align(
alignment: Alignment.centerLeft,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
achdata.achievement.name,
style: Theme.of(context)
.textTheme
.headlineMedium)))),
const Flexible(
flex: 1,
fit: FlexFit.tight,
child: SizedBox()),
Flexible(
flex: 2,
fit: FlexFit.tight,
child: Align(
alignment: Alignment.centerRight,
child: FittedBox(
child: Text(
"${achdata.achievement.points} pts",
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(
color:
Colors.grey)))))
]),
const SizedBox(height: 12),
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(
parent:
NeverScrollableScrollPhysics()),
controller: autoscrollcontroller,
child: Text(
achdata.achievement.description,
style: Theme.of(context)
.textTheme
.bodyLarge)))
]))
]));
}).toList(growable: false),
items: data
.map((achdata) => Card(
clipBehavior: Clip.hardEdge,
child: Stack(fit: StackFit.expand, children: [
if (achdata.approved != null)
ColoredBox(
color: {
AchievementApprovalStatus.approved: Colors.green,
AchievementApprovalStatus.rejected: Colors.red,
AchievementApprovalStatus.pending: Colors.grey
}[achdata.approved]!
.withAlpha(128)),
Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
flex: 5,
fit: FlexFit.tight,
child: Align(
alignment: Alignment.centerLeft,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
achdata
.achievement.name,
style: Theme.of(context)
.textTheme
.headlineMedium)))),
const Flexible(
flex: 1,
fit: FlexFit.tight,
child: SizedBox()),
Flexible(
flex: 2,
fit: FlexFit.tight,
child: Align(
alignment:
Alignment.centerRight,
child: FittedBox(
child: Text(
"${achdata.achievement.points} pts",
style: Theme.of(context)
.textTheme
.headlineSmall!
.copyWith(
color: Colors
.grey)))))
]),
const SizedBox(height: 12),
Expanded(
child: SingleChildScrollView(
physics:
const ClampingScrollPhysics(),
child: Text(
achdata.achievement.description,
style: Theme.of(context)
.textTheme
.bodyLarge)))
]))
])))
.toList(growable: false),
options: CarouselOptions(
viewportFraction:
max(300 / MediaQuery.of(context).size.width, 0.6),
enlargeCenterPage: true,
enlargeFactor: 0.2,
height: 200,
height: min(MediaQuery.of(context).size.height / 3, 200),
enableInfiniteScroll: _searchedText.text.isEmpty,
onPageChanged: (i, _) {
if (_selectedAchievement.value.achievement.id ==
Expand Down
Loading

0 comments on commit 1b01cc8

Please sign in to comment.