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 }}
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

View File

@ -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;
}

View File

@ -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<AppLocale, Translations> {
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<AppLocale, Translations> {
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 'Шансы на победу';

View File

@ -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<String, List<News>> _newsCache = {};
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 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<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
@ -129,16 +130,28 @@ class TetrioService extends DB {
Future<List<dynamic>> 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<void> 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<Map<String, dynamic>> fetchRecords(String userID) async {

View File

@ -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<FlSpot> data;
final String title;
final String yAxisTitle;
final bool bigScreen;
final double leftSpace;
final NumberFormat yFormat;
const _HistoryChartThigy({required this.data, required this.title, required this.yAxisTitle, required this.bigScreen, required this.leftSpace, required this.yFormat});
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<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
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))

View File

@ -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 = <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 {
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<RankView> 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<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
@ -51,14 +121,50 @@ class RankState extends State<RankView> 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<TetrioPlayerFromLeaderboard> 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<RankView> 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<RankView> 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<RankView> 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<RankView> 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)))
],

View File

@ -197,24 +197,24 @@ class TlMatchResultState extends State<TlMatchResultView> {
),
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<TlMatchResultView> {
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)
],
)

View File

@ -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<Widget>? 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(

View File

@ -345,7 +345,7 @@ class _TLThingyState extends State<TLThingy> {
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,

View File

@ -23,6 +23,11 @@ Future<void> copyToClipboard(String text) async {
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 {
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>[
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(

View File

@ -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'

View File

@ -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",

View File

@ -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": "Шансы на победу",