From 041b70a86c2d0c7fcf8d81d03a803bf16997d88b Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 26 Jun 2023 20:13:53 +0300 Subject: [PATCH] Pull to refresh + now we have only one chart But it's possible to change sets of data for chart Also changed fetch function in main view --- lib/data_objects/tetrio.dart | 4 +- lib/services/tetrio_crud.dart | 6 +- lib/views/main_view.dart | 268 ++++++++++++++-------------- pubspec.yaml | 2 + res/tetrio_badges/galactic2x2_2.png | Bin 0 -> 7790 bytes res/tetrio_badges/galactic2x2_3.png | Bin 0 -> 10417 bytes 6 files changed, 139 insertions(+), 141 deletions(-) create mode 100644 res/tetrio_badges/galactic2x2_2.png create mode 100644 res/tetrio_badges/galactic2x2_3.png diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index ba3d586..74de230 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -344,7 +344,7 @@ class EndContextSingle { late int piecesPlaced; late int lines; late int score; - late int seed; + late double seed; late Duration finalTime; late int tSpins; late Clears clears; @@ -373,7 +373,7 @@ class EndContextSingle { required this.finesse}); EndContextSingle.fromJson(Map json) { - seed = json['seed']; + seed = json['seed'].toDouble(); lines = json['lines']; inputs = json['inputs']; holds = json['holds'] ?? 0; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 577bfbf..ace3d54 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -34,13 +34,14 @@ const String createTetrioUsersToTrack = ''' '''; const String createTetrioTLRecordsTable = ''' CREATE TABLE IF NOT EXISTS "tetrioAlphaLeagueMathces" ( - "id" TEXT, + "id" TEXT NOT NULL UNIQUE, "replayId" TEXT, "player1id" TEXT, "player2id" TEXT, "timestamp" TEXT, "endContext1" TEXT, - "endContext2" TEXT + "endContext2" TEXT, + PRIMARY KEY("id") ) '''; @@ -70,7 +71,6 @@ class TetrioService extends DB { final allPlayers = await getAllPlayers(); _players = allPlayers.toList().first; // ??? _tetrioStreamController.add(_players); - developer.log("_loadPlayers: $_players", name: "services/tetrio_crud"); } Future deletePlayer(String id) async { diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index ccaa900..d24c8b8 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'dart:math'; @@ -17,6 +19,9 @@ String _searchFor = "dan63047"; String _titleNickname = "dan63047"; final TetrioService teto = TetrioService(); late SharedPreferences prefs; +var chartsData = >>[]; +List chartsShortTitles = ["TR", "Glicko", "RD", "APM", "PPS", "VS", "APP", "DS/S", "DS/P", "APP + DS/P", "VS/APM", "Cheese", "GbE", "wAPP", "Area", "eTR", "±eTR"]; +int chartsIndex = 0; const allowedHeightForPlayerIdInPixels = 40.0; const allowedHeightForPlayerBioInPixels = 30.0; const givenTextHeightByScreenPercentage = 0.3; @@ -43,7 +48,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { final List myTabs = [ const Tab(text: "Tetra League"), const Tab(text: "TL Records"), - const Tab(text: "TL History"), + const Tab(text: "History"), const Tab(text: "40 Lines"), const Tab(text: "Blitz"), const Tab(text: "Other"), @@ -110,33 +115,48 @@ class _MainState extends State with SingleTickerProviderStateMixin { Future fetch(String nickOrID) async { TetrioPlayer me = await teto.fetchPlayer(nickOrID); setState((){_titleNickname = me.username;}); + var tlStream = await teto.getTLStream(me.userId); + List tlMatches = []; bool isTracking = await teto.isPlayerTracking(me.userId); List states = []; if (isTracking){ teto.storeState(me); teto.saveTLMatchesFromStream(await teto.getTLStream(me.userId)); states.addAll(await teto.getPlayer(me.userId)); - } - Map records = await teto.fetchRecords(me.userId); - return [me, records, states, isTracking]; - } - - Future> getTLMatches(String userID) async { - var fetched = await teto.getTLStream(userID); - bool isTracked = await teto.isPlayerTracking(userID); - if (!isTracked) return fetched.records; - teto.saveTLMatchesFromStream(fetched); - var fromdb = await teto.getTLMatchesbyPlayerID(userID); - for (var match in fetched.records) { - if (!fromdb.contains(match)) fromdb.add(match); + chartsData = >>[ + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.gamesPlayed > 9) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.rating)], child: const Text("Tetra Rating")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.gamesPlayed > 9) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.glicko!)], child: const Text("Glicko")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.gamesPlayed > 9) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.rd!)], child: const Text("Rating Deviation")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.apm != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.apm!)], child: const Text("Attack Per Minute")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.pps != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.pps!)], child: const Text("Pieces Per Second")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.vs != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.vs!)], child: const Text("Versus Score")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.app)], child: const Text("Attack Per Piece")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.dss)], child: const Text("Downstack Per Second")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.dsp)], child: const Text("Downstack Per Piece")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.appdsp)], child: const Text("APP + DS/P")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.vsapm)], child: const Text("VS/APM")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.cheese)], child: const Text("Cheese Index")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.gbe)], child: const Text("Garbage Efficiency")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.nyaapp)], child: const Text("Weighted APP")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.area)], child: const Text("Area")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.estTr != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.estTr!.esttr)], child: const Text("Est. of TR")), + DropdownMenuItem(value: [for (var state in states) if (state.tlSeason1.esttracc != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.esttracc!)], child: const Text("Accuracy of Est.")), + ]; + tlMatches.addAll(await teto.getTLMatchesbyPlayerID(me.userId)); + for (var match in tlStream.records) { + if (!tlMatches.contains(match)) tlMatches.add(match); } - fromdb.sort((a, b) { + tlMatches.sort((a, b) { if(a.timestamp.isBefore(b.timestamp)) return 1; if(a.timestamp.isAtSameMomentAs(b.timestamp)) return 0; if(a.timestamp.isAfter(b.timestamp)) return -1; return 0; }); - return fromdb; + } else{ + tlMatches = tlStream.records; + } + Map records = await teto.fetchRecords(me.userId); + return [me, records, states, tlMatches, isTracking]; } void _justUpdate() { @@ -234,44 +254,57 @@ class _MainState extends State with SingleTickerProviderStateMixin { case ConnectionState.done: //bool bigScreen = MediaQuery.of(context).size.width > 1024; if (snapshot.hasData) { - return NestedScrollView( - controller: _scrollController, - headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: UserThingy( - player: snapshot.data![0], - showStateTimestamp: false, - setState: _justUpdate, - )), - SliverToBoxAdapter( - child: TabBar( - controller: _tabController, - isScrollable: true, - tabs: myTabs, - ), - ), - ]; + return RefreshIndicator( + onRefresh: () { + return Future(() => changePlayer(snapshot.data![0].userId)); }, - body: TabBarView( - controller: _tabController, - children: [ - TLThingy( - tl: snapshot.data![0].tlSeason1, - userID: snapshot.data![0].userId), - _TLRecords(userID: snapshot.data![0].userId, get: getTLMatches,), - _TLHistory(states: snapshot.data![2]), - _RecordThingy( - record: (snapshot.data![1]['sprint'].isNotEmpty) - ? snapshot.data![1]['sprint'][0] - : null), - _RecordThingy( - record: (snapshot.data![1]['blitz'].isNotEmpty) - ? snapshot.data![1]['blitz'][0] - : null), - _OtherThingy( - zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio) - ], + notificationPredicate: (notification) { + // with NestedScrollView local(depth == 2) OverscrollNotification are not sent + if (notification is OverscrollNotification || Platform.isIOS) { + return notification.depth == 2; + } + return notification.depth == 0; + }, + child: NestedScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + headerSliverBuilder: (context, value) { + return [ + SliverToBoxAdapter( + child: UserThingy( + player: snapshot.data![0], + showStateTimestamp: false, + setState: _justUpdate, + )), + SliverToBoxAdapter( + child: TabBar( + controller: _tabController, + isScrollable: true, + tabs: myTabs, + ), + ), + ]; + }, + body: TabBarView( + controller: _tabController, + children: [ + TLThingy( + tl: snapshot.data![0].tlSeason1, + userID: snapshot.data![0].userId), + _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), + _History(states: snapshot.data![2], update: _justUpdate), + _RecordThingy( + record: (snapshot.data![1]['sprint'].isNotEmpty) + ? snapshot.data![1]['sprint'][0] + : null), + _RecordThingy( + record: (snapshot.data![1]['blitz'].isNotEmpty) + ? snapshot.data![1]['blitz'][0] + : null), + _OtherThingy( + zen: snapshot.data![1]['zen'], bio: snapshot.data![0].bio) + ], + ), ), ); } else if (snapshot.hasError) { @@ -401,99 +434,62 @@ class _NavDrawerState extends State { class _TLRecords extends StatelessWidget { final String userID; - final Future> Function(String user) get; + final List data; - const _TLRecords({required this.userID, required this.get}); + const _TLRecords({required this.userID, required this.data}); @override Widget build(BuildContext context) { - return FutureBuilder( - future: get(userID), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const Center( - child: CircularProgressIndicator(color: Colors.white)); - case ConnectionState.done: - if (snapshot.hasError) { - return Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)); - } else { - return ListView( - physics: const ClampingScrollPhysics(), - children: (snapshot.data!.isNotEmpty) - ? [for (var value in snapshot.data!) ListTile( - leading: Text("${value.endContext.firstWhere((element) => element.userId == userID).points} : ${value.endContext.firstWhere((element) => element.userId != userID).points}", - style: const TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 28,)), - title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username}"), - subtitle: Text(dateFormat.format(value.timestamp)), - trailing: Column(mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).secondary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).secondary)} APM", style: const TextStyle(height: 1.1)), - Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: const TextStyle(height: 1.1)), - Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: const TextStyle(height: 1.1)), - ]), - onTap: (){Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TlMatchResultView(record: value, initPlayerId: userID), - ), - );}, - )] - : [const Center(child: Text("No records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], - ); - } - } - }); + return ListView( // TODO: Redo using ListView.builder() + physics: const AlwaysScrollableScrollPhysics(), + children: (data.isNotEmpty) + ? [for (var value in data) ListTile( + leading: Text("${value.endContext.firstWhere((element) => element.userId == userID).points} : ${value.endContext.firstWhere((element) => element.userId != userID).points}", + style: const TextStyle( + fontFamily: "Eurostile Round Extended", + fontSize: 28,)), + title: Text("vs. ${value.endContext.firstWhere((element) => element.userId != userID).username}"), + subtitle: Text(dateFormat.format(value.timestamp)), + trailing: Column(mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).secondary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).secondary)} APM", style: const TextStyle(height: 1.1)), + Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).tertiary)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).tertiary)} PPS", style: const TextStyle(height: 1.1)), + Text("${f2.format(value.endContext.firstWhere((element) => element.userId == userID).extra)} : ${f2.format(value.endContext.firstWhere((element) => element.userId != userID).extra)} VS", style: const TextStyle(height: 1.1)), + ]), + onTap: (){Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TlMatchResultView(record: value, initPlayerId: userID), + ), + );}, + )] + : [const Center(child: Text("No records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], + ); } } -class _TLHistory extends StatelessWidget{ +class _History extends StatelessWidget{ final List states; - const _TLHistory({required this.states}); + final Function update; + const _History({required this.states, required this.update}); @override Widget build(BuildContext context) { bool bigScreen = MediaQuery.of(context).size.width > 768; - List trData = [for (var state in states) if (state.tlSeason1.gamesPlayed > 9) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.rating)]; - List apmData = [for (var state in states) if (state.tlSeason1.apm != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.apm!)]; - List ppsData = [for (var state in states) if (state.tlSeason1.pps != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.pps!)]; - List vsData = [for (var state in states) if (state.tlSeason1.vs != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.vs!)]; - List appData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.app)]; - List dssData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.dss)]; - List dspData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.dsp)]; - List appdspData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.appdsp)]; - List vsapmData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.vsapm)]; - List cheeseData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.cheese)]; - List gbeData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.gbe)]; - List nyaappData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.nyaapp)]; - List areaData = [for (var state in states) if (state.tlSeason1.nerdStats != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.nerdStats!.area)]; - List estTrData = [for (var state in states) if (state.tlSeason1.estTr != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.estTr!.esttr)]; - List estaccData = [for (var state in states) if (state.tlSeason1.esttracc != null) FlSpot(state.state.millisecondsSinceEpoch.toDouble(), state.tlSeason1.esttracc!)]; return ListView(physics: const ClampingScrollPhysics(), children: states.isNotEmpty ? [ Column( children: [ - if(trData.length > 1) _HistoryChartThigy(data: trData, title: "Tetra Rating", yAxisTitle: "TR", bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),), - if(apmData.length > 1) _HistoryChartThigy(data: apmData, title: "Attack Per Minute", yAxisTitle: "APM", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), - if(ppsData.length > 1) _HistoryChartThigy(data: ppsData, title: "Pieces Per Second", yAxisTitle: "PPS", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), - if(vsData.length > 1) _HistoryChartThigy(data: vsData, title: "Versus Score", yAxisTitle: "VS", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), - if(appData.length > 1) _HistoryChartThigy(data: appData, title: "Attack Per Piece", yAxisTitle: "APP", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(dssData.length > 1) _HistoryChartThigy(data: dssData, title: bigScreen ? "Downstack Per Second" : "Downstack\nPer Second", yAxisTitle: "DS/S", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(dspData.length > 1) _HistoryChartThigy(data: dspData, title: bigScreen ? "Downstack Per Piece" : "Downstack\nPer Piece", yAxisTitle: "DS/P", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(appdspData.length > 1) _HistoryChartThigy(data: appdspData, title: "APP + DS/P", yAxisTitle: "APP + DS/P", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(vsapmData.length > 1) _HistoryChartThigy(data: vsapmData, title: "VS/APM", yAxisTitle: "VS/APM", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(cheeseData.length > 1) _HistoryChartThigy(data: cheeseData, title: "Cheese Index", yAxisTitle: "Cheese", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), - if(gbeData.length > 1) _HistoryChartThigy(data: gbeData, title: "Garbage Efficiency", yAxisTitle: "GbE", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(nyaappData.length > 1) _HistoryChartThigy(data: nyaappData, title: "Weighted APP", yAxisTitle: "wAPP", bigScreen: bigScreen, leftSpace: 48, yFormat: NumberFormat.compact(),), - if(areaData.length > 1) _HistoryChartThigy(data: areaData, title: "Area", yAxisTitle: "Area", bigScreen: bigScreen, leftSpace: 40, yFormat: NumberFormat.compact(),), - if(estTrData.length > 1) _HistoryChartThigy(data: estTrData, title: "Est. of TR", yAxisTitle: "eTR", bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),), - if(estaccData.length > 1) _HistoryChartThigy(data: estaccData, title: "Accuracy of Est.", yAxisTitle: "±eTR", bigScreen: bigScreen, leftSpace: 60, yFormat: NumberFormat.compact(explicitSign: true),), - if(trData.length <= 1 || apmData.length <= 1 || ppsData.length <= 1 || vsData.length <= 1 || appData.length <= 1 || dssData.length <= 1 || dspData.length <= 1 || appdspData.length <= 1 || vsapmData.length <= 1 || cheeseData.length <= 1 || gbeData.length <= 1 || nyaappData.length <= 1 || areaData.length <= 1 || estTrData.length <= 1 || estaccData.length <= 1) const Center(child: Text("Some charts aren't shown due to lack of data...", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) - // Why it's look like a garbage solution??? + DropdownButton( + items: chartsData, + value: chartsData[chartsIndex].value, + onChanged: (value) { + chartsIndex = chartsData.indexWhere((element) => element.value == value); + update(); + } + ), + if(chartsData[chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[chartsIndex].value!, title: "ss", yAxisTitle: chartsShortTitles[chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) + else const Center(child: Text("Not enough data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) ], ), ] : [const Center(child: Text("No history saved", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]); @@ -512,12 +508,12 @@ class _HistoryChartThigy extends StatelessWidget{ @override Widget build(BuildContext context) { double xInterval = bigScreen ? max(1, (data.last.x - data.first.x) / 6) : max(1, (data.last.x - data.first.x) / 3); - return AspectRatio( - aspectRatio: bigScreen ? 1.9 : 1.1, + return SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height - 100, child: Stack( children: [ - Row(mainAxisAlignment: MainAxisAlignment.center, children: [Text(title, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))]), - Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 75, 40, 48) : const EdgeInsets.fromLTRB(0, 80, 0, 48) , + Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 0, 48) , child: LineChart( LineChartData( lineBarsData: [LineChartBarData(spots: data)], @@ -558,7 +554,7 @@ class _RecordThingy extends StatelessWidget { return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; return ListView.builder( - physics: const ClampingScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), itemCount: 1, itemBuilder: (BuildContext context, int index) { return Column( @@ -881,7 +877,7 @@ class _OtherThingy extends StatelessWidget { return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; return ListView.builder( - physics: const ClampingScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), itemCount: 1, itemBuilder: (BuildContext context, int index) { return Column( diff --git a/pubspec.yaml b/pubspec.yaml index dd896dc..c9f1b7f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,6 +85,8 @@ flutter: - res/tetrio_badges/early-supporter.png - res/tetrio_badges/founder.png - res/tetrio_badges/galactic2x2_1.png + - res/tetrio_badges/galactic2x2_2.png + - res/tetrio_badges/galactic2x2_3.png - res/tetrio_badges/ggc_1.png - res/tetrio_badges/ggc_2.png - res/tetrio_badges/ggc_3.png diff --git a/res/tetrio_badges/galactic2x2_2.png b/res/tetrio_badges/galactic2x2_2.png new file mode 100644 index 0000000000000000000000000000000000000000..167cace2e99a58fc1bff02b01a8ebd4f6e8fbeb5 GIT binary patch literal 7790 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 literal 0 HcmV?d00001