feat(commands): еще немного команд
This commit is contained in:
@@ -57,6 +57,7 @@ class Api {
|
||||
_sessionExpiredController.stream;
|
||||
Stream<String> get handshakeSuccessStream =>
|
||||
_handshakeSuccessController.stream;
|
||||
Stream<String> get errorStream => _dispatcher.errorStream;
|
||||
SessionState get state => _sessionState;
|
||||
|
||||
StreamSubscription<Uint8List>? _dataSubscription;
|
||||
|
||||
@@ -190,8 +190,8 @@ class AppDatabase {
|
||||
onCreate: (db, _) => _createTables(db),
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
await db.execute(
|
||||
'ALTER TABLE profile ADD COLUMN is_active INTEGER NOT NULL DEFAULT 0',
|
||||
await _addColumnIfMissing(
|
||||
db, 'profile', 'is_active', 'INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
await db.execute('DROP TABLE IF EXISTS sync_state');
|
||||
await db.execute(_syncStateSchema);
|
||||
@@ -210,47 +210,33 @@ class AppDatabase {
|
||||
await db.execute(_messagesSchema);
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
await db.execute(
|
||||
'ALTER TABLE profile ADD COLUMN profile_options TEXT',
|
||||
);
|
||||
await _addColumnIfMissing(db, 'profile', 'profile_options', 'TEXT');
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
await db.execute(
|
||||
'ALTER TABLE chats_cache ADD COLUMN participants TEXT',
|
||||
);
|
||||
await _addColumnIfMissing(db, 'chats_cache', 'participants', 'TEXT');
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
await db.execute(
|
||||
'ALTER TABLE contacts ADD COLUMN options TEXT',
|
||||
);
|
||||
await db.execute(
|
||||
'ALTER TABLE chats_cache ADD COLUMN options TEXT',
|
||||
);
|
||||
await _addColumnIfMissing(db, 'contacts', 'options', 'TEXT');
|
||||
await _addColumnIfMissing(db, 'chats_cache', 'options', 'TEXT');
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
await db.execute(
|
||||
'ALTER TABLE chats_cache ADD COLUMN owner INTEGER',
|
||||
);
|
||||
await db.execute(
|
||||
'ALTER TABLE chats_cache ADD COLUMN admins TEXT',
|
||||
);
|
||||
await _addColumnIfMissing(db, 'chats_cache', 'owner', 'INTEGER');
|
||||
await _addColumnIfMissing(db, 'chats_cache', 'admins', 'TEXT');
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
await _createIndexes(db);
|
||||
}
|
||||
if (oldVersion < 12) {
|
||||
await db.execute(
|
||||
'ALTER TABLE chats_cache ADD COLUMN last_msg_status TEXT',
|
||||
);
|
||||
await _addColumnIfMissing(db, 'chats_cache', 'last_msg_status', 'TEXT');
|
||||
}
|
||||
if (oldVersion < 13) {
|
||||
await db.execute(
|
||||
'ALTER TABLE messages ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0',
|
||||
await _addColumnIfMissing(
|
||||
db, 'messages', 'deleted', 'INTEGER NOT NULL DEFAULT 0',
|
||||
);
|
||||
}
|
||||
if (oldVersion < 14) {
|
||||
await db.execute(
|
||||
'ALTER TABLE chats_cache ADD COLUMN in_list INTEGER NOT NULL DEFAULT 1',
|
||||
await _addColumnIfMissing(
|
||||
db, 'chats_cache', 'in_list', 'INTEGER NOT NULL DEFAULT 1',
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -281,6 +267,18 @@ class AppDatabase {
|
||||
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 {
|
||||
await db.execute(
|
||||
'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 _pushController = StreamController<Packet>.broadcast();
|
||||
final _errorController = StreamController<String>.broadcast();
|
||||
|
||||
/// Стрим всех входящих пушей (cmd == 1)
|
||||
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;
|
||||
|
||||
PacketDispatcher() {
|
||||
@@ -60,10 +72,22 @@ class PacketDispatcher {
|
||||
if (packet.cmd == CmdType.ok ||
|
||||
packet.cmd == CmdType.error ||
|
||||
packet.cmd == CmdType.notFound) {
|
||||
final payloadLog = packet.opcode == Opcode.login
|
||||
? '<скрыто: ответ login>'
|
||||
: payloadForLog(packet.payload);
|
||||
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);
|
||||
_requestTimestamps.remove(packet.seq);
|
||||
|
||||
@@ -130,5 +154,6 @@ class PacketDispatcher {
|
||||
_cleanupTimer?.cancel();
|
||||
clearPending();
|
||||
_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 'epsh_files_command.dart';
|
||||
import 'info_command.dart';
|
||||
import 'slash_command.dart';
|
||||
import 'watching_command.dart';
|
||||
|
||||
const List<SlashCommand> kSlashCommands = [
|
||||
SlashCommand('/test', '12345 test отображение'),
|
||||
SlashCommand('/info', 'сводка данных о человеке', run: runInfo),
|
||||
SlashCommand('/anim1', 'анимация текста', run: runAnim1),
|
||||
SlashCommand('/IAlwaysWatchingYou', '👁️', run: runWatching),
|
||||
SlashCommand(
|
||||
'/epshFiles',
|
||||
'цензура слов чёрными квадратами {шанс 1-100}',
|
||||
run: runEpshFiles,
|
||||
),
|
||||
SlashCommand(
|
||||
'/crush',
|
||||
'Тест устойчивости веб клиента макса',
|
||||
@@ -14,8 +24,16 @@ const List<SlashCommand> kSlashCommands = [
|
||||
];
|
||||
|
||||
SlashCommand? findSlashCommand(String text) {
|
||||
final name = text.trimLeft().split(RegExp(r'\s')).first.toLowerCase();
|
||||
for (final c in kSlashCommands) {
|
||||
if (c.name == text) return c;
|
||||
if (c.name.toLowerCase() == name) return c;
|
||||
}
|
||||
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';
|
||||
|
||||
const String kAntiFloodNotification =
|
||||
'Упс! МАХ сбросил соединение, кажется, тебе стоит немного помедлить с командами.';
|
||||
const Duration _antiFloodNotificationDuration = Duration(seconds: 3);
|
||||
|
||||
class CommandContext {
|
||||
final int accountId;
|
||||
final int chatId;
|
||||
final int? otherUserId;
|
||||
final String args;
|
||||
final MessagesModule messages;
|
||||
final bool Function() isOnline;
|
||||
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<void> Function(String id, String text) updateMessage;
|
||||
|
||||
@@ -15,6 +20,7 @@ class CommandContext {
|
||||
required this.accountId,
|
||||
required this.chatId,
|
||||
required this.otherUserId,
|
||||
required this.args,
|
||||
required this.messages,
|
||||
required this.isOnline,
|
||||
required this.isActive,
|
||||
@@ -22,6 +28,33 @@ class CommandContext {
|
||||
required this.postMessage,
|
||||
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);
|
||||
|
||||
@@ -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 _commandAnim;
|
||||
bool _commandPanelVisible = false;
|
||||
final ValueNotifier<List<SlashCommand>> _commandMatches =
|
||||
ValueNotifier(const []);
|
||||
|
||||
String _nextTempId() =>
|
||||
'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}';
|
||||
@@ -594,6 +596,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_attachAnim.dispose();
|
||||
AppCommands.current.removeListener(_updateCommandPanel);
|
||||
_commandAnim.dispose();
|
||||
_commandMatches.dispose();
|
||||
_messageController.dispose();
|
||||
_messageFocusNode.dispose();
|
||||
_scrollController.dispose();
|
||||
@@ -610,9 +613,26 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_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() {
|
||||
final show =
|
||||
AppCommands.current.value && _messageController.text.startsWith('/');
|
||||
final matches = _matchingCommands(_messageController.text);
|
||||
final show = matches.isNotEmpty;
|
||||
if (show && !listEquals(_commandMatches.value, matches)) {
|
||||
_commandMatches.value = matches;
|
||||
}
|
||||
if (show == _commandPanelVisible) return;
|
||||
_commandPanelVisible = show;
|
||||
if (show) {
|
||||
@@ -658,7 +678,14 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
Widget _buildCommandPanel() {
|
||||
return AnimatedBuilder(
|
||||
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;
|
||||
if (t == 0) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
@@ -667,7 +694,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
ignoring: t < 1,
|
||||
child: Opacity(
|
||||
opacity: t,
|
||||
child: CommandSuggestionsPanel(onSelected: _onCommandSelected),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1497,12 +1524,19 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final text = _messageController.text.trim();
|
||||
if (text.isEmpty || _myId == 0) return;
|
||||
|
||||
if (AppCommands.current.value) {
|
||||
if (AppCommands.current.value && text.startsWith('/')) {
|
||||
final command = findSlashCommand(text);
|
||||
if (command?.run != null) {
|
||||
if (command == null) {
|
||||
_messageController.clear();
|
||||
_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;
|
||||
}
|
||||
}
|
||||
@@ -1706,15 +1740,16 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
CommandContext _commandContext() => CommandContext(
|
||||
CommandContext _commandContext(String args) => CommandContext(
|
||||
accountId: _myId,
|
||||
chatId: widget.chatId,
|
||||
otherUserId: _resolveOtherId(),
|
||||
args: args,
|
||||
messages: messagesModule,
|
||||
isOnline: () => api.state == SessionState.online,
|
||||
isActive: () => mounted,
|
||||
notify: (message) {
|
||||
if (mounted) showCustomNotification(context, message);
|
||||
notify: (message, {duration}) {
|
||||
if (mounted) showCustomNotification(context, message, duration: duration);
|
||||
},
|
||||
postMessage: _postCommandMessage,
|
||||
updateMessage: _updateCommandMessage,
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
void showCustomNotification(BuildContext context, String message) {
|
||||
showCustomNotificationOnOverlay(Overlay.of(context), message);
|
||||
const Duration _defaultNotificationDuration = Duration(milliseconds: 2600);
|
||||
|
||||
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(
|
||||
builder: (context) => CustomNotification(message: message),
|
||||
builder: (context) => CustomNotification(message: message, duration: total),
|
||||
);
|
||||
overlay.insert(entry);
|
||||
Future.delayed(const Duration(milliseconds: 2600), () {
|
||||
Future.delayed(total, () {
|
||||
if (entry.mounted) entry.remove();
|
||||
});
|
||||
}
|
||||
|
||||
class CustomNotification extends StatefulWidget {
|
||||
final String message;
|
||||
const CustomNotification({required this.message, super.key});
|
||||
final Duration duration;
|
||||
const CustomNotification({
|
||||
required this.message,
|
||||
this.duration = _defaultNotificationDuration,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomNotification> createState() => _CustomNotificationState();
|
||||
@@ -38,7 +58,8 @@ class _CustomNotificationState extends State<CustomNotification>
|
||||
);
|
||||
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
||||
_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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -217,9 +217,12 @@ class KometAppState extends State<KometApp>
|
||||
StreamSubscription<LoginStatus>? _loginStatusSub;
|
||||
StreamSubscription<VpnBypassResult>? _vpnBypassSub;
|
||||
StreamSubscription<IncomingCall>? _callIncomingSub;
|
||||
StreamSubscription<String>? _serverErrorSub;
|
||||
Timer? _scheduleTimer;
|
||||
String? _lastVpnNotice;
|
||||
DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
String? _lastServerError;
|
||||
DateTime _lastServerErrorAt = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
late final ValueNotifier<bool> fpsOverlayEnabled = ValueNotifier(
|
||||
widget.initialFpsOverlay,
|
||||
);
|
||||
@@ -325,6 +328,21 @@ class KometAppState extends State<KometApp>
|
||||
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 {
|
||||
@@ -348,6 +366,7 @@ class KometAppState extends State<KometApp>
|
||||
_loginStatusSub?.cancel();
|
||||
_vpnBypassSub?.cancel();
|
||||
_callIncomingSub?.cancel();
|
||||
_serverErrorSub?.cancel();
|
||||
_scheduleTimer?.cancel();
|
||||
AppThemeModeConfig.current.removeListener(_onThemeModeChanged);
|
||||
AppAmoled.current.removeListener(_onAmoledChanged);
|
||||
|
||||
Reference in New Issue
Block a user