diff --git a/lib/views/customization_view.dart b/lib/views/customization_view.dart index 4d85148..3449d8a 100644 --- a/lib/views/customization_view.dart +++ b/lib/views/customization_view.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:tetra_stats/views/settings_view.dart' show subtitleStyle; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:window_manager/window_manager.dart'; @@ -19,6 +20,7 @@ class CustomizationView extends StatefulWidget { class CustomizationState extends State { late SharedPreferences prefs; late bool oskKagariGimmick; + late bool sheetbotRadarGraphs; void changeColor(Color color) { setState(() => pickerColor = color); @@ -47,6 +49,11 @@ class CustomizationState extends State { } else { oskKagariGimmick = true; } + if (prefs.getBool("sheetbotRadarGraphs") != null) { + sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")!; + } else { + sheetbotRadarGraphs = false; + } } ThemeData getTheme(BuildContext context, Color color){ @@ -64,7 +71,7 @@ class CustomizationState extends State { } return Scaffold( appBar: AppBar( - title: Text(t.settings), + title: Text(t.customization), ), backgroundColor: Colors.black, body: SafeArea( @@ -105,12 +112,20 @@ class CustomizationState extends State { // subtitle: Text("Not implemented"), // ), ListTile(title: Text(t.oskKagari), - subtitle: Text(t.oskKagariDescription), + subtitle: Text(t.oskKagariDescription, style: subtitleStyle), trailing: Switch(value: oskKagariGimmick, onChanged: (bool value){ prefs.setBool("oskKagariGimmick", value); setState(() { oskKagariGimmick = value; }); + }),), + ListTile(title: Text("Sheetbot-like behavior for radar graphs"), + subtitle: Text(t.oskKagariDescription, style: subtitleStyle), + trailing: Switch(value: sheetbotRadarGraphs, onChanged: (bool value){ + prefs.setBool("sheetbotRadarGraphs", value); + setState(() { + sheetbotRadarGraphs = value; + }); }),) ], )), diff --git a/lib/widgets/graphs.dart b/lib/widgets/graphs.dart index f8dd910..366d0ba 100644 --- a/lib/widgets/graphs.dart +++ b/lib/widgets/graphs.dart @@ -1,10 +1,269 @@ +import 'dart:math'; + import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:tetra_stats/main.dart' show prefs; import 'package:flutter/material.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; +class MyRadarChartPainter extends RadarChartPainter{ + MyRadarChartPainter() : super() { + _backgroundPaint = Paint() + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + _borderPaint = Paint()..style = PaintingStyle.stroke; + + _gridPaint = Paint()..style = PaintingStyle.stroke; + + _tickPaint = Paint()..style = PaintingStyle.stroke; + + _graphPaint = Paint(); + _graphBorderPaint = Paint(); + _graphPointPaint = Paint(); + _ticksTextPaint = TextPainter(); + _titleTextPaint = TextPainter(); + sheetbotRadarGraphs = prefs.getBool("sheetbotRadarGraphs")??false; + } + late Paint _borderPaint; + late Paint _backgroundPaint; + late Paint _gridPaint; + late Paint _tickPaint; + late Paint _graphPaint; + late Paint _graphBorderPaint; + late Paint _graphPointPaint; + + late TextPainter _ticksTextPaint; + late TextPainter _titleTextPaint; + + late bool sheetbotRadarGraphs; + + @override + double getChartCenterValue(RadarChartData data) { + final dataSetMaxValue = sheetbotRadarGraphs ? max(data.maxEntry.value, data.minEntry.value.abs()) : data.maxEntry.value; + final dataSetMinValue = data.minEntry.value; + final tickSpace = getSpaceBetweenTicks(data); + final centerValue = (dataSetMinValue < 0 && sheetbotRadarGraphs) ? 0.0 : dataSetMinValue; + + return dataSetMaxValue == dataSetMinValue + ? getDefaultChartCenterValue() + : centerValue; + } + + @override + double getSpaceBetweenTicks(RadarChartData data) { + final defaultCenterValue = getDefaultChartCenterValue(); + final dataSetMaxValue = sheetbotRadarGraphs ? max(data.maxEntry.value, data.minEntry.value.abs()) : data.maxEntry.value; + final dataSetMinValue = (data.minEntry.value < 0 && sheetbotRadarGraphs) ? 0.0 : data.minEntry.value; + final tickSpace = sheetbotRadarGraphs ? dataSetMaxValue / data.tickCount : (dataSetMaxValue - dataSetMinValue) / data.tickCount; + final defaultTickSpace = + (dataSetMaxValue - defaultCenterValue) / (data.tickCount + 1); + + return dataSetMaxValue == dataSetMinValue ? defaultTickSpace : tickSpace; + } + + @override + double getScaledPoint(RadarEntry point, double radius, RadarChartData data) { + final centerValue = getChartCenterValue(data); + final distanceFromPointToCenter = point.value - centerValue; + final distanceFromMaxToCenter = max(data.maxEntry.value, data.minEntry.value.abs()) - centerValue; + + if (distanceFromMaxToCenter == 0) { + return radius * distanceFromPointToCenter / 0.001; + } + + return radius * distanceFromPointToCenter / distanceFromMaxToCenter; + } + + @override + double getFirstTickValue(RadarChartData data) { + final defaultCenterValue = getDefaultChartCenterValue(); + final dataSetMaxValue = sheetbotRadarGraphs ? max(data.maxEntry.value, data.minEntry.value.abs()) : data.maxEntry.value; + final dataSetMinValue = (data.minEntry.value < 0 && sheetbotRadarGraphs) ? 0.0 : data.minEntry.value; + + return dataSetMaxValue == dataSetMinValue + ? (dataSetMaxValue - defaultCenterValue) / (data.tickCount + 1) + + defaultCenterValue + : dataSetMinValue; + } + + @override + void drawTicks( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final size = canvasWrapper.size; + + final centerX = radarCenterX(size); + final centerY = radarCenterY(size); + final centerOffset = Offset(centerX, centerY); + + /// controls Radar chart size + final radius = radarRadius(size); + + _backgroundPaint.color = data.radarBackgroundColor; + + _borderPaint + ..color = data.radarBorderData.color + ..strokeWidth = data.radarBorderData.width; + + if (data.radarShape == RadarShape.circle) { + /// draw radar background + canvasWrapper + ..drawCircle(centerOffset, radius, _backgroundPaint) + + /// draw radar border + ..drawCircle(centerOffset, radius, _borderPaint); + } else { + final path = + _generatePolygonPath(centerX, centerY, radius, data.titleCount); + + /// draw radar background + canvasWrapper + ..drawPath(path, _backgroundPaint) + + /// draw radar border + ..drawPath(path, _borderPaint); + } + + final tickSpace = getSpaceBetweenTicks(data); + final ticks = []; + var tickValue = getFirstTickValue(data); + + for (var i = 0; i <= data.tickCount; i++) { + ticks.add(tickValue); + tickValue += tickSpace; + } + + final tickDistance = radius / (ticks.length-1); + + _tickPaint + ..color = data.tickBorderData.color + ..strokeWidth = data.tickBorderData.width; + + /// draw radar ticks + ticks.sublist(1, ticks.length).asMap().forEach( + (index, tick) { + final tickRadius = tickDistance * (index + 1); + if (data.radarShape == RadarShape.circle) { + canvasWrapper.drawCircle(centerOffset, tickRadius, _tickPaint); + } else { + canvasWrapper.drawPath( + _generatePolygonPath(centerX, centerY, tickRadius, data.titleCount), + _tickPaint, + ); + } + + _ticksTextPaint + ..text = TextSpan( + text: percentage.format(tick), + style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle), + ) + ..textDirection = TextDirection.ltr + ..layout(maxWidth: size.width); + canvasWrapper.drawText( + _ticksTextPaint, + Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height/2), + ); + }, + ); + } + + Path _generatePolygonPath( + double centerX, + double centerY, + double radius, + int count, + ) { + final path = Path()..moveTo(centerX, centerY - radius); + final angle = (2 * pi) / count; + for (var index = 0; index < count; index++) { + final xAngle = cos(angle * index - pi / 2); + final yAngle = sin(angle * index - pi / 2); + path.lineTo(centerX + radius * xAngle, centerY + radius * yAngle); + } + path.lineTo(centerX, centerY - radius); + return path; + } +} + +class MyRadarChartLeaf extends RadarChartLeaf{ + MyRadarChartLeaf({required super.data, required super.targetData}); + + @override + RenderRadarChart createRenderObject(BuildContext context) => MyRenderRadarChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + ); +} + +class MyRenderRadarChart extends RenderRadarChart{ + MyRenderRadarChart(super.context, super.data, super.targetData, super.textScaler); + + @override + RadarChartPainter painter = MyRadarChartPainter(); +} + +class MyRadarChart extends ImplicitlyAnimatedWidget { + const MyRadarChart( + this.data, { + super.key, + Duration swapAnimationDuration = const Duration(milliseconds: 150), + Curve swapAnimationCurve = Curves.linear, + }) : super( + duration: swapAnimationDuration, + curve: swapAnimationCurve, + ); + + /// Determines how the [RadarChart] should be look like. + final RadarChartData data; + + @override + RadarChartState createState() => RadarChartState(); +} + +class RadarChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [RadarChartData] to the new one. + RadarChartDataTween? _radarChartDataTween; + + @override + Widget build(BuildContext context) { + final showingData = _getDate(); + + return MyRadarChartLeaf( + data: _radarChartDataTween!.evaluate(animation), + targetData: showingData, + ); + } + + RadarChartData _getDate() { + return widget.data; + } + + @override + void forEachTween(TweenVisitor visitor) { + _radarChartDataTween = visitor( + _radarChartDataTween, + widget.data, + (dynamic value) => + RadarChartDataTween(begin: value as RadarChartData, end: widget.data), + ) as RadarChartDataTween?; + } +} + class Graphs extends StatelessWidget{ + const Graphs( this.apm, this.pps, @@ -37,7 +296,7 @@ class Graphs extends StatelessWidget{ child: SizedBox( height: 310, width: 310, - child: RadarChart( + child: MyRadarChart( RadarChartData( radarShape: RadarShape.polygon, tickCount: 4, @@ -114,7 +373,7 @@ class Graphs extends StatelessWidget{ child: SizedBox( height: 310, width: 310, - child: RadarChart( + child: MyRadarChart( RadarChartData( radarShape: RadarShape.polygon, tickCount: 4, @@ -169,7 +428,7 @@ class Graphs extends StatelessWidget{ child: SizedBox( height: 310, width: 310, - child: RadarChart( + child: MyRadarChart( RadarChartData( radarShape: RadarShape.polygon, tickCount: 4, diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index 6900061..bec9f10 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -3,7 +3,7 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:tetra_stats/gen/strings.g.dart'; -import 'package:tetra_stats/main.dart'; +import 'package:tetra_stats/main.dart' show prefs; import 'package:tetra_stats/utils/colors_functions.dart'; import 'package:tetra_stats/utils/numers_formats.dart'; import 'package:tetra_stats/widgets/gauget_num.dart'; diff --git a/lib/widgets/vs_graphs.dart b/lib/widgets/vs_graphs.dart index 2ac5eea..0b78adc 100644 --- a/lib/widgets/vs_graphs.dart +++ b/lib/widgets/vs_graphs.dart @@ -1,5 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:tetra_stats/widgets/graphs.dart' show MyRadarChart; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; @@ -31,7 +32,7 @@ class VsGraphs extends StatelessWidget{ child: SizedBox( height: 310, width: 310, - child: RadarChart( + child: MyRadarChart( RadarChartData( radarShape: RadarShape.polygon, tickCount: 4, @@ -134,7 +135,7 @@ class VsGraphs extends StatelessWidget{ child: SizedBox( height: 310, width: 310, - child: RadarChart( + child: MyRadarChart( RadarChartData( radarShape: RadarShape.polygon, tickCount: 4, @@ -211,7 +212,7 @@ class VsGraphs extends StatelessWidget{ child: SizedBox( height: 310, width: 310, - child: RadarChart( + child: MyRadarChart( RadarChartData( radarShape: RadarShape.polygon, tickCount: 4,