1.4.0?
This commit is contained in:
parent
14def01b57
commit
d0ead79068
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 'Шансы на победу';
|
||||
|
|
|
@ -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');
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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,43 +755,30 @@ 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(
|
||||
// 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: LineChart(
|
||||
child: Stack(
|
||||
children: [
|
||||
LineChart(
|
||||
key: graphKey,
|
||||
LineChartData(
|
||||
lineBarsData: [LineChartBarData(spots: widget.data)],
|
||||
|
@ -749,11 +803,41 @@ class _HistoryChartThigyState extends State<_HistoryChartThigy> {
|
|||
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())))])];
|
||||
},))
|
||||
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))
|
||||
|
|
|
@ -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,8 +175,7 @@ class RankState extends State<RankView> with SingleTickerProviderStateMixin {
|
|||
child: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (context, value) {
|
||||
return [
|
||||
SliverToBoxAdapter(
|
||||
return [ SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
Flex(
|
||||
|
@ -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: 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: [
|
||||
Padding(
|
||||
padding: bigScreen
|
||||
? const EdgeInsets.fromLTRB(
|
||||
40, 40, 40, 48)
|
||||
: const EdgeInsets.fromLTRB(
|
||||
0, 40, 16, 48),
|
||||
child: ScatterChart(
|
||||
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"])
|
||||
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.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,
|
||||
),
|
||||
);
|
||||
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)))
|
||||
],
|
||||
|
|
|
@ -205,13 +205,13 @@ class TlMatchResultState extends State<TlMatchResultView> {
|
|||
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));
|
||||
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("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{
|
||||
return const Text("skill issue", textAlign: TextAlign.center);
|
||||
return Text("${snapshot.error.toString()}\n${snapshot.stackTrace}", textAlign: TextAlign.center);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "Шансы на победу",
|
||||
|
|
Loading…
Reference in New Issue