feat(commands): еще немного команд
This commit is contained in:
@@ -57,6 +57,7 @@ class Api {
|
|||||||
_sessionExpiredController.stream;
|
_sessionExpiredController.stream;
|
||||||
Stream<String> get handshakeSuccessStream =>
|
Stream<String> get handshakeSuccessStream =>
|
||||||
_handshakeSuccessController.stream;
|
_handshakeSuccessController.stream;
|
||||||
|
Stream<String> get errorStream => _dispatcher.errorStream;
|
||||||
SessionState get state => _sessionState;
|
SessionState get state => _sessionState;
|
||||||
|
|
||||||
StreamSubscription<Uint8List>? _dataSubscription;
|
StreamSubscription<Uint8List>? _dataSubscription;
|
||||||
|
|||||||
@@ -190,8 +190,8 @@ class AppDatabase {
|
|||||||
onCreate: (db, _) => _createTables(db),
|
onCreate: (db, _) => _createTables(db),
|
||||||
onUpgrade: (db, oldVersion, newVersion) async {
|
onUpgrade: (db, oldVersion, newVersion) async {
|
||||||
if (oldVersion < 2) {
|
if (oldVersion < 2) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(
|
||||||
'ALTER TABLE profile ADD COLUMN is_active INTEGER NOT NULL DEFAULT 0',
|
db, 'profile', 'is_active', 'INTEGER NOT NULL DEFAULT 0',
|
||||||
);
|
);
|
||||||
await db.execute('DROP TABLE IF EXISTS sync_state');
|
await db.execute('DROP TABLE IF EXISTS sync_state');
|
||||||
await db.execute(_syncStateSchema);
|
await db.execute(_syncStateSchema);
|
||||||
@@ -210,47 +210,33 @@ class AppDatabase {
|
|||||||
await db.execute(_messagesSchema);
|
await db.execute(_messagesSchema);
|
||||||
}
|
}
|
||||||
if (oldVersion < 7) {
|
if (oldVersion < 7) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(db, 'profile', 'profile_options', 'TEXT');
|
||||||
'ALTER TABLE profile ADD COLUMN profile_options TEXT',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (oldVersion < 8) {
|
if (oldVersion < 8) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(db, 'chats_cache', 'participants', 'TEXT');
|
||||||
'ALTER TABLE chats_cache ADD COLUMN participants TEXT',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (oldVersion < 9) {
|
if (oldVersion < 9) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(db, 'contacts', 'options', 'TEXT');
|
||||||
'ALTER TABLE contacts ADD COLUMN options TEXT',
|
await _addColumnIfMissing(db, 'chats_cache', 'options', 'TEXT');
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'ALTER TABLE chats_cache ADD COLUMN options TEXT',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (oldVersion < 10) {
|
if (oldVersion < 10) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(db, 'chats_cache', 'owner', 'INTEGER');
|
||||||
'ALTER TABLE chats_cache ADD COLUMN owner INTEGER',
|
await _addColumnIfMissing(db, 'chats_cache', 'admins', 'TEXT');
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'ALTER TABLE chats_cache ADD COLUMN admins TEXT',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (oldVersion < 11) {
|
if (oldVersion < 11) {
|
||||||
await _createIndexes(db);
|
await _createIndexes(db);
|
||||||
}
|
}
|
||||||
if (oldVersion < 12) {
|
if (oldVersion < 12) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(db, 'chats_cache', 'last_msg_status', 'TEXT');
|
||||||
'ALTER TABLE chats_cache ADD COLUMN last_msg_status TEXT',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (oldVersion < 13) {
|
if (oldVersion < 13) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(
|
||||||
'ALTER TABLE messages ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0',
|
db, 'messages', 'deleted', 'INTEGER NOT NULL DEFAULT 0',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
await db.execute(
|
await _addColumnIfMissing(
|
||||||
'ALTER TABLE chats_cache ADD COLUMN in_list INTEGER NOT NULL DEFAULT 1',
|
db, 'chats_cache', 'in_list', 'INTEGER NOT NULL DEFAULT 1',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -281,6 +267,18 @@ class AppDatabase {
|
|||||||
await _createIndexes(db);
|
await _createIndexes(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> _addColumnIfMissing(
|
||||||
|
Database db,
|
||||||
|
String table,
|
||||||
|
String column,
|
||||||
|
String definition,
|
||||||
|
) async {
|
||||||
|
final info = await db.rawQuery('PRAGMA table_info($table)');
|
||||||
|
final exists = info.any((row) => row['name'] == column);
|
||||||
|
if (exists) return;
|
||||||
|
await db.execute('ALTER TABLE $table ADD COLUMN $column $definition');
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> _createIndexes(Database db) async {
|
static Future<void> _createIndexes(Database db) async {
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(account_id, chat_id, time DESC)',
|
'CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(account_id, chat_id, time DESC)',
|
||||||
|
|||||||
@@ -17,10 +17,22 @@ class PacketDispatcher {
|
|||||||
final Map<int, PacketHandler> _pushHandlers = {};
|
final Map<int, PacketHandler> _pushHandlers = {};
|
||||||
|
|
||||||
final _pushController = StreamController<Packet>.broadcast();
|
final _pushController = StreamController<Packet>.broadcast();
|
||||||
|
final _errorController = StreamController<String>.broadcast();
|
||||||
|
|
||||||
/// Стрим всех входящих пушей (cmd == 1)
|
/// Стрим всех входящих пушей (cmd == 1)
|
||||||
Stream<Packet> get pushStream => _pushController.stream;
|
Stream<Packet> get pushStream => _pushController.stream;
|
||||||
|
|
||||||
|
Stream<String> get errorStream => _errorController.stream;
|
||||||
|
|
||||||
|
static String? _serverErrorText(dynamic payload) {
|
||||||
|
if (payload is! Map) return null;
|
||||||
|
for (final key in ['localizedMessage', 'title']) {
|
||||||
|
final v = payload[key];
|
||||||
|
if (v is String && v.trim().isNotEmpty) return v.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Timer? _cleanupTimer;
|
Timer? _cleanupTimer;
|
||||||
|
|
||||||
PacketDispatcher() {
|
PacketDispatcher() {
|
||||||
@@ -60,10 +72,22 @@ class PacketDispatcher {
|
|||||||
if (packet.cmd == CmdType.ok ||
|
if (packet.cmd == CmdType.ok ||
|
||||||
packet.cmd == CmdType.error ||
|
packet.cmd == CmdType.error ||
|
||||||
packet.cmd == CmdType.notFound) {
|
packet.cmd == CmdType.notFound) {
|
||||||
|
final payloadLog = packet.opcode == Opcode.login
|
||||||
|
? '<скрыто: ответ login>'
|
||||||
|
: payloadForLog(packet.payload);
|
||||||
logger.i(
|
logger.i(
|
||||||
'<= {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: ${payloadForLog(packet.payload)}}',
|
'<= {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: $payloadLog}',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (packet.isError) {
|
||||||
|
final isSessionExpired = packet.payload is Map &&
|
||||||
|
packet.payload['message'] == 'FAIL_LOGIN_TOKEN';
|
||||||
|
final serverText = _serverErrorText(packet.payload);
|
||||||
|
if (serverText != null && !isSessionExpired) {
|
||||||
|
_errorController.add(serverText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final completer = _pendingRequests.remove(packet.seq);
|
final completer = _pendingRequests.remove(packet.seq);
|
||||||
_requestTimestamps.remove(packet.seq);
|
_requestTimestamps.remove(packet.seq);
|
||||||
|
|
||||||
@@ -130,5 +154,6 @@ class PacketDispatcher {
|
|||||||
_cleanupTimer?.cancel();
|
_cleanupTimer?.cancel();
|
||||||
clearPending();
|
clearPending();
|
||||||
_pushController.close();
|
_pushController.close();
|
||||||
|
_errorController.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'slash_command.dart';
|
||||||
|
|
||||||
|
const double _defaultCooldownSec = 0.15;
|
||||||
|
const int _minLength = 3;
|
||||||
|
const List<String> _fillChars = ['#', '@', '%', '&', '*'];
|
||||||
|
|
||||||
|
final Random _rng = Random();
|
||||||
|
|
||||||
|
String _fill() => _fillChars[_rng.nextInt(_fillChars.length)];
|
||||||
|
|
||||||
|
Future<void> runAnim1(CommandContext ctx) async {
|
||||||
|
var cooldownSec = _defaultCooldownSec;
|
||||||
|
var text = ctx.args;
|
||||||
|
|
||||||
|
if (text.startsWith('{')) {
|
||||||
|
final match = RegExp(r'^\{\s*([0-9]*\.?[0-9]+)\s*\}\s*').firstMatch(text);
|
||||||
|
final parsed = match != null ? double.tryParse(match.group(1)!) : null;
|
||||||
|
if (match == null || parsed == null || parsed < 0) {
|
||||||
|
ctx.notify('НЕВЕРНЫЙ СИНТАКСИС🚨🚨🚨');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cooldownSec = parsed;
|
||||||
|
text = text.substring(match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
final chars = text.runes.map(String.fromCharCode).toList();
|
||||||
|
if (chars.length < _minLength) {
|
||||||
|
ctx.notify('Минимум $_minLength символа для анимации');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ctx.isOnline()) {
|
||||||
|
ctx.notify('Нет соединения');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cooldown = Duration(milliseconds: (cooldownSec * 1000).round());
|
||||||
|
|
||||||
|
final frames = _buildFrames(chars);
|
||||||
|
if (frames.isEmpty) return;
|
||||||
|
final id = await ctx.postMessage(frames.first);
|
||||||
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
|
await playFrames(ctx, id, frames, cooldown);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _buildFrames(List<String> chars) {
|
||||||
|
final n = chars.length;
|
||||||
|
final noise = List.generate(n, (_) => _fill());
|
||||||
|
final frames = <String>[chars.join()];
|
||||||
|
|
||||||
|
for (var i = 0; i <= n + 1; i++) {
|
||||||
|
final sb = StringBuffer();
|
||||||
|
for (var p = 0; p < n; p++) {
|
||||||
|
if (p < i - 1) {
|
||||||
|
sb.write(noise[p]);
|
||||||
|
} else if (p == i - 1) {
|
||||||
|
sb.write('\$');
|
||||||
|
} else if (p == i) {
|
||||||
|
sb.write(chars[p].toUpperCase());
|
||||||
|
} else {
|
||||||
|
sb.write(chars[p]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frames.add(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var w = n; w >= 1; w -= 2) {
|
||||||
|
if (w == 1) {
|
||||||
|
frames.add('%');
|
||||||
|
} else {
|
||||||
|
final sb = StringBuffer('>');
|
||||||
|
for (var k = 0; k < w - 2; k++) {
|
||||||
|
sb.write(noise[k]);
|
||||||
|
}
|
||||||
|
sb.write('<');
|
||||||
|
frames.add(sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var w = n.isEven ? 2 : 1; w <= n; w += 2) {
|
||||||
|
final start = (n - w) ~/ 2;
|
||||||
|
frames.add(chars.sublist(start, start + w).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames.where((f) => f.trim().isNotEmpty).toList();
|
||||||
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
|
import 'anim_command.dart';
|
||||||
import 'crush_command.dart';
|
import 'crush_command.dart';
|
||||||
|
import 'epsh_files_command.dart';
|
||||||
import 'info_command.dart';
|
import 'info_command.dart';
|
||||||
import 'slash_command.dart';
|
import 'slash_command.dart';
|
||||||
|
import 'watching_command.dart';
|
||||||
|
|
||||||
const List<SlashCommand> kSlashCommands = [
|
const List<SlashCommand> kSlashCommands = [
|
||||||
SlashCommand('/test', '12345 test отображение'),
|
SlashCommand('/test', '12345 test отображение'),
|
||||||
SlashCommand('/info', 'сводка данных о человеке', run: runInfo),
|
SlashCommand('/info', 'сводка данных о человеке', run: runInfo),
|
||||||
|
SlashCommand('/anim1', 'анимация текста', run: runAnim1),
|
||||||
|
SlashCommand('/IAlwaysWatchingYou', '👁️', run: runWatching),
|
||||||
|
SlashCommand(
|
||||||
|
'/epshFiles',
|
||||||
|
'цензура слов чёрными квадратами {шанс 1-100}',
|
||||||
|
run: runEpshFiles,
|
||||||
|
),
|
||||||
SlashCommand(
|
SlashCommand(
|
||||||
'/crush',
|
'/crush',
|
||||||
'Тест устойчивости веб клиента макса',
|
'Тест устойчивости веб клиента макса',
|
||||||
@@ -14,8 +24,16 @@ const List<SlashCommand> kSlashCommands = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
SlashCommand? findSlashCommand(String text) {
|
SlashCommand? findSlashCommand(String text) {
|
||||||
|
final name = text.trimLeft().split(RegExp(r'\s')).first.toLowerCase();
|
||||||
for (final c in kSlashCommands) {
|
for (final c in kSlashCommands) {
|
||||||
if (c.name == text) return c;
|
if (c.name.toLowerCase() == name) return c;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String commandArgs(String text) {
|
||||||
|
final trimmed = text.trimLeft();
|
||||||
|
final idx = trimmed.indexOf(RegExp(r'\s'));
|
||||||
|
if (idx == -1) return '';
|
||||||
|
return trimmed.substring(idx + 1).trim();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'slash_command.dart';
|
||||||
|
|
||||||
|
const int _defaultChance = 65;
|
||||||
|
const String _square = '⬛';
|
||||||
|
|
||||||
|
final Random _rng = Random();
|
||||||
|
|
||||||
|
Future<void> runEpshFiles(CommandContext ctx) async {
|
||||||
|
var chance = _defaultChance;
|
||||||
|
var text = ctx.args;
|
||||||
|
|
||||||
|
if (text.startsWith('{')) {
|
||||||
|
final match = RegExp(r'^\{\s*(\d+)\s*\}\s*').firstMatch(text);
|
||||||
|
final parsed = match != null ? int.tryParse(match.group(1)!) : null;
|
||||||
|
if (match == null || parsed == null || parsed < 1 || parsed > 100) {
|
||||||
|
ctx.notify('НЕВЕРНЫЙ СИНТАКСИС🚨🚨🚨');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chance = parsed;
|
||||||
|
text = text.substring(match.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.trim();
|
||||||
|
if (text.isEmpty) {
|
||||||
|
ctx.notify('Нет текста');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final censored = text.replaceAllMapped(RegExp(r'\S+'), (m) {
|
||||||
|
final word = m.group(0)!;
|
||||||
|
return _rng.nextInt(100) < chance ? _square * word.runes.length : word;
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.postMessage(censored);
|
||||||
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
import '../../backend/modules/messages.dart';
|
import '../../backend/modules/messages.dart';
|
||||||
|
|
||||||
|
const String kAntiFloodNotification =
|
||||||
|
'Упс! МАХ сбросил соединение, кажется, тебе стоит немного помедлить с командами.';
|
||||||
|
const Duration _antiFloodNotificationDuration = Duration(seconds: 3);
|
||||||
|
|
||||||
class CommandContext {
|
class CommandContext {
|
||||||
final int accountId;
|
final int accountId;
|
||||||
final int chatId;
|
final int chatId;
|
||||||
final int? otherUserId;
|
final int? otherUserId;
|
||||||
|
final String args;
|
||||||
final MessagesModule messages;
|
final MessagesModule messages;
|
||||||
final bool Function() isOnline;
|
final bool Function() isOnline;
|
||||||
final bool Function() isActive;
|
final bool Function() isActive;
|
||||||
final void Function(String message) notify;
|
final void Function(String message, {Duration? duration}) notify;
|
||||||
final Future<String> Function(String text) postMessage;
|
final Future<String> Function(String text) postMessage;
|
||||||
final Future<void> Function(String id, String text) updateMessage;
|
final Future<void> Function(String id, String text) updateMessage;
|
||||||
|
|
||||||
@@ -15,6 +20,7 @@ class CommandContext {
|
|||||||
required this.accountId,
|
required this.accountId,
|
||||||
required this.chatId,
|
required this.chatId,
|
||||||
required this.otherUserId,
|
required this.otherUserId,
|
||||||
|
required this.args,
|
||||||
required this.messages,
|
required this.messages,
|
||||||
required this.isOnline,
|
required this.isOnline,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
@@ -22,6 +28,33 @@ class CommandContext {
|
|||||||
required this.postMessage,
|
required this.postMessage,
|
||||||
required this.updateMessage,
|
required this.updateMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void notifyAntiFlood() =>
|
||||||
|
notify(kAntiFloodNotification, duration: _antiFloodNotificationDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> playFrames(
|
||||||
|
CommandContext ctx,
|
||||||
|
String id,
|
||||||
|
List<String> frames,
|
||||||
|
Duration delay, {
|
||||||
|
int from = 1,
|
||||||
|
}) async {
|
||||||
|
for (var i = from; i < frames.length; i++) {
|
||||||
|
await Future.delayed(delay);
|
||||||
|
if (!ctx.isActive()) return false;
|
||||||
|
if (!ctx.isOnline()) {
|
||||||
|
ctx.notifyAntiFlood();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ctx.updateMessage(id, frames[i]);
|
||||||
|
} catch (_) {
|
||||||
|
if (ctx.isActive() && !ctx.isOnline()) ctx.notifyAntiFlood();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CommandRunner = Future<void> Function(CommandContext ctx);
|
typedef CommandRunner = Future<void> Function(CommandContext ctx);
|
||||||
|
|||||||
@@ -0,0 +1,390 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'slash_command.dart';
|
||||||
|
|
||||||
|
const Duration _frameDelay = Duration(milliseconds: 100);
|
||||||
|
const Duration _holdBeforeText = Duration(seconds: 1);
|
||||||
|
const String _endText = 'I Always Watching You';
|
||||||
|
|
||||||
|
const String _art = r'''
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$@bd$$$$$$$$$$M&B$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$%kdOW$$$$;$t$$$$$kqda$$$$$$$$$$$$
|
||||||
|
$$$$$$WwmYX@$$$$$$$$$$$$kCCO*%$$$$$$$$$$
|
||||||
|
$$$$$WwJQYXd$$$$$$$$$$$$q0LOqaM&@$@$$$$$
|
||||||
|
$$$$BhamCXUUZ$$$$$$$$$$w0QQOqa%W8&&$$$$$
|
||||||
|
$$$$$@#dpOUYzX0&$$$$BdO00waoM8@$$$$$$$$$
|
||||||
|
$$$$$$$@W*bmwwmmZpqbwpb*&B$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$8&%$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$W$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$%B%$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$WaQq$$$$$$$$$@aoMB$$$$$$$$$$$$$
|
||||||
|
$$$$$$$kq0Uo$$$$'$l$$B$$OLQh8@$$$$$$$$$$
|
||||||
|
$$$$$BkQUuc@$$$$$$$$$$$$wCJmaM$$$$$$$$$$
|
||||||
|
$$$$&o0XYzc*$$$$$$$$$$$$OOQOpaW&8$8$$$$$
|
||||||
|
$$$$%hhmCYJXq$$$$$$$$$@m0QQOq*B*8#M@$$$$
|
||||||
|
$$$$$B*pqQYUzXZ8$$$$&dO00Zboo&B$$$$$$$$$
|
||||||
|
$$$$$$$@W*pwwmZZOpqbubkoW8@$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$%&%$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$&*oa@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$u$$$$&ZczLm*%$$$$$$$$$$$$$
|
||||||
|
$$$$$$$o$$$$$$$$$$$&OuruXJOk&$$$$$$$$$$$
|
||||||
|
$$$$$$h0q$$$$$$$$$$pCunnvzJwbhW$$$$$$$$$
|
||||||
|
$$$$$8wJz0@$$$$$$&Ocrxxxuz0wdk&8%&$$$$$$
|
||||||
|
$$$$$#dZJzuzwoawJznfjjxvU0mdo&B8WW@$$$$$
|
||||||
|
$$$$$$BommYccvurrjrnvzQmpkM8B$$$$$$$$$$$
|
||||||
|
$$$$$$$$$@W*abppZwbaWW&B$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$@$X$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$hCJXx0wW$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$%$$$Bc{[[1fuOhB$$$$$$$$$$$$
|
||||||
|
$$$$$$$@$$$$$$$$$$x|(/nnzJJzOo$$$$$$$$$$
|
||||||
|
$$$$$$dp$$$$$$$$$q(1|txucJ0QOqW8$$$$$$$$
|
||||||
|
$$$$@8qJM$$$$$$@zj/|(|jnz0Zwbo#8$&$$$$$$
|
||||||
|
$$$$$WdmUvqWWpYutt(|rvvvJmph8%@$(8@$$$$$
|
||||||
|
$$$$$$BomwJvcrrfrnxxvvLOdaM%@$$$$$$$$$$$
|
||||||
|
$$$$$$$$$@WahpqpwpwaMW&B$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$B@Q0$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$wLXvvqpB$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$b/}[[(jnZMB$$$$$$$$$$$$
|
||||||
|
$$$$$$8$$$$$$$$$$hf(1fnvJYUzq8$$$$$$$$$$
|
||||||
|
$$$$$@ZM$$$$$$$$$J|{/rrnXQCQOhh$$$$$$$$$
|
||||||
|
$$$$88OC@$$$$$$@zjtt(tuvL0ZqboM@8M$$$$$$
|
||||||
|
$$$$$#pZYJ#%&pYnf||jnvuzCwd*8@@8UB@$$$$$
|
||||||
|
$$$$$$%aqOYzvrjrjrvuzX0qbaWB@$$$$$$$$$$$
|
||||||
|
$$$$$$$$$B&Wopppqwd#MWB@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$hZYnrJZkB$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$@$$oj{-_?(xYh#$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$@8f{[1|nvzzzJhB$$$$$$$$$$
|
||||||
|
$$$$$$d$$$$$$$$$mjj1fxruUJJCmh*$$$$$$$$$
|
||||||
|
$$$$8&wd$$$$$$$krt|)(jvXQQwqkWM$Bq$$$$$$
|
||||||
|
$$$$$#pZQa$$$wUnt|trnuuU0pkM%@@Bk%$$$$$$
|
||||||
|
$$$$$$BopOYUcnrxxuvXXYQwh#&B$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$B&&*hpddp0MW&%@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$Moo&k@B$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$dYrj|Xwh%$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$%$$@0l'~~[}nQd#@$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$w/11(ruzzXvCb@$$$$$$$$$$
|
||||||
|
$$$$$&k$$$$$$$$$U/|1jfuXJCQ0w&M$$$$$$$$$
|
||||||
|
$$$$&WZ*$$$$$$BYjjf/fxcQ0Owp##%$Qp$$$$$$
|
||||||
|
$$$$$MpOCdB&oJujtfrxnucCwdo&@$$%&@$$$$$$
|
||||||
|
$$$$$$B*qqJzzuuvuXXJQZdbo8%$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$B8&*#M&M8%B@$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$##MkkB%$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$hJXv(rZq&$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$*@Wof<>_?{/zw*8$$$$$$$$$$$$
|
||||||
|
$$$$$$@$$$$$$$$$8x{1)juczXcJq&$$$$$$$$$$
|
||||||
|
$$$$$@p$$$$$$$$$Zt||tnnzUUCQq*h$$$$$$$$$
|
||||||
|
$$$$%MqZ$$$$$$$wxjf1|fcU0Omq*#W$hx$$$$$$
|
||||||
|
$$$$$WbwJqB@#OvrftfxuucU0daW@@$8&@$$$$$$
|
||||||
|
$$$$$$@#ppLYcuvcvzXUCOpkoW8@$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$@8W#**#%W88B$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$8**Qao8$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$WYrn|uYQo$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$t0r|)1ruXUYLa$$$$$$$$$$$$
|
||||||
|
$$$$$$%$$$$$$$$$n{[1{|jzYXJLpW$$$$$$$$$$
|
||||||
|
$$$$$@O8$$$$$$8u{[{[}/nUCCZZbh#$$$$$$$$$
|
||||||
|
$$$$8oZUZ8$B*v/[}]{1f/nvLmw#BBB$ut$$$$$$
|
||||||
|
$$$$$&hwYvunf((()ttjnxvzUmbM%@&W$$$$$$$$
|
||||||
|
$$$$$$$WbwOXzXvvuvzJQmqk*&%$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$@$8MW&%&%B@$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$*kkwqk@$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$wx|)(nuJh%$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$@B(^(1(jrvYXYQ*$$$$$$$$$$$$
|
||||||
|
$$$$$W$$$$$$$$$Ott[}(|rzJUQZob$$$$$$$$$$
|
||||||
|
$$$$$OC$$$$$$Mv]]{}}{fuCQmqdhM$$$$$$$$$$
|
||||||
|
$$$BWpCvJkaLx([[[[}1jzUJ0mpM%%$th8$$$$$$
|
||||||
|
$$$$B*dJvunt){11)t//xnucCwa8@$&MB$$$$$$$
|
||||||
|
$$$$$$%awqYvJUvvvvULmmh*&%@$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$B8&W&8&BWB$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$MW@$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$BhZwpW$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$%M@$$$$$$$$$$aLcvcUw&$$$$$$$$$$$$
|
||||||
|
$$$$$$dCp$$$$$$$$$$$*0vzYwOpb#$$$$$$$$$$
|
||||||
|
$$$$$WJXOB$$$$$$$$$$pLYXXJOmpk*&$$$$$$$$
|
||||||
|
$$$$#kQzJ0&$$$$$$$%QvuvvcU0wda#@$%$$$$$$
|
||||||
|
$$$$BobOUccYOo*o0UuxnnvcY0qk#%$$$X8$$$$$$
|
||||||
|
$$$$$@8hpQUzczcvnuuvzU0wk#%B$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$B&M*hdqwqa#M8B$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$hm@$$$$$$$$$$MB$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$%pYX@$$$$$$$$$$$@wmwo$$$$$$$$$$$$
|
||||||
|
$$$$$$pcxrc$$$$$$$$$$$$$ZCLda%$$$$$$$$$$
|
||||||
|
$$$$$*Jvuuna$$$$$$$$$$$kqOJZd*8B$$$$$$$$
|
||||||
|
$$$@opZCJcYuC@$$$$$$$B0JLLJLwo8W&$$$$$$$
|
||||||
|
$$$$BabUcYvvnncCkkd0CvvzzCLq#%BB$8$$$$$$
|
||||||
|
$$$$$$8aw0UzcczYccvvzU0qk#%B$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$%&#obdqqZboW&@$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$%$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$#ZqqB$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$#mcv(na$$$$$$$$$$$$MB$$$$$$$$$$$$
|
||||||
|
$$$$$@Zzf}1{u&$$$B$$$$$$$$Wko@$$$$$$$$$$
|
||||||
|
$$$$@wXnrjjfnB$$$$$$$$$$$$ohoWB$$$$$$$$$
|
||||||
|
$$$@WZQQJcuxuUM$$$$$$$$$@kqpaM8W]$$$$$$$
|
||||||
|
$$$$%haQXuvUvuncw@$$$$8bmZZb#%8%b#$$$$$$
|
||||||
|
$$$$$@WdpOYYzczzUUJLLZmphW%@$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$8&*abpqZbk*#8@$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$@M&pwkmpdB$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$8dZqLJnY{uq$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$8pX//[]?++]p`$@$$$|$$$$$$$$$$$$$$$
|
||||||
|
$$$$$%hJYf(}__-]U$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$hZYXcnnrrrjjC$$$$$$$$$$$$$$$$@$@$$$$
|
||||||
|
$$$$8hbmZmOYXcuvXp@$$$$$$$$$$$$$$m8$$$$$
|
||||||
|
$$$$$$WkbmJccLJUUUQo$$$$$$$&8@$$$$$$$$$$
|
||||||
|
$$$$$$$$%MoqbkbrOwdqdkoMWB$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$@B$$$$$$$$$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$Bq*YqmCqb&$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$%baQXJuOvzJJpY$$$@$$$$$$$$$$$$$
|
||||||
|
$$$$$$$88kZXY/t)|j/nh$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$@@kZwCvuj/1)|/C$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$@&#dZqLUYYXzvcZ$$$$$$$$$$$$$$$B@$$$$
|
||||||
|
$$$$$$@WhMkqq0QCLCQZbB$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$%MkwppqZwwqpko$$$@%8B@$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$B%8W8&&%B@$$$hmhoZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$<&Wp8$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$akqCZmbY0d%$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$*oqYJjxfxYp$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$hwwCzvj/jfq$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$8&dpOLLUzuXk$$$$$$$$$$$$$$$$$B$$$$
|
||||||
|
$$$$$$$&oopww0LJCmW$$$$$$$$$$$%@$$$$$$$$
|
||||||
|
$$$$$$$$$BMaqpdwmpqa$$$$$$W&B@$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$%8a&&M&8%@$$hmhaZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$%kqO."0CucOZkW$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$BabJL(zjC({Xx/8@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$**qCfr1{{}?[{C$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$@$bJ0vnxf[][}}O$$$$$$$$$$$$$$$@$$$$
|
||||||
|
$$$$$@M*pZwJUvzuvnn&$$$$$$$$$$$$$$$@$$$$
|
||||||
|
$$$$$$@WaooqkOQQCUUZ@$$$$$$$$$$@$$$$$$$$
|
||||||
|
$$$$$$$$$BWawbqqqwmwp#$$$$@&B$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$@88#8B&W&@$$$hmhaZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$%Wb$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$@#wQtnW$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$p0r}({tB$$$$$$$$$$$$kdB$$$$$$$$$
|
||||||
|
$$$$$$$hXn(()||Z$$$$$$$$$$$%kdh8$$$$$$$$
|
||||||
|
$$$$$@oLJXcujfrrQ$$$$$$$$@dOOwqo&&$$$$$$
|
||||||
|
$$$$$&#qO0nrfrjj(jxQoWWb0UzcULda*k8#B$$$
|
||||||
|
$$$$$$@*bqYvxvjrrrrvnnrnvzJwhM88@@@$$$$$
|
||||||
|
$$$$$$$$@8#bdbqqO0Lpdkh*8B$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$@$$$@$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$WwB@$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$&*oqwZbwZ00M$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$%ao*dqQcXYOvd$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$%$Mhow0CUYzUC$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$%###kdbwm0QL@$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$@&kqkbqpwdbB$$$$$$BB@$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$@B%###&MW&M8B@hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$8*&WaB$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$BQnv/vZZ&@$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$@Bpl`+]}/z0h&$$$$$$$$$$$
|
||||||
|
$$$$$$$B$$$$$$$$$$kx11(fuzzYCq&$$$$$$$$$
|
||||||
|
$$$$$$$wk$$$$$$$$%buftfuuzJJ0OaoB$$$$$$$
|
||||||
|
$$$$$@aq0$$$$$$$$dzrf/fncCQ0wpo&@#O@$$$$
|
||||||
|
$$$$$$&ddLp$$$$oCujfrnuuvJZqoM%$&Q&@$$$$
|
||||||
|
$$$$$$$@#dqJUJzvunnvcXCOdh#WB@$$$$$$$$$$
|
||||||
|
$$$$$$$$$$@%W#okkbdk*#8@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$@pUcuQwa@$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$8r/{{xUmkM$$$$$$$$$$$$
|
||||||
|
$$$$$$$@B$$$$$$$$$$Ct|1/rcJ0ma@$$$$$$$$$
|
||||||
|
$$$$$$$bm$$$$$$$$$$Lr/jnuXJCQZa88$$$$$$$
|
||||||
|
$$$$$@&bLB$$$$$$$$#znrjnXYUQZph*8$a8$$$$
|
||||||
|
$$$$$$8bdCa$$$$$$qvuucnncJ0dh*@$@YMB$$$$
|
||||||
|
$$$$$$$@MdwLOOQLzuuuvcJ0ma*W8B$@$$$$$$$$
|
||||||
|
$$$$$$$$$$@&W#akdddh#W&B%$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$Bo0awdd@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$hvztzQd*%$$$$$$$$$$$$$
|
||||||
|
$$$$$$$BB$$$$$b$@$$J/t{/uzZoW@$$$$$$$$$$
|
||||||
|
$$$$$$$ma$$$$$$$$$$dxrruJCLQ0qM$$$$$$$$$
|
||||||
|
$$$$$@@Ow$$$$$$$$$$wcvuXYJ0ZZpo*B$@$$$$$
|
||||||
|
$$$$$$#amZ$$$$$$$$wJYXzzULZkko$$$J#B$$$$
|
||||||
|
$$$$$$$8kpOp%$$8qOUXzUJ0wh#W&B@@$$$$$$$$
|
||||||
|
$$$$$$$$$$Wohqpdw0ZdkboW8@$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$@*YpJhwa%$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$*rz//zJW8$$$$$$$$$$$$$
|
||||||
|
$$$$$$$M@$$$$$$$$$$0f/|xuuqba8$$$$$$$$$$
|
||||||
|
$$$$$$$Q#$$$$$$$$$$knrrcYJ00ZwM@$$$$$$$$
|
||||||
|
$$$$$88OO$$$$$$$$$@QvvczUCQZwd#*B$%$$$$$
|
||||||
|
$$$$$$#bw0$$$$$$$@OXzXvcYLmha#$$$q%$$$$$
|
||||||
|
$$$$$$$8od00o#Wk0JzXXXCmk*MW8B$$$$$$$$$$
|
||||||
|
$$$$$$$$$$8#*kqpwmdbka#88@$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$@%pohqbo&$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$apz0Q$$$$$$@wwpo$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$Mpzrtj$$$$$$$$$$@QQ0q&$$$$$$$$$$
|
||||||
|
$$$$$$$WXu|//$$$$$@$$$$@$BCULOh@$$$$$$$$
|
||||||
|
$$$$$$BOcxnju$$$$$$$$$$$$@0JLwa#%B$$$$$$
|
||||||
|
$$$$$&bp0QXnz#$$$$$$$$$$$dZZ0wbk*&%@$$$$
|
||||||
|
$$$$$$8kp0YUYz0$$$$$$$$@bZOmwq*&%@@@$$$$
|
||||||
|
$$$$$$$$WamqQUXzCw*WWhqqqkoM&%$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$B8W#oobdo#M&%@$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$pkwaC0mXzZwk@$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$oLL/un%$$$$$8UzOw#@$$$$$$$$$$$
|
||||||
|
$$$$$$$$wOf{1(@$$$$$$$$$@JJcLd$$$$$$$$$$
|
||||||
|
$$$$$$$bn/1)($$$$$$$$$$@$@UzU0mW@$$$$$$$
|
||||||
|
$$$$$B%Qcnufx$$$$$$$$$$$$@0JCZbM%&$$$$$$
|
||||||
|
$$$$$%hpOQYvva$$$$$$$$$$$km0Qmdko&%&@$$$
|
||||||
|
$$$$$$@apZYUYzL@$$$$$$$@bOOmZwo&%@@@$$$$
|
||||||
|
$$$$$$$$8omp0JUXJwM8&apqphoW&%@$$$$$$$$$
|
||||||
|
$$$$$$$$$$$@%&###kk#MW8B$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$hdwaLmmXUQqp%$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$#Q0tut#$$$$$@Jc0Z*B$$$$$$$$$$$
|
||||||
|
$$$$$$$$pOt{{}a$$$$$$$$$$UUzYZB$$$$$$$$$
|
||||||
|
$$$$$$$#c/111B$$$$$%$$$$$$QzUQZ*@$$$$$$$
|
||||||
|
$$$$$@&Qzunfr$$$$$$$$$$$$$wJC0bM&W$$$$$$
|
||||||
|
$$$$$BadZ0zvup$$$$$$$$$$$8mO0mbh*MW%W$$$
|
||||||
|
$$$$$$@*qmYJUzU@$$$$$$$$*mOOZwo8%@@%$$$$
|
||||||
|
$$$$$$$$8ompLCUXUmW%%#dqqka*&%@$$$$$$$$$
|
||||||
|
$$$$$$$$$$@@%&##*kko*W88@$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$&h*qqh0qpW*$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$@aZvntL%$$$$#Jc0pW$$$$$$$$$$$$
|
||||||
|
$$$$$$$$@wz1(}z$$$$$$$$$@UJYUb@$$$$$$$$$
|
||||||
|
$$$$$$$$Un{11O$$$$+$k$$$$@0vzCp#@$$$$$$$
|
||||||
|
$$$$$$&pcnrfj8$$$$$$$$$$$$dLLCba&%@$$$$$
|
||||||
|
$$$$$@#bOCUzvZ$$$$$$$$$$$$p0QZd**WW$%$$$
|
||||||
|
$$$$$$@*pmCUQXC@$$$$$$$$@qZOZwkW%B@BB$$$
|
||||||
|
$$$$$$$$%amdLCYzJd$$$$#qwwdo*&B$$$$$$$$$
|
||||||
|
$$$$$$$$$$$@&MokqOzh*o#&B@$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$qbad#M@$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$kOnUrqa@$@#CJwq#@$$$$$$$$$$$$
|
||||||
|
$$$$$$$$@pvjx[O$$$$$$$$$kUCUqW$$$$$$$$$$
|
||||||
|
$$$$$$$@Ux({1B$$$B`$%$$Z@MYucJo8$$$$$$$$
|
||||||
|
$$$$$$%wvrf|j$$$$$$$$$$$$$0JJZboWB@$$$$$
|
||||||
|
$$$$$BodLzYzvB$$$$$$$$$$$%Z00Zqko#1$B$$$
|
||||||
|
$$$$$$Bohw0YCYo$$$$$$$$$@m0QLZpM%%B8@$$$
|
||||||
|
$$$$$$$$WkpZJJXYq@$$$$WbwZmh*M%$$$$$$$$$
|
||||||
|
$$$$$$$$$$$8Wodpq1vbhZ*#WBB$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$@dq$$$$$$$$$$M8@$$$$$$$$$$$$$$
|
||||||
|
$$$$$$$8ddOW$$$$;$/$$@@$dqdM$$$$$$$$$$$$
|
||||||
|
$$$$$$&qOJcB$$$$$$$$$$$$kLCO*%$$$$$$$$$$
|
||||||
|
$$$$@#wJLXzq$$$$$$$$$$$$qOQZwkW&@@$$$$$$
|
||||||
|
$$$$@ohwQXCJO$$$$$$$$$$qO0OOq*BWB8&$$$$$
|
||||||
|
$$$$$@Wpd0XUzzZM$$$@@dO00qko#%@$$$$$$$$$
|
||||||
|
$$$$$$$$%*pwppZqQpqbQpko&B@$$$$$$$$$$$$$
|
||||||
|
$$$$$$$$$$$$$@BB$$@$$$$$$$$hmhaZ$kmC$$$$
|
||||||
|
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
''';
|
||||||
|
|
||||||
|
List<String> _buildFrames() {
|
||||||
|
final all = const LineSplitter().convert(_art);
|
||||||
|
var start = 0;
|
||||||
|
while (start < all.length && all[start].trim().isEmpty) {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
final lines = all.sublist(start);
|
||||||
|
|
||||||
|
var width = 0;
|
||||||
|
for (final line in lines) {
|
||||||
|
if (line.length > width) width = line.length;
|
||||||
|
}
|
||||||
|
final padLine = ''.padRight(width, '\$');
|
||||||
|
|
||||||
|
final frames = <String>[];
|
||||||
|
final buffer = <String>[];
|
||||||
|
for (final line in lines) {
|
||||||
|
buffer.add(line.isEmpty ? padLine : line.padRight(width, '\$'));
|
||||||
|
if (line.contains(r'Z$kmC')) {
|
||||||
|
frames.add(buffer.join('\n'));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> _frames = _buildFrames();
|
||||||
|
|
||||||
|
Future<void> runWatching(CommandContext ctx) async {
|
||||||
|
if (!ctx.isOnline()) {
|
||||||
|
ctx.notify('Нет соединения');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_frames.isEmpty) return;
|
||||||
|
|
||||||
|
final id = await ctx.postMessage(_frames.first);
|
||||||
|
if (id.isEmpty) return;
|
||||||
|
|
||||||
|
final completed = await playFrames(ctx, id, _frames, _frameDelay);
|
||||||
|
if (!completed) return;
|
||||||
|
|
||||||
|
await Future.delayed(_holdBeforeText);
|
||||||
|
if (!ctx.isActive()) return;
|
||||||
|
try {
|
||||||
|
await ctx.updateMessage(id, _endText);
|
||||||
|
} catch (_) {
|
||||||
|
if (ctx.isActive() && !ctx.isOnline()) ctx.notifyAntiFlood();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,8 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
late final AnimationController _attachAnim;
|
late final AnimationController _attachAnim;
|
||||||
late final AnimationController _commandAnim;
|
late final AnimationController _commandAnim;
|
||||||
bool _commandPanelVisible = false;
|
bool _commandPanelVisible = false;
|
||||||
|
final ValueNotifier<List<SlashCommand>> _commandMatches =
|
||||||
|
ValueNotifier(const []);
|
||||||
|
|
||||||
String _nextTempId() =>
|
String _nextTempId() =>
|
||||||
'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}';
|
'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}';
|
||||||
@@ -594,6 +596,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_attachAnim.dispose();
|
_attachAnim.dispose();
|
||||||
AppCommands.current.removeListener(_updateCommandPanel);
|
AppCommands.current.removeListener(_updateCommandPanel);
|
||||||
_commandAnim.dispose();
|
_commandAnim.dispose();
|
||||||
|
_commandMatches.dispose();
|
||||||
_messageController.dispose();
|
_messageController.dispose();
|
||||||
_messageFocusNode.dispose();
|
_messageFocusNode.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -610,9 +613,26 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
_updateCommandPanel();
|
_updateCommandPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<SlashCommand> _matchingCommands(String raw) {
|
||||||
|
if (!AppCommands.current.value) return const [];
|
||||||
|
final text = raw.trimLeft();
|
||||||
|
if (!text.startsWith('/')) return const [];
|
||||||
|
if (text.contains(RegExp(r'\s'))) return const [];
|
||||||
|
final query = text.toLowerCase();
|
||||||
|
for (final c in kSlashCommands) {
|
||||||
|
if (!c.hidden && c.name.toLowerCase() == query) return const [];
|
||||||
|
}
|
||||||
|
return kSlashCommands
|
||||||
|
.where((c) => !c.hidden && c.name.toLowerCase().startsWith(query))
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
void _updateCommandPanel() {
|
void _updateCommandPanel() {
|
||||||
final show =
|
final matches = _matchingCommands(_messageController.text);
|
||||||
AppCommands.current.value && _messageController.text.startsWith('/');
|
final show = matches.isNotEmpty;
|
||||||
|
if (show && !listEquals(_commandMatches.value, matches)) {
|
||||||
|
_commandMatches.value = matches;
|
||||||
|
}
|
||||||
if (show == _commandPanelVisible) return;
|
if (show == _commandPanelVisible) return;
|
||||||
_commandPanelVisible = show;
|
_commandPanelVisible = show;
|
||||||
if (show) {
|
if (show) {
|
||||||
@@ -658,7 +678,14 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
Widget _buildCommandPanel() {
|
Widget _buildCommandPanel() {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: _commandAnim,
|
animation: _commandAnim,
|
||||||
builder: (context, _) {
|
child: ValueListenableBuilder<List<SlashCommand>>(
|
||||||
|
valueListenable: _commandMatches,
|
||||||
|
builder: (context, matches, _) => CommandSuggestionsPanel(
|
||||||
|
commands: matches,
|
||||||
|
onSelected: _onCommandSelected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context, child) {
|
||||||
final t = _commandAnim.value;
|
final t = _commandAnim.value;
|
||||||
if (t == 0) return const SizedBox.shrink();
|
if (t == 0) return const SizedBox.shrink();
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -667,7 +694,7 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
ignoring: t < 1,
|
ignoring: t < 1,
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: t,
|
opacity: t,
|
||||||
child: CommandSuggestionsPanel(onSelected: _onCommandSelected),
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1497,12 +1524,19 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
final text = _messageController.text.trim();
|
final text = _messageController.text.trim();
|
||||||
if (text.isEmpty || _myId == 0) return;
|
if (text.isEmpty || _myId == 0) return;
|
||||||
|
|
||||||
if (AppCommands.current.value) {
|
if (AppCommands.current.value && text.startsWith('/')) {
|
||||||
final command = findSlashCommand(text);
|
final command = findSlashCommand(text);
|
||||||
if (command?.run != null) {
|
if (command == null) {
|
||||||
_messageController.clear();
|
_messageController.clear();
|
||||||
_hasText.value = false;
|
_hasText.value = false;
|
||||||
unawaited(command!.run!(_commandContext()));
|
showCustomNotification(context, 'ТАКОЙ КОМАНДЫ НЕТУ🚨🚨🚨');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command.run != null) {
|
||||||
|
final args = commandArgs(text);
|
||||||
|
_messageController.clear();
|
||||||
|
_hasText.value = false;
|
||||||
|
unawaited(command.run!(_commandContext(args)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1706,15 +1740,16 @@ class _ChatScreenState extends State<ChatScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandContext _commandContext() => CommandContext(
|
CommandContext _commandContext(String args) => CommandContext(
|
||||||
accountId: _myId,
|
accountId: _myId,
|
||||||
chatId: widget.chatId,
|
chatId: widget.chatId,
|
||||||
otherUserId: _resolveOtherId(),
|
otherUserId: _resolveOtherId(),
|
||||||
|
args: args,
|
||||||
messages: messagesModule,
|
messages: messagesModule,
|
||||||
isOnline: () => api.state == SessionState.online,
|
isOnline: () => api.state == SessionState.online,
|
||||||
isActive: () => mounted,
|
isActive: () => mounted,
|
||||||
notify: (message) {
|
notify: (message, {duration}) {
|
||||||
if (mounted) showCustomNotification(context, message);
|
if (mounted) showCustomNotification(context, message, duration: duration);
|
||||||
},
|
},
|
||||||
postMessage: _postCommandMessage,
|
postMessage: _postCommandMessage,
|
||||||
updateMessage: _updateCommandMessage,
|
updateMessage: _updateCommandMessage,
|
||||||
|
|||||||
@@ -1,23 +1,43 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
void showCustomNotification(BuildContext context, String message) {
|
const Duration _defaultNotificationDuration = Duration(milliseconds: 2600);
|
||||||
showCustomNotificationOnOverlay(Overlay.of(context), message);
|
|
||||||
|
void showCustomNotification(
|
||||||
|
BuildContext context,
|
||||||
|
String message, {
|
||||||
|
Duration? duration,
|
||||||
|
}) {
|
||||||
|
showCustomNotificationOnOverlay(
|
||||||
|
Overlay.of(context),
|
||||||
|
message,
|
||||||
|
duration: duration,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showCustomNotificationOnOverlay(OverlayState overlay, String message) {
|
void showCustomNotificationOnOverlay(
|
||||||
|
OverlayState overlay,
|
||||||
|
String message, {
|
||||||
|
Duration? duration,
|
||||||
|
}) {
|
||||||
|
final total = duration ?? _defaultNotificationDuration;
|
||||||
final entry = OverlayEntry(
|
final entry = OverlayEntry(
|
||||||
builder: (context) => CustomNotification(message: message),
|
builder: (context) => CustomNotification(message: message, duration: total),
|
||||||
);
|
);
|
||||||
overlay.insert(entry);
|
overlay.insert(entry);
|
||||||
Future.delayed(const Duration(milliseconds: 2600), () {
|
Future.delayed(total, () {
|
||||||
if (entry.mounted) entry.remove();
|
if (entry.mounted) entry.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomNotification extends StatefulWidget {
|
class CustomNotification extends StatefulWidget {
|
||||||
final String message;
|
final String message;
|
||||||
const CustomNotification({required this.message, super.key});
|
final Duration duration;
|
||||||
|
const CustomNotification({
|
||||||
|
required this.message,
|
||||||
|
this.duration = _defaultNotificationDuration,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CustomNotification> createState() => _CustomNotificationState();
|
State<CustomNotification> createState() => _CustomNotificationState();
|
||||||
@@ -38,7 +58,8 @@ class _CustomNotificationState extends State<CustomNotification>
|
|||||||
);
|
);
|
||||||
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
Future.delayed(const Duration(milliseconds: 2300), () {
|
final fadeOutDelay = widget.duration - const Duration(milliseconds: 300);
|
||||||
|
Future.delayed(fadeOutDelay > Duration.zero ? fadeOutDelay : Duration.zero, () {
|
||||||
if (mounted) _controller.reverse();
|
if (mounted) _controller.reverse();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,9 +217,12 @@ class KometAppState extends State<KometApp>
|
|||||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||||
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
||||||
StreamSubscription<IncomingCall>? _callIncomingSub;
|
StreamSubscription<IncomingCall>? _callIncomingSub;
|
||||||
|
StreamSubscription<String>? _serverErrorSub;
|
||||||
Timer? _scheduleTimer;
|
Timer? _scheduleTimer;
|
||||||
String? _lastVpnNotice;
|
String? _lastVpnNotice;
|
||||||
DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0);
|
DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
String? _lastServerError;
|
||||||
|
DateTime _lastServerErrorAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||||
widget.initialFpsOverlay,
|
widget.initialFpsOverlay,
|
||||||
);
|
);
|
||||||
@@ -325,6 +328,21 @@ class KometAppState extends State<KometApp>
|
|||||||
showCustomNotificationOnOverlay(overlay, msg);
|
showCustomNotificationOnOverlay(overlay, msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_serverErrorSub = api.errorStream.listen((msg) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (msg == _lastServerError &&
|
||||||
|
now.difference(_lastServerErrorAt).inSeconds < 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastServerError = msg;
|
||||||
|
_lastServerErrorAt = now;
|
||||||
|
|
||||||
|
final overlay = KometApp.navigatorKey.currentState?.overlay;
|
||||||
|
if (overlay != null) {
|
||||||
|
showCustomNotificationOnOverlay(overlay, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onIncomingCall(IncomingCall call) async {
|
Future<void> _onIncomingCall(IncomingCall call) async {
|
||||||
@@ -348,6 +366,7 @@ class KometAppState extends State<KometApp>
|
|||||||
_loginStatusSub?.cancel();
|
_loginStatusSub?.cancel();
|
||||||
_vpnBypassSub?.cancel();
|
_vpnBypassSub?.cancel();
|
||||||
_callIncomingSub?.cancel();
|
_callIncomingSub?.cancel();
|
||||||
|
_serverErrorSub?.cancel();
|
||||||
_scheduleTimer?.cancel();
|
_scheduleTimer?.cancel();
|
||||||
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
||||||
AppAmoled.current.removeListener(_onAmoledChanged);
|
AppAmoled.current.removeListener(_onAmoledChanged);
|
||||||
|
|||||||
Reference in New Issue
Block a user