This commit is contained in:
dan63047 2024-01-22 21:00:24 +03:00
parent 14def01b57
commit d0ead79068
13 changed files with 502 additions and 244 deletions

View File

@ -40,37 +40,37 @@ jobs:
tag: Auto-${{ github.run_number }} tag: Auto-${{ github.run_number }}
body: Builded with GitHub Action workflow body: Builded with GitHub Action workflow
token: ${{ secrets.TOKEN }} token: ${{ secrets.TOKEN }}
build-and-release-linux: # build-and-release-linux:
name: Build Linux App # name: Build Linux App
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- uses: actions/checkout@v2 # - uses: actions/checkout@v2
- uses: subosito/flutter-action@v1 # - uses: subosito/flutter-action@v1
- uses: ashutoshvarma/setup-ninja@master # - uses: ashutoshvarma/setup-ninja@master
with: # with:
channel: 'stable' # channel: 'stable'
flutter-version: '3.16.5' # flutter-version: '3.16.5'
- name: Install project dependencies # - name: Install project dependencies
run: flutter pub get # run: flutter pub get
- name: Build artifacts # - name: Build artifacts
run: flutter build linux --release # run: flutter build linux --release
- name: Archive Release # - name: Archive Release
uses: thedoctor0/zip-release@master # uses: thedoctor0/zip-release@master
with: # with:
type: 'zip' # type: 'zip'
filename: TetraStats-${{github.ref_name}}-windows.zip # filename: TetraStats-${{github.ref_name}}-windows.zip
directory: build/linux/x64/runner/Release/bundle # directory: build/linux/x64/runner/Release/bundle
- name: Push to Releases # - name: Push to Releases
uses: ncipollo/release-action@v1 # uses: ncipollo/release-action@v1
with: # with:
prerelease: true # prerelease: true
allowUpdates: true # allowUpdates: true
replacesArtifacts: false # replacesArtifacts: false
discussionCategory: autobuilded-releases # discussionCategory: autobuilded-releases
artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip" # artifacts: "build/linux/x64/runner/Release/bundle/TetraStats-${{github.ref_name}}-linux.zip"
tag: Auto-${{ github.run_number }} # tag: Auto-${{ github.run_number }}
body: Builded with GitHub Action workflow # body: Builded with GitHub Action workflow
token: ${{ secrets.TOKEN }} # token: ${{ secrets.TOKEN }}
build-and-release-android: build-and-release-android:
name: Build Android App name: Build Android App
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -661,7 +661,7 @@ class NerdStats {
dsp = ((_vs / 100) - (_apm / 60)) / _pps; dsp = ((_vs / 100) - (_apm / 60)) / _pps;
appdsp = app + dsp; appdsp = app + dsp;
cheese = (dsp * 150) + ((vsapm - 2) * 50) + (0.6 - app) * 125; 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)); nyaapp = app - 5 * tan(radians((cheese / -30) + 1));
area = _apm * 1 + _pps * 45 + _vs * 0.444 + app * 185 + dss * 175 + dsp * 450 + gbe * 315; area = _apm * 1 + _pps * 45 + _vs * 0.444 + app * 185 + dss * 175 + dsp * 450 + gbe * 315;
} }

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 2 /// 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 // coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
@ -182,6 +182,8 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get anonRecord => 'Guests are not allowed to set records'; String get anonRecord => 'Guests are not allowed to set records';
String get notEnoughData => 'Not enough data'; String get notEnoughData => 'Not enough data';
String get noHistorySaved => 'No history saved'; 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 obtainDate({required Object date}) => 'Obtained ${date}';
String fetchDate({required Object date}) => 'Fetched ${date}'; String fetchDate({required Object date}) => 'Fetched ${date}';
String get exactGametime => 'Exact gametime'; String get exactGametime => 'Exact gametime';
@ -250,6 +252,9 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get match => 'Match'; String get match => 'Match';
String roundNumber({required Object n}) => 'Round ${n}'; String roundNumber({required Object n}) => 'Round ${n}';
String get statsFor => 'Stats for'; 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 registred => 'Registred';
String get playedTL => 'Played Tetra League'; String get playedTL => 'Played Tetra League';
String get winChance => 'Win Chance'; String get winChance => 'Win Chance';
@ -763,6 +768,8 @@ class _StringsRu implements Translations {
@override String get anonRecord => 'Гостям нельзя ставить рекорды'; @override String get anonRecord => 'Гостям нельзя ставить рекорды';
@override String get notEnoughData => 'Недостаточно данных'; @override String get notEnoughData => 'Недостаточно данных';
@override String get noHistorySaved => 'Нет сохранённой истории'; @override String get noHistorySaved => 'Нет сохранённой истории';
@override String get pseudoTooltipHeaderInit => 'Наведите курсор на точку';
@override String get pseudoTooltipFooterInit => 'чтобы узнать подробности';
@override String obtainDate({required Object date}) => 'Получено ${date}'; @override String obtainDate({required Object date}) => 'Получено ${date}';
@override String fetchDate({required Object date}) => 'На момент ${date}'; @override String fetchDate({required Object date}) => 'На момент ${date}';
@override String get exactGametime => 'Время, проведённое в игре'; @override String get exactGametime => 'Время, проведённое в игре';
@ -831,6 +838,9 @@ class _StringsRu implements Translations {
@override String get match => 'Матч'; @override String get match => 'Матч';
@override String roundNumber({required Object n}) => 'Раунд ${n}'; @override String roundNumber({required Object n}) => 'Раунд ${n}';
@override String get statsFor => 'Статистика за'; @override String get statsFor => 'Статистика за';
@override String get matchLength => 'Продолжительность матча';
@override String get roundLength => 'Продолжительность раунда';
@override String get winner => 'Победитель';
@override String get registred => 'Зарегистрирован'; @override String get registred => 'Зарегистрирован';
@override String get playedTL => 'Играл в Тетра Лигу'; @override String get playedTL => 'Играл в Тетра Лигу';
@override String get winChance => 'Шансы на победу'; @override String get winChance => 'Шансы на победу';
@ -1336,6 +1346,8 @@ extension on Translations {
case 'anonRecord': return 'Guests are not allowed to set records'; case 'anonRecord': return 'Guests are not allowed to set records';
case 'notEnoughData': return 'Not enough data'; case 'notEnoughData': return 'Not enough data';
case 'noHistorySaved': return 'No history saved'; 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 'obtainDate': return ({required Object date}) => 'Obtained ${date}';
case 'fetchDate': return ({required Object date}) => 'Fetched ${date}'; case 'fetchDate': return ({required Object date}) => 'Fetched ${date}';
case 'exactGametime': return 'Exact gametime'; case 'exactGametime': return 'Exact gametime';
@ -1404,6 +1416,9 @@ extension on Translations {
case 'match': return 'Match'; case 'match': return 'Match';
case 'roundNumber': return ({required Object n}) => 'Round ${n}'; case 'roundNumber': return ({required Object n}) => 'Round ${n}';
case 'statsFor': return 'Stats for'; case 'statsFor': return 'Stats for';
case 'matchLength': return 'Match Length';
case 'roundLength': return 'Round Length';
case 'winner': return 'Winner';
case 'registred': return 'Registred'; case 'registred': return 'Registred';
case 'playedTL': return 'Played Tetra League'; case 'playedTL': return 'Played Tetra League';
case 'winChance': return 'Win Chance'; case 'winChance': return 'Win Chance';
@ -1843,6 +1858,8 @@ extension on _StringsRu {
case 'anonRecord': return 'Гостям нельзя ставить рекорды'; case 'anonRecord': return 'Гостям нельзя ставить рекорды';
case 'notEnoughData': return 'Недостаточно данных'; case 'notEnoughData': return 'Недостаточно данных';
case 'noHistorySaved': return 'Нет сохранённой истории'; case 'noHistorySaved': return 'Нет сохранённой истории';
case 'pseudoTooltipHeaderInit': return 'Наведите курсор на точку';
case 'pseudoTooltipFooterInit': return 'чтобы узнать подробности';
case 'obtainDate': return ({required Object date}) => 'Получено ${date}'; case 'obtainDate': return ({required Object date}) => 'Получено ${date}';
case 'fetchDate': return ({required Object date}) => 'На момент ${date}'; case 'fetchDate': return ({required Object date}) => 'На момент ${date}';
case 'exactGametime': return 'Время, проведённое в игре'; case 'exactGametime': return 'Время, проведённое в игре';
@ -1911,6 +1928,9 @@ extension on _StringsRu {
case 'match': return 'Матч'; case 'match': return 'Матч';
case 'roundNumber': return ({required Object n}) => 'Раунд ${n}'; case 'roundNumber': return ({required Object n}) => 'Раунд ${n}';
case 'statsFor': return 'Статистика за'; case 'statsFor': return 'Статистика за';
case 'matchLength': return 'Продолжительность матча';
case 'roundLength': return 'Продолжительность раунда';
case 'winner': return 'Победитель';
case 'registred': return 'Зарегистрирован'; case 'registred': return 'Зарегистрирован';
case 'playedTL': return 'Играл в Тетра Лигу'; case 'playedTL': return 'Играл в Тетра Лигу';
case 'winChance': return 'Шансы на победу'; case 'winChance': return 'Шансы на победу';

View File

@ -57,6 +57,7 @@ const String createTetrioTLReplayStats = '''
CREATE TABLE IF NOT EXISTS "tetrioTLReplayStats" ( CREATE TABLE IF NOT EXISTS "tetrioTLReplayStats" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"data" TEXT NOT NULL, "data" TEXT NOT NULL,
"freyhoe" TEXT NOT NULL,
PRIMARY KEY("id") PRIMARY KEY("id")
) )
'''; ''';
@ -70,8 +71,8 @@ class TetrioService extends DB {
final Map<String, List<News>> _newsCache = {}; final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {}; final Map<String, Map<String, double?>> _topTRcache = {};
final Map<String, TetraLeagueAlphaStream> _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} final Map<String, TetraLeagueAlphaStream> _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("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("Kagari-chan loves osk (Tetra Stats dev build)", http.Client());
static final TetrioService _shared = TetrioService._sharedInstance(); static final TetrioService _shared = TetrioService._sharedInstance();
factory TetrioService() => _shared; factory TetrioService() => _shared;
late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController; late final StreamController<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
@ -129,16 +130,28 @@ class TetrioService extends DB {
Future<List<dynamic>> szyGetReplay(String replayID) async { Future<List<dynamic>> szyGetReplay(String replayID) async {
try{ try{
// read from cache
var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID); var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID);
return cached.value; return cached.value;
}catch (e){ }catch (e){
// actually going to obtain // actually going to obtain
} }
Uri url = Uri.https('inoue.szy.lol', '/api/replay/$replayID');
var downloadPath = await getDownloadsDirectory(); Uri url;
downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); if (kIsWeb) {
var replayFile = File("${downloadPath.path}/$replayID.ttrm"); url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID});
if (replayFile.existsSync()) return [replayFile.readAsStringSync(), replayFile.readAsBytesSync()]; } 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{ try{
final response = await client.get(url); final response = await client.get(url);
@ -533,10 +546,12 @@ class TetrioService extends DB {
Future<void> deleteTLMatch(String matchID) async { Future<void> deleteTLMatch(String matchID) async {
await ensureDbIsOpen(); await ensureDbIsOpen();
final db = getDatabaseOrThrow(); 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]); final results = await db.delete(tetraLeagueMatchesTable, where: '$idCol = ?', whereArgs: [matchID]);
if (results != 1) { if (results != 1) {
throw CouldNotDeleteMatch(); throw CouldNotDeleteMatch();
} }
await db.delete(tetrioTLReplayStatsTable, where: '$idCol = ?', whereArgs: [rID]);
} }
Future<Map<String, dynamic>> fetchRecords(String userID) async { Future<Map<String, dynamic>> fetchRecords(String userID) async {

View File

@ -591,7 +591,7 @@ class _History extends StatelessWidget{
update(); 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))) 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{ class _HistoryChartThigy extends StatefulWidget{
final List<FlSpot> data; final List<FlSpot> data;
final String title;
final String yAxisTitle; final String yAxisTitle;
final bool bigScreen; final bool bigScreen;
final double leftSpace; final double leftSpace;
final NumberFormat yFormat; 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 @override
State<_HistoryChartThigy> createState() => _HistoryChartThigyState(); State<_HistoryChartThigy> createState() => _HistoryChartThigyState();
} }
class _HistoryChartThigyState extends State<_HistoryChartThigy> { class _HistoryChartThigyState extends State<_HistoryChartThigy> {
late String previousAxisTitle;
late double minX; late double minX;
late double maxX; late double maxX;
late double minY; late double minY;
late double actualMinY; late double actualMinY;
late double maxY; late double maxY;
late double actualMaxY; 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 @override
void initState(){ void initState(){
super.initState(); super.initState();
minX = widget.data.first.x; minX = widget.data.first.x;
maxX = widget.data.last.x; maxX = widget.data.last.x;
minY = widget.data.reduce((value, element){ setMinMaxY();
num n = min(value.y, element.y); previousAxisTitle = widget.yAxisTitle;
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;
actualMaxY = maxY; actualMaxY = maxY;
actualMinY = minY; actualMinY = minY;
recalculateScales();
} }
@override @override
@ -651,20 +645,93 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> {
super.dispose(); super.dispose();
actualMinY = 0; actualMinY = 0;
minY = 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<State<StatefulWidget>> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
GlobalKey graphKey = GlobalKey(); 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); 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); 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 graphStartX = padding.left+widget.leftSpace;
double graphEndX = MediaQuery.sizeOf(context).width - padding.right; double graphEndX = MediaQuery.sizeOf(context).width - padding.right;
if (previousAxisTitle != widget.yAxisTitle) {
setMinMaxY();
recalculateScales();
previousAxisTitle = widget.yAxisTitle;
}
return SizedBox( return SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 104, height: MediaQuery.of(context).size.height - 104,
@ -688,73 +755,90 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> {
maxX = min(newMaxX, widget.data.last.x); maxX = min(newMaxX, widget.data.last.x);
minY = max(newMinY, actualMinY); minY = max(newMinY, actualMinY);
maxY = min(newMaxY, actualMaxY); 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: child:
GestureDetector( GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.translucent,
onDoubleTap: () { onDoubleTap: () {
setState(() { setState(() {
minX = widget.data.first.x; minX = widget.data.first.x;
maxX = widget.data.last.x; maxX = widget.data.last.x;
minY = actualMinY; minY = actualMinY;
maxY = actualMaxY; maxY = actualMaxY;
recalculateScales();
}); });
}, },
onPanUpdate: (dragUpdDet) { // TODO: onScaleUpdate:(details) => scaleHandler(details, graphKey, graphStartX, graphEndX),
print(dragUpdDet); // TODO: Figure out wtf is going on with gestures
// TODO: Somehow highlight touched spot (handleBuiltInTouches breaks getTooltipItems and getTouchedSpotIndicator)
setState(() { child: Padding( padding: padding,
minX -= (xScale / dragFactor) * dragUpdDet.delta.dx; child: Stack(
maxX -= (xScale / dragFactor) * dragUpdDet.delta.dx; children: [
minY += (yScale / dragFactor) * dragUpdDet.delta.dy; LineChart(
maxY += (yScale / dragFactor) * dragUpdDet.delta.dy; key: graphKey,
LineChartData(
if (minX < widget.data.first.x) { lineBarsData: [LineChartBarData(spots: widget.data)],
minX = widget.data.first.x; clipData: const FlClipData.all(),
maxX = widget.data.first.x + xScale; borderData: FlBorderData(show: false),
} gridData: FlGridData(verticalInterval: xInterval),
if (maxX > widget.data.last.x) { minX: minX,
maxX = widget.data.last.x; maxX: maxX,
minX = maxX - xScale; minY: minY,
} maxY: maxY,
}); titlesData: FlTitlesData(topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
}, rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
child: AbsorbPointer( bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){
child: Padding( padding: padding, return value != meta.min && value != meta.max ? SideTitleWidget(
child: LineChart( axisSide: meta.axisSide,
key: graphKey, child: Text(DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))),
LineChartData( ) : Container();
lineBarsData: [LineChartBarData(spots: widget.data)], })),
clipData: const FlClipData.all(), leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: widget.leftSpace, getTitlesWidget: (double value, TitleMeta meta){
borderData: FlBorderData(show: false), return value != meta.min && value != meta.max ? SideTitleWidget(
gridData: FlGridData(verticalInterval: xInterval), axisSide: meta.axisSide,
minX: minX, child: Text(widget.yFormat.format(value)),
maxX: maxX, ) : Container();
minY: minY, }))),
maxY: maxY, lineTouchData: LineTouchData(
titlesData: FlTitlesData(topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), handleBuiltInTouches: false,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), touchCallback:(touchEvent, touchResponse) {
bottomTitles: AxisTitles(sideTitles: SideTitles(interval: xInterval, showTitles: true, reservedSize: 30, getTitlesWidget: (double value, TitleMeta meta){ if (touchEvent is FlPanUpdateEvent){
return value != meta.min && value != meta.max ? SideTitleWidget( dragHandler(touchEvent.details);
axisSide: meta.axisSide, return;
child: Text(DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).format(DateTime.fromMillisecondsSinceEpoch(value.floor()))), }
) : Container(); if (touchEvent is FlPointerHoverEvent){
})), setState(() {
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: widget.leftSpace, getTitlesWidget: (double value, TitleMeta meta){ if (touchResponse?.lineBarSpots?.first == null) {
return value != meta.min && value != meta.max ? SideTitleWidget( hoveredPointId = -1;
axisSide: meta.axisSide, } else {
child: Text(widget.yFormat.format(value)), hoveredPointId = touchResponse!.lineBarSpots!.first.spotIndex;
) : Container(); headerTooltip = "${_f4.format(touchResponse.lineBarSpots!.first.y)} ${widget.yAxisTitle}";
}))), footerTooltip = _dateFormat.format(DateTime.fromMillisecondsSinceEpoch(touchResponse.lineBarSpots!.first.x.floor()));
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())))])]; });
},)) }
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; break;
default: 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; return result;
@ -1055,7 +1139,7 @@ class _OtherThingy extends StatelessWidget {
return ListTile( return ListTile(
title: RichText( title: RichText(
text: TextSpan( 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, text: t.newsParts.leaderboardStart,
children: [ children: [
TextSpan(text: "${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: "${news.data["rank"]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
@ -1070,7 +1154,7 @@ class _OtherThingy extends StatelessWidget {
return ListTile( return ListTile(
title: RichText( title: RichText(
text: TextSpan( 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, text: t.newsParts.personalbest,
children: [ children: [
TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: "${gametypes[news.data["gametype"]]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
@ -1093,7 +1177,7 @@ class _OtherThingy extends StatelessWidget {
return ListTile( return ListTile(
title: RichText( title: RichText(
text: TextSpan( 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, text: t.newsParts.badgeStart,
children: [ children: [
TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: "${news.data["label"]} ", style: const TextStyle(fontWeight: FontWeight.bold)),
@ -1115,7 +1199,7 @@ class _OtherThingy extends StatelessWidget {
return ListTile( return ListTile(
title: RichText( title: RichText(
text: TextSpan( 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, text: t.newsParts.rankupStart,
children: [ children: [
TextSpan(text: t.newsParts.rankupMiddle(r: news.data["rank"].toString().toUpperCase()), style: const TextStyle(fontWeight: FontWeight.bold)), 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( return ListTile(
title: RichText( title: RichText(
text: TextSpan( 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, text: t.newsParts.supporterStart,
children: [ children: [
TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold))
@ -1158,7 +1242,7 @@ class _OtherThingy extends StatelessWidget {
return ListTile( return ListTile(
title: RichText( title: RichText(
text: TextSpan( 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, text: t.newsParts.supporterGiftStart,
children: [ children: [
TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold)) TextSpan(text: t.newsParts.tetoSupporter, style: const TextStyle(fontWeight: FontWeight.bold))

View File

@ -1,11 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:tetra_stats/data_objects/tetrio.dart';
import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/gen/strings.g.dart';
import 'package:tetra_stats/views/main_view.dart' show MainView; 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'; import 'package:window_manager/window_manager.dart';
var _chartsShortTitlesDropdowns = <DropdownMenuItem>[for (MapEntry e in chartsShortTitles.entries) DropdownMenuItem(value: e.key, child: Text(e.value),)]; var _chartsShortTitlesDropdowns = <DropdownMenuItem>[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<RankView> with SingleTickerProviderStateMixin { class RankState extends State<RankView> with SingleTickerProviderStateMixin {
late ScrollController _scrollController; late ScrollController _scrollController;
late TabController _tabController; 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 @override
void initState() { void initState() {
@ -41,6 +60,57 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}"); windowManager.setTitle("Tetra Stats: ${widget.rank[1]["everyone"] ? t.everyoneAverages : t.rankAverages(rank: widget.rank[0].rank.toUpperCase())}");
} }
super.initState(); super.initState();
previousAxisTitles = _chartsX.toString()+_chartsY.toString();
recalculateBoundaries();
resetScale();
}
void recalculateBoundaries(){
actualMinX = (widget.rank[1]["entries"] as List<TetrioPlayerFromLeaderboard>).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<TetrioPlayerFromLeaderboard>).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<TetrioPlayerFromLeaderboard>).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<TetrioPlayerFromLeaderboard>).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 @override
@ -51,14 +121,50 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
super.dispose(); 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() { void _justUpdate() {
setState(() {}); setState(() {});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = Translations.of(context); GlobalKey graphKey = GlobalKey();
bool bigScreen = MediaQuery.of(context).size.width > 768; 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<TetrioPlayerFromLeaderboard> they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country); List<TetrioPlayerFromLeaderboard> they = TetrioPlayersLeaderboard("lol", []).getStatRanking(widget.rank[1]["entries"]!, _sortBy, reversed: _reversed, country: _country);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -69,9 +175,8 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
child: NestedScrollView( child: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (context, value) { headerSliverBuilder: (context, value) {
return [ return [ SliverToBoxAdapter(
SliverToBoxAdapter( child: Column(
child: Column(
children: [ children: [
Flex( Flex(
direction: Axis.vertical, direction: Axis.vertical,
@ -131,11 +236,9 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( const Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Text(t.currentAxis(axis: "X"), child: Text("X:", style: TextStyle(fontSize: 22))),
style:
const TextStyle(fontSize: 22))),
DropdownButton( DropdownButton(
items: _chartsShortTitlesDropdowns, items: _chartsShortTitlesDropdowns,
value: _chartsX, value: _chartsX,
@ -152,10 +255,9 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( const Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Text(t.currentAxis(axis: "Y"), child: Text("Y:", style: TextStyle(fontSize: 22)),
style: const TextStyle(fontSize: 22)),
), ),
DropdownButton( DropdownButton(
items: _chartsShortTitlesDropdowns, items: _chartsShortTitlesDropdowns,
@ -174,79 +276,109 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 104, height: MediaQuery.of(context).size.height - 104,
child: Stack( child: Listener(
children: [ behavior: HitTestBehavior.translucent,
Padding( onPointerSignal: (signal) {
padding: bigScreen if (signal is PointerScrollEvent) {
? const EdgeInsets.fromLTRB( RenderBox graphBox = graphKey.currentContext?.findRenderObject() as RenderBox;
40, 40, 40, 48) Offset graphPosition = graphBox.localToGlobal(Offset.zero);
: const EdgeInsets.fromLTRB( double scrollPosRelativeX = (signal.position.dx - graphStartX) / (graphEndX - graphStartX);
0, 40, 16, 48), double scrollPosRelativeY = (signal.position.dy - graphPosition.dy) / (graphBox.size.height - 30); // size - bottom titles height
child: ScatterChart( double newMinX, newMaxX, newMinY, newMaxY;
ScatterChartData( newMinX = minX - (xScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeX;
scatterSpots: [ newMaxX = maxX + (xScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeX);
for (TetrioPlayerFromLeaderboard entry newMinY = minY - (yScale / scaleFactor) * signal.scrollDelta.dy * (1-scrollPosRelativeY);
in widget.rank[1]["entries"]) newMaxY = maxY + (yScale / scaleFactor) * signal.scrollDelta.dy * scrollPosRelativeY;
_MyScatterSpot( if ((newMaxX - newMinX).isNegative) return;
entry.getStatByEnum(_chartsX) if ((newMaxY - newMinY).isNegative) return;
as double, setState(() {
entry.getStatByEnum(_chartsY) minX = max(newMinX, actualMinX);
as double, maxX = min(newMaxX, actualMaxX);
entry.userId, minY = max(newMinY, actualMinY);
entry.username, maxY = min(newMaxY, actualMaxY);
dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3)) recalculateScales();
], _scrollController.jumpTo(_scrollController.position.maxScrollExtent - signal.scrollDelta.dy);
scatterTouchData: ScatterTouchData( });
touchTooltipData: }},
ScatterTouchTooltipData( child: GestureDetector(
fitInsideHorizontally: true, behavior: HitTestBehavior.opaque,
fitInsideVertically: true, onDoubleTap: () {
getTooltipItems: setState(() {
(touchedSpot) { minX = actualMinX;
touchedSpot maxX = actualMaxX;
as _MyScatterSpot; minY = actualMinY;
return ScatterTooltipItem( maxY = actualMaxY;
"${touchedSpot.nickname}\n", recalculateScales();
textStyle: const TextStyle( });
fontFamily: },
"Eurostile Round Extended"), // TODO: Figure out wtf is going on with gestures
children: [ child: Padding(
TextSpan( padding: bigScreen ? const EdgeInsets.fromLTRB(40, 40, 40, 48) : const EdgeInsets.fromLTRB(0, 40, 16, 48),
text: child: Stack(
"${_f4.format(touchedSpot.x)} ${chartsShortTitles[_chartsX]}\n${_f4.format(touchedSpot.y)} ${chartsShortTitles[_chartsY]}", children: [
style: const TextStyle( ScatterChart(
fontFamily: key: graphKey,
"Eurostile Round")) ScatterChartData(
]); minX: minX,
}), maxX: maxX,
touchCallback: (event, response) { minY: minY,
if (event.runtimeType == maxY: maxY,
FlTapDownEvent && clipData: const FlClipData.all(),
response?.touchedSpot?.spot != scatterSpots: [
null) { for (TetrioPlayerFromLeaderboard entry in widget.rank[1]["entries"])
var spot = response?.touchedSpot if (entry.apm != 0.0 && entry.vs != 0.0) // prevents from ScatterChart "Offset argument contained a NaN value." exception
?.spot as _MyScatterSpot; _MyScatterSpot(
Navigator.push( entry.getStatByEnum(_chartsX) as double,
context, entry.getStatByEnum(_chartsY) as double,
MaterialPageRoute( entry.userId,
builder: (context) => entry.username,
MainView( dotPainter: FlDotCirclePainter(color: rankColors[entry.rank]??Colors.white, radius: 3))
player: ],
spot.nickname), scatterTouchData: ScatterTouchData(
maintainState: false, 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
), ),
), Padding(
swapAnimationDuration: const Duration( padding: EdgeInsets.fromLTRB(graphStartX+8, padding.top/2+8, 0, 0),
milliseconds: 150), // Optional child: Column(
swapAnimationCurve: children: [
Curves.linear, // Optional 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))) else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))
], ],

View File

@ -197,24 +197,24 @@ class TlMatchResultState extends State<TlMatchResultView> {
), ),
SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) { SliverToBoxAdapter(child: FutureBuilder(future: replayData, builder: (context, snapshot) {
switch(snapshot.connectionState){ switch(snapshot.connectionState){
case ConnectionState.none: case ConnectionState.none:
case ConnectionState.waiting: case ConnectionState.waiting:
case ConnectionState.active: case ConnectionState.active:
return const LinearProgressIndicator(); return const LinearProgressIndicator();
case ConnectionState.done: case ConnectionState.done:
if (!snapshot.hasError){ if (!snapshot.hasError){
if (roundSelector.isNegative){ if (roundSelector.isNegative){
var time = framesToTime(snapshot.data!.totalLength); var time = framesToTime(snapshot.data!.totalLength);
return Center(child: Text("Match Length: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center)); return Center(child: Text("${t.matchLength}: ${time.inMinutes}:${secs.format(time.inMicroseconds /1000000 % 60)}", textAlign: TextAlign.center));
}else{ }else{
var time = framesToTime(snapshot.data!.roundLengths[roundSelector]); 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,)); 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{ }else{
return const Text("skill issue", textAlign: TextAlign.center); return Text("${snapshot.error.toString()}\n${snapshot.stackTrace}", textAlign: TextAlign.center);
} }
} }
},),), },),),
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: Divider(), child: Divider(),
@ -480,7 +480,7 @@ class TlMatchResultState extends State<TlMatchResultView> {
CompareBoolThingy( CompareBoolThingy(
greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock, greenSide: widget.record.endContext.firstWhere((element) => element.userId == widget.initPlayerId).handling.safeLock,
redSide: 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) trueIsBetter: true)
], ],
) )

View File

@ -12,7 +12,7 @@ class StatCellNum extends StatelessWidget {
this.fractionDigits, this.fractionDigits,
this.oldPlayerStat, this.oldPlayerStat,
required this.higherIsBetter, required this.higherIsBetter,
this.okText}); this.okText, this.alertTitle});
final num playerStat; final num playerStat;
final num? oldPlayerStat; final num? oldPlayerStat;
@ -20,6 +20,7 @@ class StatCellNum extends StatelessWidget {
final String playerStatLabel; final String playerStatLabel;
final String? okText; final String? okText;
final bool isScreenBig; final bool isScreenBig;
final String? alertTitle;
final List<Widget>? alertWidgets; final List<Widget>? alertWidgets;
final int? fractionDigits; final int? fractionDigits;
@ -43,6 +44,7 @@ class StatCellNum extends StatelessWidget {
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
//fontWeight: FontWeight.bold, //fontWeight: FontWeight.bold,
fontSize: isScreenBig ? 32 : 24, fontSize: isScreenBig ? 32 : 24,
color: Colors.white
) )
) )
), ),
@ -66,7 +68,7 @@ class StatCellNum extends StatelessWidget {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) => AlertDialog( builder: (BuildContext context) => AlertDialog(
title: Text(playerStatLabel.replaceAll(RegExp(r'\n'), " "), title: Text(alertTitle??playerStatLabel.replaceAll(RegExp(r'\n'), " "),
style: const TextStyle( style: const TextStyle(
fontFamily: "Eurostile Round Extended")), fontFamily: "Eurostile Round Extended")),
content: SingleChildScrollView( content: SingleChildScrollView(

View File

@ -345,7 +345,7 @@ class _TLThingyState extends State<TLThingy> {
oldPlayerStat: oldTl?.nerdStats?.cheese,), oldPlayerStat: oldTl?.nerdStats?.cheese,),
StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe, StatCellNum(playerStat: currentTl.nerdStats!.gbe, isScreenBig: bigScreen, fractionDigits: 3, playerStatLabel: t.statCellNum.gbe,
alertWidgets: [Text(t.statCellNum.gbeDescription), 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}"),], Text("${t.exactValue}: ${currentTl.nerdStats!.gbe}"),],
okText: t.popupActions.ok, okText: t.popupActions.ok,
higherIsBetter: true, higherIsBetter: true,

View File

@ -23,6 +23,11 @@ Future<void> copyToClipboard(String text) async {
await Clipboard.setData(ClipboardData(text: text)); await Clipboard.setData(ClipboardData(text: text));
} }
List<Shadow> textShadow = const <Shadow>[
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 { class UserThingy extends StatelessWidget {
final TetrioPlayer player; final TetrioPlayer player;
final bool showStateTimestamp; final bool showStateTimestamp;
@ -109,18 +114,7 @@ class UserThingy extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontFamily: "Eurostile Round Extended", fontFamily: "Eurostile Round Extended",
fontSize: bigScreen ? 42 : 28, fontSize: bigScreen ? 42 : 28,
shadows: const <Shadow>[ shadows: textShadow,
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,
),
],
)), )),
TextButton( TextButton(
child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), 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, playerStat: player.gameTime.inHours,
playerStatLabel: t.statCellNum.hoursPlayed, playerStatLabel: t.statCellNum.hoursPlayed,
isScreenBig: bigScreen, 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,), higherIsBetter: true,),
if (player.gamesPlayed >= 0) if (player.gamesPlayed >= 0)
StatCellNum( StatCellNum(

View File

@ -2,7 +2,7 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO description: Track your and other player stats in TETR.IO
publish_to: 'none' publish_to: 'none'
version: 1.3.0+13 version: 1.4.0+14
environment: environment:
sdk: '>=3.0.0' sdk: '>=3.0.0'

View File

@ -47,6 +47,8 @@
"anonRecord": "Guests are not allowed to set records", "anonRecord": "Guests are not allowed to set records",
"notEnoughData": "Not enough data", "notEnoughData": "Not enough data",
"noHistorySaved": "No history saved", "noHistorySaved": "No history saved",
"pseudoTooltipHeaderInit": "Hover over point",
"pseudoTooltipFooterInit": "to see detailed data",
"obtainDate": "Obtained ${date}", "obtainDate": "Obtained ${date}",
"fetchDate": "Fetched ${date}", "fetchDate": "Fetched ${date}",
"exactGametime": "Exact gametime", "exactGametime": "Exact gametime",
@ -115,6 +117,9 @@
"match": "Match", "match": "Match",
"roundNumber": "Round $n", "roundNumber": "Round $n",
"statsFor": "Stats for", "statsFor": "Stats for",
"matchLength": "Match Length",
"roundLength": "Round Length",
"winner": "Winner",
"registred": "Registred", "registred": "Registred",
"playedTL": "Played Tetra League", "playedTL": "Played Tetra League",
"winChance": "Win Chance", "winChance": "Win Chance",

View File

@ -47,6 +47,8 @@
"anonRecord": "Гостям нельзя ставить рекорды", "anonRecord": "Гостям нельзя ставить рекорды",
"notEnoughData": "Недостаточно данных", "notEnoughData": "Недостаточно данных",
"noHistorySaved": "Нет сохранённой истории", "noHistorySaved": "Нет сохранённой истории",
"pseudoTooltipHeaderInit": "Наведите курсор на точку",
"pseudoTooltipFooterInit": "чтобы узнать подробности",
"obtainDate": "Получено ${date}", "obtainDate": "Получено ${date}",
"fetchDate": "На момент ${date}", "fetchDate": "На момент ${date}",
"exactGametime": "Время, проведённое в игре", "exactGametime": "Время, проведённое в игре",
@ -115,6 +117,9 @@
"match": "Матч", "match": "Матч",
"roundNumber": "Раунд $n", "roundNumber": "Раунд $n",
"statsFor": "Статистика за", "statsFor": "Статистика за",
"matchLength": "Продолжительность матча",
"roundLength": "Продолжительность раунда",
"winner": "Победитель",
"registred": "Зарегистрирован", "registred": "Зарегистрирован",
"playedTL": "Играл в Тетра Лигу", "playedTL": "Играл в Тетра Лигу",
"winChance": "Шансы на победу", "winChance": "Шансы на победу",