From d0ead79068412da4dab31283a142da146b9f450a Mon Sep 17 00:00:00 2001 From: dan63047 Date: Mon, 22 Jan 2024 21:00:24 +0300 Subject: [PATCH] 1.4.0? --- .github/workflows/main.yml | 62 +++---- lib/data_objects/tetrio.dart | 2 +- lib/gen/strings.g.dart | 24 ++- lib/services/tetrio_crud.dart | 29 ++- lib/views/main_view.dart | 252 ++++++++++++++++--------- lib/views/rank_averages_view.dart | 298 +++++++++++++++++++++--------- lib/views/tl_match_view.dart | 38 ++-- lib/widgets/stat_sell_num.dart | 6 +- lib/widgets/tl_thingy.dart | 2 +- lib/widgets/user_thingy.dart | 21 +-- pubspec.yaml | 2 +- res/i18n/strings.i18n.json | 5 + res/i18n/strings_ru.i18n.json | 5 + 13 files changed, 502 insertions(+), 244 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd64ba6..f22269d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,37 +40,37 @@ jobs: tag: Auto-${{ github.run_number }} body: Builded with GitHub Action workflow token: ${{ secrets.TOKEN }} - build-and-release-linux: - name: Build Linux App - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: subosito/flutter-action@v1 - - uses: ashutoshvarma/setup-ninja@master - with: - channel: 'stable' - flutter-version: '3.16.5' - - name: Install project dependencies - run: flutter pub get - - name: Build artifacts - run: flutter build linux --release - - name: Archive Release - uses: thedoctor0/zip-release@master - with: - type: 'zip' - filename: TetraStats-${{github.ref_name}}-windows.zip - directory: build/linux/x64/runner/Release/bundle - - name: Push to Releases - uses: ncipollo/release-action@v1 - with: - prerelease: true - allowUpdates: true - replacesArtifacts: false - discussionCategory: autobuilded-releases - artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip" - tag: Auto-${{ github.run_number }} - body: Builded with GitHub Action workflow - token: ${{ secrets.TOKEN }} + # build-and-release-linux: + # name: Build Linux App + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 + # - uses: subosito/flutter-action@v1 + # - uses: ashutoshvarma/setup-ninja@master + # with: + # channel: 'stable' + # flutter-version: '3.16.5' + # - name: Install project dependencies + # run: flutter pub get + # - name: Build artifacts + # run: flutter build linux --release + # - name: Archive Release + # uses: thedoctor0/zip-release@master + # with: + # type: 'zip' + # filename: TetraStats-${{github.ref_name}}-windows.zip + # directory: build/linux/x64/runner/Release/bundle + # - name: Push to Releases + # uses: ncipollo/release-action@v1 + # with: + # prerelease: true + # allowUpdates: true + # replacesArtifacts: false + # discussionCategory: autobuilded-releases + # artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip" + # tag: Auto-${{ github.run_number }} + # body: Builded with GitHub Action workflow + # token: ${{ secrets.TOKEN }} build-and-release-android: name: Build Android App runs-on: ubuntu-latest diff --git a/lib/data_objects/tetrio.dart b/lib/data_objects/tetrio.dart index e7540e9..5e555b8 100644 --- a/lib/data_objects/tetrio.dart +++ b/lib/data_objects/tetrio.dart @@ -661,7 +661,7 @@ class NerdStats { dsp = ((_vs / 100) - (_apm / 60)) / _pps; appdsp = app + dsp; cheese = (dsp * 150) + ((vsapm - 2) * 50) + (0.6 - app) * 125; - gbe = ((app * dss) / _pps) * 2; + gbe = app * dsp * 2; nyaapp = app - 5 * tan(radians((cheese / -30) + 1)); area = _apm * 1 + _pps * 45 + _vs * 0.444 + app * 185 + dss * 175 + dsp * 450 + gbe * 315; } diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart index 752c53a..67be3b4 100644 --- a/lib/gen/strings.g.dart +++ b/lib/gen/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 994 (497 per locale) +/// Strings: 1004 (502 per locale) /// -/// Built on 2024-01-05 at 16:51 UTC +/// Built on 2024-01-22 at 17:10 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -182,6 +182,8 @@ class Translations implements BaseTranslations { String get anonRecord => 'Guests are not allowed to set records'; String get notEnoughData => 'Not enough data'; String get noHistorySaved => 'No history saved'; + String get pseudoTooltipHeaderInit => 'Hover over point'; + String get pseudoTooltipFooterInit => 'to see detailed data'; String obtainDate({required Object date}) => 'Obtained ${date}'; String fetchDate({required Object date}) => 'Fetched ${date}'; String get exactGametime => 'Exact gametime'; @@ -250,6 +252,9 @@ class Translations implements BaseTranslations { String get match => 'Match'; String roundNumber({required Object n}) => 'Round ${n}'; String get statsFor => 'Stats for'; + String get matchLength => 'Match Length'; + String get roundLength => 'Round Length'; + String get winner => 'Winner'; String get registred => 'Registred'; String get playedTL => 'Played Tetra League'; String get winChance => 'Win Chance'; @@ -763,6 +768,8 @@ class _StringsRu implements Translations { @override String get anonRecord => 'Гостям нельзя ставить рекорды'; @override String get notEnoughData => 'Недостаточно данных'; @override String get noHistorySaved => 'Нет сохранённой истории'; + @override String get pseudoTooltipHeaderInit => 'Наведите курсор на точку'; + @override String get pseudoTooltipFooterInit => 'чтобы узнать подробности'; @override String obtainDate({required Object date}) => 'Получено ${date}'; @override String fetchDate({required Object date}) => 'На момент ${date}'; @override String get exactGametime => 'Время, проведённое в игре'; @@ -831,6 +838,9 @@ class _StringsRu implements Translations { @override String get match => 'Матч'; @override String roundNumber({required Object n}) => 'Раунд ${n}'; @override String get statsFor => 'Статистика за'; + @override String get matchLength => 'Продолжительность матча'; + @override String get roundLength => 'Продолжительность раунда'; + @override String get winner => 'Победитель'; @override String get registred => 'Зарегистрирован'; @override String get playedTL => 'Играл в Тетра Лигу'; @override String get winChance => 'Шансы на победу'; @@ -1336,6 +1346,8 @@ extension on Translations { case 'anonRecord': return 'Guests are not allowed to set records'; case 'notEnoughData': return 'Not enough data'; case 'noHistorySaved': return 'No history saved'; + case 'pseudoTooltipHeaderInit': return 'Hover over point'; + case 'pseudoTooltipFooterInit': return 'to see detailed data'; case 'obtainDate': return ({required Object date}) => 'Obtained ${date}'; case 'fetchDate': return ({required Object date}) => 'Fetched ${date}'; case 'exactGametime': return 'Exact gametime'; @@ -1404,6 +1416,9 @@ extension on Translations { case 'match': return 'Match'; case 'roundNumber': return ({required Object n}) => 'Round ${n}'; case 'statsFor': return 'Stats for'; + case 'matchLength': return 'Match Length'; + case 'roundLength': return 'Round Length'; + case 'winner': return 'Winner'; case 'registred': return 'Registred'; case 'playedTL': return 'Played Tetra League'; case 'winChance': return 'Win Chance'; @@ -1843,6 +1858,8 @@ extension on _StringsRu { case 'anonRecord': return 'Гостям нельзя ставить рекорды'; case 'notEnoughData': return 'Недостаточно данных'; case 'noHistorySaved': return 'Нет сохранённой истории'; + case 'pseudoTooltipHeaderInit': return 'Наведите курсор на точку'; + case 'pseudoTooltipFooterInit': return 'чтобы узнать подробности'; case 'obtainDate': return ({required Object date}) => 'Получено ${date}'; case 'fetchDate': return ({required Object date}) => 'На момент ${date}'; case 'exactGametime': return 'Время, проведённое в игре'; @@ -1911,6 +1928,9 @@ extension on _StringsRu { case 'match': return 'Матч'; case 'roundNumber': return ({required Object n}) => 'Раунд ${n}'; case 'statsFor': return 'Статистика за'; + case 'matchLength': return 'Продолжительность матча'; + case 'roundLength': return 'Продолжительность раунда'; + case 'winner': return 'Победитель'; case 'registred': return 'Зарегистрирован'; case 'playedTL': return 'Играл в Тетра Лигу'; case 'winChance': return 'Шансы на победу'; diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index cc0c5bd..8a97d77 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -57,6 +57,7 @@ const String createTetrioTLReplayStats = ''' CREATE TABLE IF NOT EXISTS "tetrioTLReplayStats" ( "id" TEXT NOT NULL, "data" TEXT NOT NULL, + "freyhoe" TEXT NOT NULL, PRIMARY KEY("id") ) '''; @@ -70,8 +71,8 @@ class TetrioService extends DB { final Map> _newsCache = {}; final Map> _topTRcache = {}; final Map _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} - //final client = UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); - final client = UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()); + final client = UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); + //final client = UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()); static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; late final StreamController>> _tetrioStreamController; @@ -129,16 +130,28 @@ class TetrioService extends DB { Future> szyGetReplay(String replayID) async { try{ + // read from cache var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID); return cached.value; }catch (e){ // actually going to obtain } - Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); - var downloadPath = await getDownloadsDirectory(); - downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); - var replayFile = File("${downloadPath.path}/$replayID.ttrm"); - if (replayFile.existsSync()) return [replayFile.readAsStringSync(), replayFile.readAsBytesSync()]; + + Uri url; + if (kIsWeb) { + url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID}); + } else { + url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); + } + + // trying to obtain replay from download directory first + if (!kIsWeb){ // can't obtain download directory on web + var downloadPath = await getDownloadsDirectory(); + downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); + var replayFile = File("${downloadPath.path}/$replayID.ttrm"); + if (replayFile.existsSync()) return [replayFile.readAsStringSync(), replayFile.readAsBytesSync()]; + } + try{ final response = await client.get(url); @@ -533,10 +546,12 @@ class TetrioService extends DB { Future deleteTLMatch(String matchID) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); + final rID = (await db.query(tetraLeagueMatchesTable, where: '$idCol = ?', whereArgs: [matchID])).first[replayID]; final results = await db.delete(tetraLeagueMatchesTable, where: '$idCol = ?', whereArgs: [matchID]); if (results != 1) { throw CouldNotDeleteMatch(); } + await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]); } Future> fetchRecords(String userID) async { diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 2625613..2f0d9e6 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -591,7 +591,7 @@ class _History extends StatelessWidget{ update(); } ), - 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(),) + if(chartsData[_chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[_chartsIndex].value!, 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))) ], ) @@ -601,49 +601,43 @@ class _History extends StatelessWidget{ class _HistoryChartThigy extends StatefulWidget{ final List 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}); + const _HistoryChartThigy({required this.data, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat}); @override State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); } class _HistoryChartThigyState extends State<_HistoryChartThigy> { + late String previousAxisTitle; late double minX; late double maxX; late double minY; late double actualMinY; late double maxY; late double actualMaxY; + late double xScale; + late double yScale; + String headerTooltip = t.pseudoTooltipHeaderInit; + String footerTooltip = t.pseudoTooltipFooterInit; + int hoveredPointId = -1; + double scaleFactor = 5e2; + double dragFactor = 7e2; @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; + setMinMaxY(); + previousAxisTitle = widget.yAxisTitle; actualMaxY = maxY; actualMinY = minY; + recalculateScales(); } @override @@ -651,20 +645,93 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { super.dispose(); actualMinY = 0; minY = 0; - +} + + void setMinMaxY(){ + actualMinY = widget.data.reduce((value, element){ + num n = min(value.y, element.y); + if (value.y == n) { + return value; + } else { + return element; + } + }).y; + actualMaxY = widget.data.reduce((value, element){ + num n = max(value.y, element.y); + if (value.y == n) { + return value; + } else { + return element; + } + }).y; + minY = actualMinY; + maxY = actualMaxY; + } + + void recalculateScales(){ + xScale = maxX - minX; + yScale = maxY - minY; + } + + void dragHandler(DragUpdateDetails dragUpdDet){ + setState(() { + minX -= (xScale / dragFactor) * dragUpdDet.delta.dx; + maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx; + minY += (yScale / dragFactor) * dragUpdDet.delta.dy; + maxY += (yScale / dragFactor) * dragUpdDet.delta.dy; + + 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; + } + if(minY < actualMinY){ + minY = actualMinY; + maxY = actualMinY + yScale; + } + if(maxY > actualMaxY){ + maxY = actualMaxY; + minY = actualMaxY - yScale; + } + }); + } + + void scaleHandler(ScaleUpdateDetails details, GlobalKey> graphKey, double graphStartX, double graphEndX){ + RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox; + Offset graphPosition = graphBox.localToGlobal(Offset.zero); + double scrollPosRelativeX = (details.focalPoint.dx - graphStartX) / (graphEndX - graphStartX); + double scrollPosRelativeY = (details.focalPoint.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height + double newMinX, newMaxX, newMinY, newMaxY; + newMinX = minX - (xScale / scaleFactor) * (details.horizontalScale-1) * scrollPosRelativeX; + newMaxX = maxX + (xScale / scaleFactor) * (details.horizontalScale-1) * (1-scrollPosRelativeX); + newMinY = minY - (yScale / scaleFactor) * (details.horizontalScale-1) * (1-scrollPosRelativeY); + newMaxY = maxY + (yScale / scaleFactor) * (details.horizontalScale-1) * scrollPosRelativeY; + if ((newMaxX - newMinX).isNegative) return; + if ((newMaxY - newMinY).isNegative) return; + setState(() { + minX = max(newMinX, widget.data.first.x); + maxX = min(newMaxX, widget.data.last.x); + minY = max(newMinY, actualMinY); + maxY = min(newMaxY, actualMaxY); + recalculateScales(); + }); } @override Widget build(BuildContext context) { GlobalKey graphKey = GlobalKey(); - double xScale = maxX - minX; - double yScale = maxY - minY; - double scaleFactor = 5e2; - double dragFactor = 7e2; 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; + if (previousAxisTitle != widget.yAxisTitle) { + setMinMaxY(); + recalculateScales(); + previousAxisTitle = widget.yAxisTitle; + } return SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 104, @@ -688,73 +755,90 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> { maxX = min(newMaxX, widget.data.last.x); minY = max(newMinY, actualMinY); maxY = min(newMaxY, actualMaxY); - _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); + recalculateScales(); + _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); // TODO: find a better way to stop scrolling in NestedScrollView }); } }, child: GestureDetector( - behavior: HitTestBehavior.opaque, + behavior: HitTestBehavior.translucent, onDoubleTap: () { setState(() { minX = widget.data.first.x; maxX = widget.data.last.x; minY = actualMinY; maxY = actualMaxY; + recalculateScales(); }); }, - onPanUpdate: (dragUpdDet) { - print(dragUpdDet); - - setState(() { - minX -= (xScale / dragFactor) * dragUpdDet.delta.dx; - maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx; - minY += (yScale / dragFactor) * dragUpdDet.delta.dy; - maxY += (yScale / dragFactor) * dragUpdDet.delta.dy; - - 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: AbsorbPointer( - child: Padding( padding: padding, - child: LineChart( - key: graphKey, - LineChartData( - lineBarsData: [LineChartBarData(spots: widget.data)], - clipData: const FlClipData.all(), - borderData: FlBorderData(show: false), - gridData: FlGridData(verticalInterval: xInterval), - minX: minX, - maxX: maxX, - minY: minY, - maxY: maxY, - 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())))])]; - },)) + // TODO: onScaleUpdate:(details) => scaleHandler(details, graphKey, graphStartX, graphEndX), + // TODO: Figure out wtf is going on with gestures + // TODO: Somehow highlight touched spot (handleBuiltInTouches breaks getTooltipItems and getTouchedSpotIndicator) + child: Padding( padding: padding, + child: Stack( + children: [ + LineChart( + key: graphKey, + LineChartData( + lineBarsData: [LineChartBarData(spots: widget.data)], + clipData: const FlClipData.all(), + borderData: FlBorderData(show: false), + gridData: FlGridData(verticalInterval: xInterval), + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + 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( + handleBuiltInTouches: false, + touchCallback:(touchEvent, touchResponse) { + if (touchEvent is FlPanUpdateEvent){ + dragHandler(touchEvent.details); + return; + } + if (touchEvent is FlPointerHoverEvent){ + setState(() { + if (touchResponse?.lineBarSpots?.first == null) { + hoveredPointId = -1; + } else { + hoveredPointId = touchResponse!.lineBarSpots!.first.spotIndex; + headerTooltip = "${_f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}"; + footerTooltip = _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor())); + } + }); + } + if (touchEvent is FlPointerExitEvent){ + setState(() {hoveredPointId = -1;}); + } + }, + ) + ) + ), + Padding( + padding: EdgeInsets.only(left: widget.leftSpace), + child: Column( + children: [ + AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24, color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(headerTooltip)), + AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round", color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(footerTooltip)), + ], + ), ) - ), - ), + ], + ), ), ), ) @@ -1031,7 +1115,7 @@ class _OtherThingy extends StatelessWidget { ))); break; default: - result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold))); + result.add(TextSpan(text: " $shit", style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white))); } } return result; @@ -1055,7 +1139,7 @@ class _OtherThingy extends StatelessWidget { return ListTile( title: RichText( text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16), + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), text: t.newsParts.leaderboardStart, children: [ TextSpan(text: "№${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), @@ -1070,7 +1154,7 @@ class _OtherThingy extends StatelessWidget { return ListTile( title: RichText( text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16), + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), text: t.newsParts.personalbest, children: [ TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), @@ -1093,7 +1177,7 @@ class _OtherThingy extends StatelessWidget { return ListTile( title: RichText( text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16), + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), text: t.newsParts.badgeStart, children: [ TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), @@ -1115,7 +1199,7 @@ class _OtherThingy extends StatelessWidget { return ListTile( title: RichText( text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16), + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), text: t.newsParts.rankupStart, children: [ TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), @@ -1137,7 +1221,7 @@ class _OtherThingy extends StatelessWidget { return ListTile( title: RichText( text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16), + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), text: t.newsParts.supporterStart, children: [ TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) @@ -1158,7 +1242,7 @@ class _OtherThingy extends StatelessWidget { return ListTile( title: RichText( text: TextSpan( - style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16), + style: const TextStyle(fontFamily: 'Eurostile Round', fontSize: 16, color: Colors.white), text: t.newsParts.supporterGiftStart, children: [ TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) diff --git a/lib/views/rank_averages_view.dart b/lib/views/rank_averages_view.dart index f236dc1..1d16569 100644 --- a/lib/views/rank_averages_view.dart +++ b/lib/views/rank_averages_view.dart @@ -1,11 +1,14 @@ import 'dart:io'; +import 'dart:math'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/views/main_view.dart' show MainView; +import 'package:tetra_stats/widgets/user_thingy.dart' show textShadow; import 'package:window_manager/window_manager.dart'; var _chartsShortTitlesDropdowns = [for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)]; @@ -31,6 +34,22 @@ class RankView extends StatefulWidget { class RankState extends State with SingleTickerProviderStateMixin { late ScrollController _scrollController; late TabController _tabController; + late String previousAxisTitles; + late double minX; + late double actualMinX; + late double maxX; + late double actualMaxX; + late double minY; + late double actualMinY; + late double maxY; + late double actualMaxY; + late double xScale; + late double yScale; + String headerTooltip = t.pseudoTooltipHeaderInit; + String footerTooltip = t.pseudoTooltipFooterInit; + int hoveredPointId = -1; + double scaleFactor = 5e2; + double dragFactor = 7e2; @override void initState() { @@ -41,6 +60,57 @@ class RankState extends State with SingleTickerProviderStateMixin { windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}"); } super.initState(); + previousAxisTitles = _chartsX.toString()+_chartsY.toString(); + recalculateBoundaries(); + resetScale(); + } + + void recalculateBoundaries(){ + actualMinX = (widget.rank[1]["entries"] as List).reduce((value, element) { + num n = min(value.getStatByEnum(_chartsX), element.getStatByEnum(_chartsX)); + if (value.getStatByEnum(_chartsX) == n) { + return value; + } else { + return element; + } + }).getStatByEnum(_chartsX) as double; + actualMaxX = (widget.rank[1]["entries"] as List).reduce((value, element) { + num n = max(value.getStatByEnum(_chartsX), element.getStatByEnum(_chartsX)); + if (value.getStatByEnum(_chartsX) == n) { + return value; + } else { + return element; + } + }).getStatByEnum(_chartsX) as double; + actualMinY = (widget.rank[1]["entries"] as List).reduce((value, element) { + num n = min(value.getStatByEnum(_chartsY), element.getStatByEnum(_chartsY)); + if (value.getStatByEnum(_chartsY) == n) { + return value; + } else { + return element; + } + }).getStatByEnum(_chartsY) as double; + actualMaxY = (widget.rank[1]["entries"] as List).reduce((value, element) { + num n = max(value.getStatByEnum(_chartsY), element.getStatByEnum(_chartsY)); + if (value.getStatByEnum(_chartsY) == n) { + return value; + } else { + return element; + } + }).getStatByEnum(_chartsY) as double; + } + + void resetScale(){ + maxX = actualMaxX; + minX = actualMinX; + maxY = actualMaxY; + minY = actualMinY; + recalculateScales(); + } + + void recalculateScales(){ + xScale = maxX - minX; + yScale = maxY - minY; } @override @@ -51,14 +121,50 @@ class RankState extends State with SingleTickerProviderStateMixin { super.dispose(); } + void dragHandler(DragUpdateDetails dragUpdDet){ + setState(() { + minX -= (xScale / dragFactor) * dragUpdDet.delta.dx; + maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx; + minY += (yScale / dragFactor) * dragUpdDet.delta.dy; + maxY += (yScale / dragFactor) * dragUpdDet.delta.dy; + + if (minX < actualMinX) { + minX = actualMinX; + maxX = actualMinX + xScale; + } + if (maxX > actualMaxX) { + maxX = actualMaxX; + minX = maxX - xScale; + } + if(minY < actualMinY){ + minY = actualMinY; + maxY = actualMinY + yScale; + } + if(maxY > actualMaxY){ + maxY = actualMaxY; + minY = actualMaxY - yScale; + } + }); + } + void _justUpdate() { setState(() {}); } @override Widget build(BuildContext context) { - final t = Translations.of(context); + GlobalKey graphKey = GlobalKey(); bool bigScreen = MediaQuery.of(context).size.width > 768; + EdgeInsets padding = bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48); + double graphStartX = padding.left; + double graphEndX = MediaQuery.sizeOf(context).width - padding.right; + if (previousAxisTitles != _chartsX.toString()+_chartsY.toString()){ + recalculateBoundaries(); + resetScale(); + previousAxisTitles = _chartsX.toString()+_chartsY.toString(); + print(padding); + }; + final t = Translations.of(context); List they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); return Scaffold( appBar: AppBar( @@ -69,9 +175,8 @@ class RankState extends State with SingleTickerProviderStateMixin { child: NestedScrollView( controller: _scrollController, headerSliverBuilder: (context, value) { - return [ - SliverToBoxAdapter( - child: Column( + return [ SliverToBoxAdapter( + child: Column( children: [ Flex( direction: Axis.vertical, @@ -131,11 +236,9 @@ class RankState extends State with SingleTickerProviderStateMixin { Row( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(t.currentAxis(axis: "X"), - style: - const TextStyle(fontSize: 22))), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text("X:", style: TextStyle(fontSize: 22))), DropdownButton( items: _chartsShortTitlesDropdowns, value: _chartsX, @@ -152,10 +255,9 @@ class RankState extends State with SingleTickerProviderStateMixin { Row( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(t.currentAxis(axis: "Y"), - style: const TextStyle(fontSize: 22)), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text("Y:", style: TextStyle(fontSize: 22)), ), DropdownButton( items: _chartsShortTitlesDropdowns, @@ -174,79 +276,109 @@ class RankState extends State with SingleTickerProviderStateMixin { SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height - 104, - child: Stack( - children: [ - Padding( - padding: bigScreen - ? const EdgeInsets.fromLTRB( - 40, 40, 40, 48) - : const EdgeInsets.fromLTRB( - 0, 40, 16, 48), - child: ScatterChart( - ScatterChartData( - scatterSpots: [ - for (TetrioPlayerFromLeaderboard entry - in widget.rank[1]["entries"]) - _MyScatterSpot( - entry.getStatByEnum(_chartsX) - as double, - entry.getStatByEnum(_chartsY) - as double, - entry.userId, - entry.username, - dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3)) - ], - scatterTouchData: ScatterTouchData( - touchTooltipData: - ScatterTouchTooltipData( - fitInsideHorizontally: true, - fitInsideVertically: true, - getTooltipItems: - (touchedSpot) { - touchedSpot - as _MyScatterSpot; - return ScatterTooltipItem( - "${touchedSpot.nickname}\n", - textStyle: const TextStyle( - fontFamily: - "Eurostile Round Extended"), - children: [ - TextSpan( - text: - "${_f4.format(touchedSpot.x)} ${chartsShortTitles[_chartsX]}\n${_f4.format(touchedSpot.y)} ${chartsShortTitles[_chartsY]}", - style: const TextStyle( - fontFamily: - "Eurostile Round")) - ]); - }), - touchCallback: (event, response) { - if (event.runtimeType == - FlTapDownEvent && - response?.touchedSpot?.spot != - null) { - var spot = response?.touchedSpot - ?.spot as _MyScatterSpot; - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MainView( - player: - spot.nickname), - maintainState: false, - ), - ); - } - }, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerSignal: (signal) { + if (signal is PointerScrollEvent) { + RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox; + Offset graphPosition = graphBox.localToGlobal(Offset.zero); + double scrollPosRelativeX = (signal.position.dx - graphStartX) / (graphEndX - graphStartX); + double scrollPosRelativeY = (signal.position.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height + double newMinX, newMaxX, newMinY, newMaxY; + newMinX = minX - (xScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeX; + newMaxX = maxX + (xScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeX); + newMinY = minY - (yScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeY); + newMaxY = maxY + (yScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeY; + if ((newMaxX - newMinX).isNegative) return; + if ((newMaxY - newMinY).isNegative) return; + setState(() { + minX = max(newMinX, actualMinX); + maxX = min(newMaxX, actualMaxX); + minY = max(newMinY, actualMinY); + maxY = min(newMaxY, actualMaxY); + recalculateScales(); + _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy); + }); + }}, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onDoubleTap: () { + setState(() { + minX = actualMinX; + maxX = actualMaxX; + minY = actualMinY; + maxY = actualMaxY; + recalculateScales(); + }); + }, + // TODO: Figure out wtf is going on with gestures + child: Padding( + padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48), + child: Stack( + children: [ + ScatterChart( + key: graphKey, + ScatterChartData( + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + clipData: const FlClipData.all(), + scatterSpots: [ + for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"]) + if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception + _MyScatterSpot( + entry.getStatByEnum(_chartsX) as double, + entry.getStatByEnum(_chartsY) as double, + entry.userId, + entry.username, + dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3)) + ], + scatterTouchData: ScatterTouchData( + handleBuiltInTouches: false, + touchCallback:(touchEvent, touchResponse) { + if (touchEvent is FlPanUpdateEvent){ + dragHandler(touchEvent.details); + return; + } + if (touchEvent is FlPointerHoverEvent){ + setState(() { + if (touchResponse?.touchedSpot == null) { + hoveredPointId = -1; + } else { + hoveredPointId = touchResponse!.touchedSpot!.spotIndex; + _MyScatterSpot castedPoint = touchResponse.touchedSpot!.spot as _MyScatterSpot; + headerTooltip = castedPoint.nickname; + footerTooltip = "${_f4.format(castedPoint.x)} ${chartsShortTitles[_chartsX]}; ${_f4.format(castedPoint.y)} ${chartsShortTitles[_chartsY]}"; + } + }); + } + if (touchEvent is FlPointerExitEvent){ + setState(() {hoveredPointId = -1;}); + } + if (touchEvent is FlTapUpEvent && touchResponse?.touchedSpot?.spot != null){ + _MyScatterSpot spot = touchResponse!.touchedSpot!.spot as _MyScatterSpot; + Navigator.push(context, MaterialPageRoute(builder: (context) => MainView(player: spot.nickname), maintainState: false)); + } + }, + ), + ), + swapAnimationDuration: const Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional ), - ), - swapAnimationDuration: const Duration( - milliseconds: 150), // Optional - swapAnimationCurve: - Curves.linear, // Optional + Padding( + padding: EdgeInsets.fromLTRB(graphStartX+8, padding.top/2+8, 0, 0), + child: Column( + children: [ + AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 24, color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(headerTooltip)), + AnimatedDefaultTextStyle(style: TextStyle(fontFamily: "Eurostile Round", color: Color.fromARGB(hoveredPointId == -1 ? 100 : 255, 255, 255, 255), shadows: hoveredPointId != -1 ? textShadow : null), duration: Durations.medium1, curve: Curves.elasticInOut, child: Text(footerTooltip)), + ], + ), + ) + ], ), ), - ], + ), )) else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) ], diff --git a/lib/views/tl_match_view.dart b/lib/views/tl_match_view.dart index e134b17..383f104 100644 --- a/lib/views/tl_match_view.dart +++ b/lib/views/tl_match_view.dart @@ -197,24 +197,24 @@ class TlMatchResultState extends State { ), SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { switch(snapshot.connectionState){ - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return const LinearProgressIndicator(); - case ConnectionState.done: - if (!snapshot.hasError){ - if (roundSelector.isNegative){ - var time = framesToTime(snapshot.data!.totalLength); - return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center)); - }else{ - var time = framesToTime(snapshot.data!.roundLengths[roundSelector]); - return Center(child: Text("Round Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\nWinner: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,)); - } - }else{ - return const Text("skill issue", textAlign: TextAlign.center); - } - - } + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const LinearProgressIndicator(); + case ConnectionState.done: + if (!snapshot.hasError){ + if (roundSelector.isNegative){ + var time = framesToTime(snapshot.data!.totalLength); + return Center(child: Text("${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center)); + }else{ + var time = framesToTime(snapshot.data!.roundLengths[roundSelector]); + return Center(child: Text("${t.roundLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}\n${t.winner}: ${snapshot.data!.roundWinners[roundSelector][1]}", textAlign: TextAlign.center,)); + } + }else{ + return Text("${snapshot.error.toString()}\n${snapshot.stackTrace}", textAlign: TextAlign.center); + } + + } },),), const SliverToBoxAdapter( child: Divider(), @@ -480,7 +480,7 @@ class TlMatchResultState extends State { CompareBoolThingy( greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, redSide: widget.record.endContext.firstWhere((element) => element.userId != widget.initPlayerId).handling.safeLock, - label: "safeLock", + label: "Safe HD", trueIsBetter: true) ], ) diff --git a/lib/widgets/stat_sell_num.dart b/lib/widgets/stat_sell_num.dart index bef73f9..32be43a 100644 --- a/lib/widgets/stat_sell_num.dart +++ b/lib/widgets/stat_sell_num.dart @@ -12,7 +12,7 @@ class StatCellNum extends StatelessWidget { this.fractionDigits, this.oldPlayerStat, required this.higherIsBetter, - this.okText}); + this.okText, this.alertTitle}); final num playerStat; final num? oldPlayerStat; @@ -20,6 +20,7 @@ class StatCellNum extends StatelessWidget { final String playerStatLabel; final String? okText; final bool isScreenBig; + final String? alertTitle; final List? alertWidgets; final int? fractionDigits; @@ -43,6 +44,7 @@ class StatCellNum extends StatelessWidget { fontFamily: "Eurostile Round Extended", //fontWeight: FontWeight.bold, fontSize: isScreenBig ? 32 : 24, + color: Colors.white ) ) ), @@ -66,7 +68,7 @@ class StatCellNum extends StatelessWidget { showDialog( context: context, builder: (BuildContext context) => AlertDialog( - title: Text(playerStatLabel.replaceAll(RegExp(r'\n'), " "), + title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "), style: const TextStyle( fontFamily: "Eurostile Round Extended")), content: SingleChildScrollView( diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index dd5d1cc..3d3a3c9 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -345,7 +345,7 @@ class _TLThingyState extends State { oldPlayerStat: oldTl?.nerdStats?.cheese,), StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, alertWidgets: [Text(t.statCellNum.gbeDescription), - Text("${t.formula}: ((APP * DS/S) / PPS) * 2"), + Text("${t.formula}: APP * DS/P * 2"), Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),], okText: t.popupActions.ok, higherIsBetter: true, diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 12d8e11..26f8f0d 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -23,6 +23,11 @@ Future copyToClipboard(String text) async { await Clipboard.setData(ClipboardData(text: text)); } +List textShadow = const [ + Shadow(offset: Offset(0.0, 0.0), blurRadius: 3.0, color: Colors.black), + Shadow(offset: Offset(0.0, 0.0), blurRadius: 8.0, color: Colors.black), +]; + class UserThingy extends StatelessWidget { final TetrioPlayer player; final bool showStateTimestamp; @@ -109,18 +114,7 @@ class UserThingy extends StatelessWidget { style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28, - shadows: const [ - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 3.0, - color: Colors.black, - ), - Shadow( - offset: Offset(0.0, 0.0), - blurRadius: 8.0, - color: Colors.black, - ), - ], + shadows: textShadow, )), TextButton( child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), @@ -280,7 +274,8 @@ class UserThingy extends StatelessWidget { playerStat: player.gameTime.inHours, playerStatLabel: t.statCellNum.hoursPlayed, isScreenBig: bigScreen, - alertWidgets: [Text("${t.exactGametime}: ${player.gameTime.toString()}")], + alertTitle: t.exactGametime, + alertWidgets: [Text(player.gameTime.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended"),)], higherIsBetter: true,), if (player.gamesPlayed >= 0) StatCellNum( diff --git a/pubspec.yaml b/pubspec.yaml index c738b49..ad81b1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -version: 1.3.0+13 +version: 1.4.0+14 environment: sdk: '>=3.0.0' diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json index 24e3fc2..9ecf2f5 100644 --- a/res/i18n/strings.i18n.json +++ b/res/i18n/strings.i18n.json @@ -47,6 +47,8 @@ "anonRecord": "Guests are not allowed to set records", "notEnoughData": "Not enough data", "noHistorySaved": "No history saved", + "pseudoTooltipHeaderInit": "Hover over point", + "pseudoTooltipFooterInit": "to see detailed data", "obtainDate": "Obtained ${date}", "fetchDate": "Fetched ${date}", "exactGametime": "Exact gametime", @@ -115,6 +117,9 @@ "match": "Match", "roundNumber": "Round $n", "statsFor": "Stats for", + "matchLength": "Match Length", + "roundLength": "Round Length", + "winner": "Winner", "registred": "Registred", "playedTL": "Played Tetra League", "winChance": "Win Chance", diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json index 447e95c..8b9772a 100644 --- a/res/i18n/strings_ru.i18n.json +++ b/res/i18n/strings_ru.i18n.json @@ -47,6 +47,8 @@ "anonRecord": "Гостям нельзя ставить рекорды", "notEnoughData": "Недостаточно данных", "noHistorySaved": "Нет сохранённой истории", + "pseudoTooltipHeaderInit": "Наведите курсор на точку", + "pseudoTooltipFooterInit": "чтобы узнать подробности", "obtainDate": "Получено ${date}", "fetchDate": "На момент ${date}", "exactGametime": "Время, проведённое в игре", @@ -115,6 +117,9 @@ "match": "Матч", "roundNumber": "Раунд $n", "statsFor": "Статистика за", + "matchLength": "Продолжительность матча", + "roundLength": "Продолжительность раунда", + "winner": "Победитель", "registred": "Зарегистрирован", "playedTL": "Играл в Тетра Лигу", "winChance": "Шансы на победу",