releasing this, then fully focusing on redesign

This commit is contained in:
dan63047 2024-09-05 00:12:26 +03:00
parent 3d28e5a214
commit 1b5b9d7a3f
8 changed files with 125 additions and 145 deletions

View File

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 1216 (608 per locale)
/// Strings: 1210 (605 per locale)
///
/// Built on 2024-08-07 at 15:58 UTC
/// Built on 2024-09-04 at 20:41 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
@ -224,9 +224,6 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String get smooth => 'Smooth';
String get postSeason => 'Off-season';
String get seasonStarts => 'Season starts in:';
String get myMessadgeHeader => 'A messadge from dan63';
String get myMessadgeBody => 'TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.';
String preSeasonMessage({required Object n}) => 'Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied';
String get nanow => 'Not avaliable for now...';
String seasonEnds({required Object countdown}) => 'Season ends in ${countdown}';
String get seasonEnded => 'Season has ended';
@ -293,7 +290,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
String stateViewTitle({required Object nickname, required Object date}) => '${nickname} account on ${date}';
String statesViewTitle({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
String matchesViewTitle({required Object nickname}) => '${nickname} TL matches';
String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD';
String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
String stateRemoved({required Object date}) => '${date} state was removed from database!';
String matchRemoved({required Object date}) => '${date} match was removed from database!';
String get viewAllMatches => 'View all matches';
@ -938,9 +935,6 @@ class _StringsRu implements Translations {
@override String get smooth => 'Гладкий';
@override String get postSeason => 'Внесезонье';
@override String get seasonStarts => 'Сезон начнётся через:';
@override String get myMessadgeHeader => 'Сообщение от dan63';
@override String get myMessadgeBody => 'TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.';
@override String preSeasonMessage({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона';
@override String get nanow => 'Пока недоступно...';
@override String seasonEnds({required Object countdown}) => 'Сезон закончится через ${countdown}';
@override String get seasonEnded => 'Сезон закончился';
@ -1007,7 +1001,7 @@ class _StringsRu implements Translations {
@override String stateViewTitle({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
@override String statesViewTitle({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
@override String matchesViewTitle({required Object nickname}) => 'Матчи аккаунта ${nickname}';
@override String statesViewEntry({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD';
@override String statesViewEntry({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
@override String stateRemoved({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!';
@override String matchRemoved({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!';
@override String get viewAllMatches => 'Все матчи';
@ -1644,9 +1638,6 @@ extension on Translations {
case 'smooth': return 'Smooth';
case 'postSeason': return 'Off-season';
case 'seasonStarts': return 'Season starts in:';
case 'myMessadgeHeader': return 'A messadge from dan63';
case 'myMessadgeBody': return 'TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.';
case 'preSeasonMessage': return ({required Object n}) => 'Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied';
case 'nanow': return 'Not avaliable for now...';
case 'seasonEnds': return ({required Object countdown}) => 'Season ends in ${countdown}';
case 'seasonEnded': return 'Season has ended';
@ -1713,7 +1704,7 @@ extension on Translations {
case 'stateViewTitle': return ({required Object nickname, required Object date}) => '${nickname} account on ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} states of ${nickname} account';
case 'matchesViewTitle': return ({required Object nickname}) => '${nickname} TL matches';
case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => 'Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD';
case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
case 'stateRemoved': return ({required Object date}) => '${date} state was removed from database!';
case 'matchRemoved': return ({required Object date}) => '${date} match was removed from database!';
case 'viewAllMatches': return 'View all matches';
@ -2274,9 +2265,6 @@ extension on _StringsRu {
case 'smooth': return 'Гладкий';
case 'postSeason': return 'Внесезонье';
case 'seasonStarts': return 'Сезон начнётся через:';
case 'myMessadgeHeader': return 'Сообщение от dan63';
case 'myMessadgeBody': return 'TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.';
case 'preSeasonMessage': return ({required Object n}) => 'Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона';
case 'nanow': return 'Пока недоступно...';
case 'seasonEnds': return ({required Object countdown}) => 'Сезон закончится через ${countdown}';
case 'seasonEnded': return 'Сезон закончился';
@ -2343,7 +2331,7 @@ extension on _StringsRu {
case 'stateViewTitle': return ({required Object nickname, required Object date}) => 'Аккаунт ${nickname} ${date}';
case 'statesViewTitle': return ({required Object number, required Object nickname}) => '${number} состояний аккаунта ${nickname}';
case 'matchesViewTitle': return ({required Object nickname}) => 'Матчи аккаунта ${nickname}';
case 'statesViewEntry': return ({required Object level, required Object gameTime, required Object friends, required Object rd}) => '${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD';
case 'statesViewEntry': return ({required Object level, required Object glicko, required Object rd, required Object games}) => '${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно';
case 'stateRemoved': return ({required Object date}) => 'Состояние от ${date} было удалено из локальной базы данных!';
case 'matchRemoved': return ({required Object date}) => 'Матч от ${date} был удален из локальной базы данных!';
case 'viewAllMatches': return 'Все матчи';

View File

@ -213,6 +213,7 @@ class TetrioService extends DB {
_players.removeWhere((key, value) => key == id);
_tetrioStreamController.add(_players);
}
await db.delete(tetrioLeagueTable, where: "id LIKE ?", whereArgs: ["$id%"]);
}
/// Gets nickname from database or requests it from API if missing.
@ -1117,46 +1118,14 @@ class TetrioService extends DB {
}
}
/// Remove state (which is [tetrioPlayer]) from the local database
// Future<void> deleteState(TetrioPlayer tetrioPlayer) async {
// await ensureDbIsOpen();
// final db = getDatabaseOrThrow();
// //List<TetrioPlayer> states = await getPlayer(tetrioPlayer.userId);
// // removing state from map that contain every state of each user
// states.removeWhere((element) => element.state == tetrioPlayer.state);
// // Making map of the states (without deleted one)
// final Map<String, dynamic> statesJson = {};
// // for (var e in states) {
// // statesJson.addEntries({(e.state.millisecondsSinceEpoch ~/ 1000).toString(): e.toJson()}.entries);
// // }
// // Rewriting database entry with new json
// await db.update(tetrioUsersTable, {idCol: tetrioPlayer.userId, nickCol: tetrioPlayer.username, statesCol: jsonEncode(statesJson)},
// where: '$idCol = ?', whereArgs: [tetrioPlayer.userId]);
// _tetrioStreamController.add(_players);
// }
/// Returns list of all states of player with given [id] from database. Can return empty list if player
/// was not found.
// Future<List<TetrioPlayer>> getPlayer(String id) async {
// await ensureDbIsOpen();
// final db = getDatabaseOrThrow();
// List<TetrioPlayer> states = [];
// final results = await db.query(tetrioUsersTable, limit: 1, where: '$idCol = ?', whereArgs: [id.toLowerCase()]);
// if (results.isEmpty) {
// return states; // it empty
// } else {
// dynamic rawStates = results.first['jsonStates'] as String;
// rawStates = json.decode(rawStates);
// // recreating objects of states
// rawStates.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), id, results.first[nickCol] as String)));
// // updating the stream
// _players.removeWhere((key, value) => key == id);
// _players.addEntries({states.last.userId: states.last.username}.entries);
// _tetrioStreamController.add(_players);
// return states;
// }
// }
/// Remove state, which has [dbID] from the local database
/// ([dbid] is a concatenation of player id and UINX milliseconds in hex)
Future<void> deleteState(String dbID) async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
int result = await db.delete(tetrioLeagueTable, where: "id = ?", whereArgs: [dbID]);
if (result == 0) throw Exception("Failed to remove a row $dbID - it's probably not exist");
}
/// Retrieves general stats of [user] (nickname or id) from Tetra Channel api. Returns [TetrioPlayer] object of this user.
/// If [isItDiscordID] is true, function expects [user] to be a discord user id. Throws an exception if fails to retrieve.
@ -1256,17 +1225,14 @@ class TetrioService extends DB {
}
}
/// Retrieves whole [tetrioUsersTable] and returns Map with [TetrioPlayer] objects of everyone in database
Future<Map<String, List<TetrioPlayer>>> getAllPlayers() async {
/// Retrieves whole [tetrioUsersTable] and returns Map {id: nickname} of everyone in database
Future<Map<String, String>> getAllPlayers() async {
await ensureDbIsOpen();
final db = getDatabaseOrThrow();
final players = await db.query(tetrioUsersTable);
Map<String, List<TetrioPlayer>> data = {};
Map<String, String> data = {};
for (var entry in players){
var test = json.decode(entry['jsonStates'] as String);
List<TetrioPlayer> states = [];
test.forEach((k, v) => states.add(TetrioPlayer.fromJson(v, DateTime.fromMillisecondsSinceEpoch(int.parse(k) * 1000), entry[idCol] as String, entry[nickCol] as String)));
data.addEntries({states.last.userId: states}.entries);
data[entry[idCol] as String] = entry[nickCol] as String;
}
return data;
}

View File

@ -4,14 +4,15 @@ 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/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:tetra_stats/widgets/tl_thingy.dart';
import 'package:tetra_stats/widgets/user_thingy.dart';
import 'package:window_manager/window_manager.dart';
final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms();
class StateView extends StatefulWidget {
final TetrioPlayer state;
final TetraLeague state;
const StateView({super.key, required this.state});
@override
@ -28,7 +29,7 @@ class StateState extends State<StateView> {
_scrollController = ScrollController();
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.stateViewTitle(nickname: widget.state.username.toUpperCase(), date: dateFormat.format(widget.state.state))}");
windowManager.setTitle("State from ${timestamp(widget.state.timestamp)}");
}
super.initState();
}
@ -49,15 +50,12 @@ class StateState extends State<StateView> {
final t = Translations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(t.stateViewTitle(nickname: widget.state.username.toUpperCase(), date: dateFormat.format(widget.state.state))),
title: Text("State from ${timestamp(widget.state.timestamp)}"),
),
backgroundColor: Colors.black,
body: SafeArea(
child: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, value) {
return [SliverToBoxAdapter(child: UserThingy(player: widget.state, showStateTimestamp: true, setState: _justUpdate))];
},
body: Container())));
child: TLThingy(tl: widget.state, userID: widget.state.id, states: [])
)
);
}
}

View File

@ -1,18 +1,19 @@
import 'dart:io';
import 'package:flutter/foundation.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/main.dart' show teto;
import 'package:tetra_stats/utils/numers_formats.dart';
import 'package:tetra_stats/views/mathes_view.dart';
import 'package:tetra_stats/views/state_view.dart';
import 'package:tetra_stats/widgets/text_timestamp.dart';
import 'package:window_manager/window_manager.dart';
class StatesView extends StatefulWidget {
final List<TetraLeague> states;
const StatesView({super.key, required this.states});
final String nickname;
final String id;
const StatesView({required this.nickname, required this.id, super.key});
@override
State<StatefulWidget> createState() => StatesState();
@ -25,7 +26,7 @@ class StatesState extends State<StatesView> {
void initState() {
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS){
windowManager.getTitle().then((value) => oldWindowTitle = value);
windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}");
//windowManager.setTitle("Tetra Stats: ${t.statesViewTitle(number: widget.states.length, nickname: widget.states.last.id.toUpperCase())}");
}
super.initState();
}
@ -41,14 +42,14 @@ class StatesState extends State<StatesView> {
final t = Translations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(t.statesViewTitle(number: widget.states.length, nickname: widget.states.first.id)),
title: Text(t.statesViewTitle(number: "", nickname: widget.nickname)),
actions: [
IconButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MatchesView(userID: widget.states.first.id, username: widget.states.first.id),
builder: (context) => MatchesView(userID: widget.id, username: widget.nickname),
),
);
}, icon: const Icon(Icons.list), tooltip: t.viewAllMatches)
@ -56,30 +57,63 @@ class StatesState extends State<StatesView> {
),
backgroundColor: Colors.black,
body: SafeArea(
child: ListView.builder(
itemCount: widget.states.length,
child: FutureBuilder<List<TetraLeague>>(future: teto.getStates(widget.id), builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return const Center(child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done:
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data!.length,
prototypeItem: ListTile(
title: Text(""),
subtitle: Text("", style: TextStyle(color: Colors.grey)),
trailing: IconButton(icon: const Icon(Icons.delete_forever), onPressed: (){}),
),
itemBuilder: (context, index) {
return ListTile(
title: Text(timestamp(widget.states[index].timestamp)),
//subtitle: Text(t.statesViewEntry(level: widget.states[index].level.toStringAsFixed(2), gameTime: widget.states[index].gameTime, friends: widget.states[index].friendCount, rd: 0)),
title: Text(timestamp(snapshot.data![index].timestamp)),
subtitle: Text(
t.statesViewEntry(level: f2.format(snapshot.data![index].tr), games: intf.format(snapshot.data![index].gamesPlayed), glicko: snapshot.data![index].glicko != null ? f2.format(snapshot.data![index].glicko) : "---", rd: snapshot.data![index].rd != null ? f2.format(snapshot.data![index].rd) : "--"),
style: TextStyle(color: Colors.grey),
),
trailing: IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () {
//DateTime nn = widget.states[index].state;
// teto.deleteState(widget.states[index]).then((value) => setState(() {
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(nn)))));
// }));
teto.deleteState(snapshot.data![index].id+snapshot.data![index].timestamp.millisecondsSinceEpoch.toRadixString(16)).then((value) => setState(() {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stateRemoved(date: timestamp(snapshot.data![index].timestamp)))));
}));
},
),
onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => StateView(state: widget.states[index]),
// ),
// );
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StateView(state: snapshot.data![index]),
),
);
},
);
})));
});
} else if (snapshot.hasError) {
return Center(child:
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(snapshot.error.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 42, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(snapshot.stackTrace.toString(), style: const TextStyle(fontFamily: "Eurostile Round", fontSize: 18), textAlign: TextAlign.center),
),
],
)
);
}
break;
}
return const Center(child: Text('default case of FutureBuilder', style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 42), textAlign: TextAlign.center));
}
)));}
}
}

View File

@ -78,7 +78,7 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
case ConnectionState.active:
return const Center(child: CircularProgressIndicator(color: Colors.white));
case ConnectionState.done:
final allPlayers = (snapshot.data != null) ? snapshot.data as Map<String, List<TetrioPlayer>> : <String, List<TetrioPlayer>>{};
final allPlayers = (snapshot.data != null) ? snapshot.data as Map<String, String> : <String, String>{};
List<String> keys = allPlayers.keys.toList();
return NestedScrollView(
headerSliverBuilder: (context, value) {
@ -107,24 +107,24 @@ class TrackedPlayersState extends State<TrackedPlayersView> {
body: ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
print(index);
return ListTile(
title: Text(t.trackedPlayersEntry(nickname: allPlayers[keys[index]]!.last.username, numberOfStates: allPlayers[keys[index]]!.length)),
subtitle: Text(t.trackedPlayersDescription(firstStateDate: timestamp(allPlayers[keys[index]]!.first.state), lastStateDate: timestamp(allPlayers[keys[index]]!.last.state))),
title: Text(allPlayers[keys[index]]??"No nickname (huh?)"),
subtitle: Text(keys[index], style: TextStyle(fontFamily: "Eurostile Round Condensed", color: Colors.grey)),
trailing: IconButton(
icon: const Icon(Icons.delete_forever),
onPressed: () {
String nn = allPlayers[keys[index]]!.last.username;
setState(() {teto.deletePlayer(keys[index]);});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: nn))));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.trackedPlayersStatesDeleted(nickname: allPlayers[keys[index]]??"No nickname (huh?)"))));
},
),
onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => StatesView(states: allPlayers[keys[index]]!),
// ),
// );
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StatesView(nickname: allPlayers[keys[index]]!, id: keys[index]),
),
);
},
);
}));

View File

@ -2,7 +2,7 @@ name: tetra_stats
description: Track your and other player stats in TETR.IO
publish_to: 'none'
version: 1.6.8+34
version: 1.6.9+35
environment:
sdk: '>=3.0.0'

View File

@ -89,9 +89,6 @@
"smooth": "Smooth",
"postSeason": "Off-season",
"seasonStarts": "Season starts in:",
"myMessadgeHeader": "A messadge from dan63",
"myMessadgeBody": "TETR.IO Tetra Channel API has been seriously modified after the last update, therefore, some functions may not work. I will try to catch up and add new stats (and return back the old ones) as soon, as public docs on new Tetra Channel API will be available.",
"preSeasonMessage": "Right now you can play unranked FT3 matches with hidden glicko (200 RD 🙂).\nSeason ${n} rules applied",
"nanow": "Not avaliable for now...",
"seasonEnds": "Season ends in ${countdown}",
"seasonEnded": "Season has ended",
@ -158,7 +155,7 @@
"stateViewTitle": "${nickname} account on ${date}",
"statesViewTitle": "${number} states of ${nickname} account",
"matchesViewTitle": "${nickname} TL matches",
"statesViewEntry": "Level ${level}, ${gameTime} of gametime, ${friends} friends, ${rd} RD",
"statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно",
"stateRemoved": "${date} state was removed from database!",
"matchRemoved": "${date} match was removed from database!",
"viewAllMatches": "View all matches",

View File

@ -89,9 +89,6 @@
"smooth": "Гладкий",
"postSeason": "Внесезонье",
"seasonStarts": "Сезон начнётся через:",
"myMessadgeHeader": "Сообщение от dan63",
"myMessadgeBody": "TETR.IO Tetra Channel API был серьёзно изменён после последнего обновления, поэтому некоторый функционал может не работать. Я постараюсь добавить новую статистику (и вернуть старую) как только будут опубликована новая документация по данному API.",
"preSeasonMessage": "Прямо сейчас вы можете сыграть безранговый матч до трёх побед со скрытым Glicko (200 RD 🙂).\nПрименяются правила ${n} сезона",
"nanow": "Пока недоступно...",
"seasonEnds": "Сезон закончится через ${countdown}",
"seasonEnded": "Сезон закончился",
@ -158,7 +155,7 @@
"stateViewTitle": "Аккаунт ${nickname} ${date}",
"statesViewTitle": "${number} состояний аккаунта ${nickname}",
"matchesViewTitle": "Матчи аккаунта ${nickname}",
"statesViewEntry": "${level} уровень, ${gameTime} сыграно, ${friends} друзей, ${rd} RD",
"statesViewEntry": "${level} TR, ${glicko}±${rd} Glicko, ${games} игр сыграно",
"stateRemoved": "Состояние от ${date} было удалено из локальной базы данных!",
"matchRemoved": "Матч от ${date} был удален из локальной базы данных!",
"viewAllMatches": "Все матчи",