Really wacky implementation of interactivity for history graph + very small things

This commit is contained in:
dan63047 2024-01-13 21:49:36 +03:00
parent e524952970
commit a6b3a0282a
19 changed files with 162 additions and 89 deletions

View File

@ -940,9 +940,9 @@ class TetraLeagueAlpha {
rd = json['rd'] != null ? json['rd']!.toDouble() : noTrRd;
rank = json['rank'] != null ? json['rank']!.toString() : 'z';
bestRank = json['bestrank'] != null ? json['bestrank']!.toString() : 'z';
apm = json['apm'] != null ? json['apm']!.toDouble() : null;
pps = json['pps'] != null ? json['pps']!.toDouble() : null;
vs = json['vs'] != null ? json['vs']!.toDouble() : null;
apm = json['apm']?.toDouble();
pps = json['pps']?.toDouble();
vs = json['vs']?.toDouble();
decaying = json['decaying'] ?? false;
standing = json['standing'] ?? -1;
percentile = json['percentile'] != null ? json['percentile'].toDouble() : rankCutoffs[rank];

View File

@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math';
import 'tetrio.dart';

View File

@ -18,7 +18,7 @@ import 'package:go_router/go_router.dart';
late final PackageInfo packageInfo;
late SharedPreferences prefs;
ColorScheme sheme = ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white);
ColorScheme sheme = const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white);
void setAccentColor(Color color){
sheme = ColorScheme.dark(primary: color, secondary: Colors.white);

View File

@ -17,7 +17,7 @@ final NumberFormat f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings
late String oldWindowTitle;
class CalcView extends StatefulWidget {
const CalcView({Key? key}) : super(key: key);
const CalcView({super.key});
@override
State<StatefulWidget> createState() => CalcState();

View File

@ -1,3 +1,5 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'dart:math';
import 'package:flutter/foundation.dart';
@ -28,8 +30,7 @@ class CompareView extends StatefulWidget {
final List<dynamic> redSide;
final Mode greenMode;
final Mode redMode;
const CompareView({Key? key, required this.greenSide, required this.redSide, required this.greenMode, required this.redMode})
: super(key: key);
const CompareView({super.key, required this.greenSide, required this.redSide, required this.greenMode, required this.redMode});
@override
State<StatefulWidget> createState() => CompareState();

View File

@ -12,7 +12,7 @@ Color pickerColor = Colors.cyanAccent;
Color currentColor = Colors.cyanAccent;
class CustomizationView extends StatefulWidget {
const CustomizationView({Key? key}) : super(key: key);
const CustomizationView({super.key});
@override
State<StatefulWidget> createState() => CustomizationState();
@ -67,7 +67,7 @@ class CustomizationState extends State<CustomizationView> {
child: ListView(
children: [
ListTile(
title: Text("Accent Color"),
title: const Text("Accent Color"),
trailing: ColorIndicator(HSVColor.fromColor(Theme.of(context).colorScheme.primary)),
onTap: () {
showDialog(
@ -111,11 +111,11 @@ class CustomizationState extends State<CustomizationView> {
),
]));
}),
ListTile(
const ListTile(
title: Text("Font"),
subtitle: Text("Not implemented"),
),
ListTile(
const ListTile(
title: Text("Stats Table in TL mathes list"),
subtitle: Text("Not implemented"),
),

View File

@ -1,8 +1,8 @@
// ignore_for_file: type_literal_in_constant_pattern
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:http/http.dart';
@ -12,7 +12,6 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/data_objects/tetrio_multiplayer_replay.dart';
import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/services/tetrio_crud.dart';
import 'package:tetra_stats/main.dart' show prefs;
@ -34,6 +33,7 @@ final TetrioService teto = TetrioService();
var chartsData = <DropdownMenuItem<List<FlSpot>>>[];
List _historyShortTitles = ["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;
late ScrollController _scrollController;
final NumberFormat _timeInSec = NumberFormat("#,###.###s.");
final NumberFormat secs = NumberFormat("00.###");
final NumberFormat _f2 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 2);
@ -42,7 +42,7 @@ final DateFormat _dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.lan
class MainView extends StatefulWidget {
final String? player;
const MainView({Key? key, this.player}) : super(key: key);
const MainView({super.key, this.player});
String get title => "Tetra Stats: $_titleNickname";
@ -58,11 +58,10 @@ String get40lTime(int microseconds){
return microseconds > 60000000 ? "${(microseconds/1000000/60).floor()}:${(secs.format(microseconds /1000000 % 60))}" : _timeInSec.format(microseconds / 1000000);
}
class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
class _MainState extends State<MainView> with TickerProviderStateMixin {
final bodyGlobalKey = GlobalKey();
bool _searchBoolean = false;
late TabController _tabController;
late ScrollController _scrollController;
late bool fixedScroll;
Widget _searchTextField() {
@ -313,13 +312,13 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
onRefresh: () {
return Future(() => changePlayer(snapshot.data![0].userId));
},
// notificationPredicate: (notification) {
// // with NestedScrollView local(depth == 2) OverscrollNotification are not sent
// if (!kIsWeb && (notification is OverscrollNotification || Platform.isIOS)) {
// return notification.depth == 2;
// }
// return notification.depth == 0;
// },
notificationPredicate: (notification) {
// with NestedScrollView local(depth == 2) OverscrollNotification are not sent
if (!kIsWeb && (notification is OverscrollNotification || Platform.isIOS)) {
return notification.depth == 2;
}
return notification.depth == 0;
},
child: NestedScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
@ -334,7 +333,7 @@ class _MainState extends State<MainView> with SingleTickerProviderStateMixin {
SliverToBoxAdapter(
child: TabBar(
controller: _tabController,
padding: EdgeInsets.all(0.0),
padding: const EdgeInsets.all(0.0),
isScrollable: true,
tabs: [
Tab(text: t.tetraLeague),
@ -423,18 +422,15 @@ class NavDrawer extends StatefulWidget {
}
class _NavDrawerState extends State<NavDrawer> {
late ScrollController _scrollController;
String homePlayerNickname = "Checking...";
@override
void initState() {
super.initState();
_setHomePlayerNickname(prefs.getString("player"));
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@ -584,8 +580,7 @@ class _History extends StatelessWidget{
@override
Widget build(BuildContext context) {
bool bigScreen = MediaQuery.of(context).size.width > 768;
return ListView(physics: const ClampingScrollPhysics(),
children: states.isNotEmpty ? [
return states.isNotEmpty ?
Column(
children: [
DropdownButton(
@ -599,55 +594,135 @@ class _History extends StatelessWidget{
if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, title: "ss", yAxisTitle: _historyShortTitles[_chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? _f2 : NumberFormat.compact(),)
else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)))
],
),
] : [Center(child: Text(t.noHistorySaved, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)))]);
)
: Center(child: Text(t.noHistorySaved, textAlign: TextAlign.center, style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 28)));
}
}
class _HistoryChartThigy extends StatelessWidget{
class _HistoryChartThigy extends StatefulWidget{
final List<FlSpot> data;
final String title;
final String yAxisTitle;
final bool bigScreen;
final double leftSpace;
final NumberFormat yFormat;
const _HistoryChartThigy({required this.data, required this.title, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat});
@override
State<_HistoryChartThigy> createState() => _HistoryChartThigyState();
}
class _HistoryChartThigyState extends State<_HistoryChartThigy> {
late double minX;
late double maxX;
late double minY;
late double maxY;
@override
void initState(){
super.initState();
minX = widget.data.first.x;
maxX = widget.data.last.x;
minY = widget.data.reduce((value, element){
num n = min(value.y, element.y);
if (value.y == n) {
return value;
} else {
return element;
}
}).y;
maxY = widget.data.reduce((value, element){
num n = max(value.y, element.y);
if (value.y == n) {
return value;
} else {
return element;
}
}).y;
}
@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);
double xScale = maxX - minX;
double xInterval = widget.bigScreen ? max(1, xScale / 6) : max(1, xScale / 3);
EdgeInsets padding = widget.bigScreen ? const EdgeInsets.fromLTRB(40, 30, 40, 30) : const EdgeInsets.fromLTRB(0, 40, 16, 48);
double graphStartX = padding.left+widget.leftSpace;
double graphEndX = MediaQuery.sizeOf(context).width - padding.right;
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 100,
child: Stack(
children: [
Padding( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48) ,
child: LineChart(
LineChartData(
lineBarsData: [LineChartBarData(spots: data)],
borderData: FlBorderData(show: false),
gridData: FlGridData(verticalInterval: xInterval),
titlesData: FlTitlesData(topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){
return value != meta.min && value != meta.max ? SideTitleWidget(
axisSide: meta.axisSide,
child: Text(DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))),
) : Container();
})),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: leftSpace, getTitlesWidget: (double value, TitleMeta meta){
return value != meta.min && value != meta.max ? SideTitleWidget(
axisSide: meta.axisSide,
child: Text(yFormat.format(value)),
) : Container();
}))),
lineTouchData: LineTouchData(touchTooltipData: LineTouchTooltipData( fitInsideHorizontally: true, fitInsideVertically: true, getTooltipItems: (touchedSpots) {
return [for (var v in touchedSpots) LineTooltipItem("${_f4.format(v.y)} $yAxisTitle \n", const TextStyle(), children: [TextSpan(text: _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(v.x.floor())))])];
},))
)
height: MediaQuery.of(context).size.height - 104,
child: Listener(
onPointerSignal: (signal) {
if (signal is PointerScrollEvent) {
double scrollPosRelativeX = (signal.position.dx - graphStartX) / (graphEndX - graphStartX);
double newMinX, newMaxX;
newMinX = minX - (xScale / 5e2) * signal.scrollDelta.dy * scrollPosRelativeX;
newMaxX = maxX + (xScale / 5e2) * signal.scrollDelta.dy * (1-scrollPosRelativeX);
if ((newMaxX - newMinX).isNegative) return;
setState(() {
minX = max(newMinX, widget.data.first.x);
maxX = min(newMaxX, widget.data.last.x);
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
});
}
},
child:
GestureDetector(
onDoubleTap: () {
setState(() {
minX = widget.data.first.x;
maxX = widget.data.last.x;
});
},
onHorizontalDragUpdate: (dragUpdDet) {
var horizontalDistance = dragUpdDet.primaryDelta ?? 0;
if (horizontalDistance == 0) return;
setState(() {
minX -= (xScale / 7e2) * horizontalDistance;
maxX -= (xScale / 7e2) * horizontalDistance;
if (minX < widget.data.first.x) {
minX = widget.data.first.x;
maxX = widget.data.first.x + xScale;
}
if (maxX > widget.data.last.x) {
maxX = widget.data.last.x;
minX = maxX - xScale;
}
});
},
child: Padding( padding: padding,
child: LineChart(
LineChartData(
lineBarsData: [LineChartBarData(spots: widget.data)],
clipData: const FlClipData.all(),
borderData: FlBorderData(show: false),
gridData: FlGridData(verticalInterval: xInterval),
minX: minX,
maxX: maxX,
titlesData: FlTitlesData(topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){
return value != meta.min && value != meta.max ? SideTitleWidget(
axisSide: meta.axisSide,
child: Text(DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))),
) : Container();
})),
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: widget.leftSpace, getTitlesWidget: (double value, TitleMeta meta){
return value != meta.min && value != meta.max ? SideTitleWidget(
axisSide: meta.axisSide,
child: Text(widget.yFormat.format(value)),
) : Container();
}))),
lineTouchData: LineTouchData(touchTooltipData: LineTouchTooltipData( fitInsideHorizontally: true, fitInsideVertically: true, getTooltipItems: (touchedSpots) {
return [for (var v in touchedSpots) LineTooltipItem("${_f4.format(v.y)} ${widget.yAxisTitle} \n", const TextStyle(), children: [TextSpan(text: _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(v.x.floor())))])];
},))
)
),
),
),
],
)
);
}
@ -655,7 +730,7 @@ class _HistoryChartThigy extends StatelessWidget{
class _RecordThingy extends StatelessWidget {
final RecordSingle? record;
const _RecordThingy({Key? key, required this.record}) : super(key: key);
const _RecordThingy({required this.record});
@override
Widget build(BuildContext context) {
@ -900,8 +975,7 @@ class _OtherThingy extends StatelessWidget {
final String? bio;
final Distinguishment? distinguishment;
final List<News>? newsletter;
const _OtherThingy({Key? key, required this.zen, required this.bio, required this.distinguishment, this.newsletter})
: super(key: key);
const _OtherThingy({required this.zen, required this.bio, required this.distinguishment, this.newsletter});
List<InlineSpan> getDistinguishmentTitle(String? text) {
if (distinguishment?.type == "twc") return [const TextSpan(text: "TETR.IO World Champion", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.yellowAccent))];

View File

@ -14,7 +14,7 @@ late String oldWindowTitle;
class MatchesView extends StatefulWidget {
final String userID;
final String username;
const MatchesView({Key? key, required this.userID, required this.username}) : super(key: key);
const MatchesView({super.key, required this.userID, required this.username});
@override
State<StatefulWidget> createState() => MatchesState();

View File

@ -22,7 +22,7 @@ final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSetting
class RankView extends StatefulWidget {
final List rank;
const RankView({Key? key, required this.rank}) : super(key: key);
const RankView({super.key, required this.rank});
@override
State<StatefulWidget> createState() => RankState();
@ -505,5 +505,5 @@ class _MyScatterSpot extends ScatterSpot {
String nickname;
//Color color;
//FlDotPainter painter = FlDotCirclePainter(color: color, radius: 2);
_MyScatterSpot(super.x, super.y, this.id, this.nickname, {FlDotPainter? super.dotPainter});
_MyScatterSpot(super.x, super.y, this.id, this.nickname, {super.dotPainter});
}

View File

@ -8,7 +8,7 @@ import 'package:window_manager/window_manager.dart';
import 'main_view.dart'; // lol
class RankAveragesView extends StatefulWidget {
const RankAveragesView({Key? key}) : super(key: key);
const RankAveragesView({super.key});
@override
State<StatefulWidget> createState() => RanksAverages();

View File

@ -15,7 +15,7 @@ import 'package:window_manager/window_manager.dart';
late String oldWindowTitle;
class SettingsView extends StatefulWidget {
const SettingsView({Key? key}) : super(key: key);
const SettingsView({super.key});
@override
State<StatefulWidget> createState() => SettingsState();
@ -236,9 +236,9 @@ class SettingsState extends State<SettingsView> {
},
),
),
ListTile(title: Text("Customization"),
subtitle: Text("I don't want to implement this"),
trailing: Icon(Icons.arrow_right),
ListTile(title: const Text("Customization"),
subtitle: const Text("I don't want to implement this"),
trailing: const Icon(Icons.arrow_right),
onTap: () {
Navigator.pushNamed(context, "/customization");
},),

View File

@ -12,7 +12,7 @@ final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.lang
class StateView extends StatefulWidget {
final TetrioPlayer state;
const StateView({Key? key, required this.state}) : super(key: key);
const StateView({super.key, required this.state});
@override
State<StatefulWidget> createState() => StateState();
@ -58,6 +58,6 @@ class StateState extends State<StateView> {
headerSliverBuilder: (context, value) {
return [SliverToBoxAdapter(child: UserThingy(player: widget.state, showStateTimestamp: true, setState: _justUpdate))];
},
body: TLThingy(tl: widget.state.tlSeason1, userID: widget.state.userId, states: [],))));
body: TLThingy(tl: widget.state.tlSeason1, userID: widget.state.userId, states: const [],))));
}
}

View File

@ -10,7 +10,7 @@ import 'package:window_manager/window_manager.dart';
class StatesView extends StatefulWidget {
final List<TetrioPlayer> states;
const StatesView({Key? key, required this.states}) : super(key: key);
const StatesView({super.key, required this.states});
@override
State<StatefulWidget> createState() => StatesState();

View File

@ -20,7 +20,7 @@ late String _oldWindowTitle;
final NumberFormat _f4 = NumberFormat.decimalPatternDigits(locale: LocaleSettings.currentLocale.languageCode, decimalDigits: 4);
class TLLeaderboardView extends StatefulWidget {
const TLLeaderboardView({Key? key}) : super(key: key);
const TLLeaderboardView({super.key});
@override
State<StatefulWidget> createState() => TLLeaderboardState();
@ -42,7 +42,7 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final NumberFormat _f2 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
final NumberFormat f2 = NumberFormat.decimalPattern(LocaleSettings.currentLocale.languageCode)..maximumFractionDigits = 2;
return Scaffold(
appBar: AppBar(
title: Text(t.tlLeaderboard),
@ -175,11 +175,11 @@ class TLLeaderboardState extends State<TLLeaderboardView> {
return ListTile(
leading: Text((index+1).toString(), style: bigScreen ? const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28) : null),
title: Text(allPlayers[index].username, style: const TextStyle(fontFamily: "Eurostile Round Extended")),
subtitle: Text(_sortBy == Stats.tr ? "${_f2.format(allPlayers[index].apm)} APM, ${_f2.format(allPlayers[index].pps)} PPS, ${_f2.format(allPlayers[index].vs)} VS, ${_f2.format(allPlayers[index].nerdStats.app)} APP, ${_f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}"),
subtitle: Text(_sortBy == Stats.tr ? "${f2.format(allPlayers[index].apm)} APM, ${f2.format(allPlayers[index].pps)} PPS, ${f2.format(allPlayers[index].vs)} VS, ${f2.format(allPlayers[index].nerdStats.app)} APP, ${f2.format(allPlayers[index].nerdStats.vsapm)} VS/APM" : "${_f4.format(allPlayers[index].getStatByEnum(_sortBy))} ${chartsShortTitles[_sortBy]}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("${_f2.format(allPlayers[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
Text("${f2.format(allPlayers[index].rating)} TR", style: bigScreen ? const TextStyle(fontSize: 28) : null),
Image.asset("res/tetrio_tl_alpha_ranks/${allPlayers[index].rank}.png", height: bigScreen ? 48 : 16),
],
),

View File

@ -29,8 +29,7 @@ Duration framesToTime(int frames){
class TlMatchResultView extends StatefulWidget {
final TetraLeagueAlphaRecord record;
final String initPlayerId;
const TlMatchResultView({Key? key, required this.record, required this.initPlayerId})
: super(key: key);
const TlMatchResultView({super.key, required this.record, required this.initPlayerId});
@override
State<StatefulWidget> createState() => TlMatchResultState();

View File

@ -13,7 +13,7 @@ final TetrioService teto = TetrioService();
late String oldWindowTitle;
class TrackedPlayersView extends StatefulWidget {
const TrackedPlayersView({Key? key}) : super(key: key);
const TrackedPlayersView({super.key});
@override
State<StatefulWidget> createState() => TrackedPlayersState();

View File

@ -37,7 +37,7 @@ class StatCellNum extends StatelessWidget {
RichText(
text: TextSpan(text: intf.format(integer),
children: [
TextSpan(text: fractionf.format(fraction).substring(1), style: TextStyle(fontSize: 16))
TextSpan(text: fractionf.format(fraction).substring(1), style: const TextStyle(fontSize: 16))
],
style: TextStyle(
fontFamily: "Eurostile Round Extended",

View File

@ -22,7 +22,7 @@ class TLThingy extends StatefulWidget {
final bool showTitle;
final bool bot;
final double? topTR;
const TLThingy({Key? key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.topTR}) : super(key: key);
const TLThingy({super.key, required this.tl, required this.userID, required this.states, this.showTitle = true, this.bot=false, this.topTR});
@override
State<TLThingy> createState() => _TLThingyState();

View File

@ -28,7 +28,7 @@ class UserThingy extends StatelessWidget {
final bool showStateTimestamp;
final Function setState;
const UserThingy({Key? key, required this.player, required this.showStateTimestamp, required this.setState}) : super(key: key);
const UserThingy({super.key, required this.player, required this.showStateTimestamp, required this.setState});
@override
Widget build(BuildContext context) {