diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 35db697..7e115ef 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -57,6 +57,7 @@ class Api { _sessionExpiredController.stream; Stream get handshakeSuccessStream => _handshakeSuccessController.stream; + Stream get errorStream => _dispatcher.errorStream; SessionState get state => _sessionState; StreamSubscription? _dataSubscription; diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index f604cbf..5d3efa7 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -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 _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 _createIndexes(Database db) async { await db.execute( 'CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(account_id, chat_id, time DESC)', diff --git a/lib/core/transport/dispatcher.dart b/lib/core/transport/dispatcher.dart index 8562283..0a5f268 100644 --- a/lib/core/transport/dispatcher.dart +++ b/lib/core/transport/dispatcher.dart @@ -17,10 +17,22 @@ class PacketDispatcher { final Map _pushHandlers = {}; final _pushController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); /// Стрим всех входящих пушей (cmd == 1) Stream get pushStream => _pushController.stream; + Stream 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(); } } diff --git a/lib/frontend/commands/anim_command.dart b/lib/frontend/commands/anim_command.dart new file mode 100644 index 0000000..321f3f3 --- /dev/null +++ b/lib/frontend/commands/anim_command.dart @@ -0,0 +1,88 @@ +import 'dart:math'; + +import 'slash_command.dart'; + +const double _defaultCooldownSec = 0.15; +const int _minLength = 3; +const List _fillChars = ['#', '@', '%', '&', '*']; + +final Random _rng = Random(); + +String _fill() => _fillChars[_rng.nextInt(_fillChars.length)]; + +Future 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 _buildFrames(List chars) { + final n = chars.length; + final noise = List.generate(n, (_) => _fill()); + final frames = [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(); +} diff --git a/lib/frontend/commands/command_registry.dart b/lib/frontend/commands/command_registry.dart index 2d16898..ca54a7d 100644 --- a/lib/frontend/commands/command_registry.dart +++ b/lib/frontend/commands/command_registry.dart @@ -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 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 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(); +} diff --git a/lib/frontend/commands/epsh_files_command.dart b/lib/frontend/commands/epsh_files_command.dart new file mode 100644 index 0000000..d8e7fa5 --- /dev/null +++ b/lib/frontend/commands/epsh_files_command.dart @@ -0,0 +1,37 @@ +import 'dart:math'; + +import 'slash_command.dart'; + +const int _defaultChance = 65; +const String _square = '⬛'; + +final Random _rng = Random(); + +Future 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); +} diff --git a/lib/frontend/commands/slash_command.dart b/lib/frontend/commands/slash_command.dart index 5c86436..d6b7201 100644 --- a/lib/frontend/commands/slash_command.dart +++ b/lib/frontend/commands/slash_command.dart @@ -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 Function(String text) postMessage; final Future 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 playFrames( + CommandContext ctx, + String id, + List 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 Function(CommandContext ctx); diff --git a/lib/frontend/commands/watching_command.dart b/lib/frontend/commands/watching_command.dart new file mode 100644 index 0000000..433c225 --- /dev/null +++ b/lib/frontend/commands/watching_command.dart @@ -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 _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 = []; + final buffer = []; + 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 _frames = _buildFrames(); + +Future 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(); + } +} diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index a463aa0..ac37459 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -164,6 +164,8 @@ class _ChatScreenState extends State late final AnimationController _attachAnim; late final AnimationController _commandAnim; bool _commandPanelVisible = false; + final ValueNotifier> _commandMatches = + ValueNotifier(const []); String _nextTempId() => 'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}'; @@ -594,6 +596,7 @@ class _ChatScreenState extends State _attachAnim.dispose(); AppCommands.current.removeListener(_updateCommandPanel); _commandAnim.dispose(); + _commandMatches.dispose(); _messageController.dispose(); _messageFocusNode.dispose(); _scrollController.dispose(); @@ -610,9 +613,26 @@ class _ChatScreenState extends State _updateCommandPanel(); } + List _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 Widget _buildCommandPanel() { return AnimatedBuilder( animation: _commandAnim, - builder: (context, _) { + child: ValueListenableBuilder>( + 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 ignoring: t < 1, child: Opacity( opacity: t, - child: CommandSuggestionsPanel(onSelected: _onCommandSelected), + child: child, ), ), ); @@ -1497,12 +1524,19 @@ class _ChatScreenState extends State 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 } } - 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, diff --git a/lib/frontend/widgets/custom_notification.dart b/lib/frontend/widgets/custom_notification.dart index 20c16a8..786d3ec 100644 --- a/lib/frontend/widgets/custom_notification.dart +++ b/lib/frontend/widgets/custom_notification.dart @@ -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 createState() => _CustomNotificationState(); @@ -38,7 +58,8 @@ class _CustomNotificationState extends State ); _opacity = Tween(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(); }); } diff --git a/lib/main.dart b/lib/main.dart index c1705cc..ea561f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -217,9 +217,12 @@ class KometAppState extends State StreamSubscription? _loginStatusSub; StreamSubscription? _vpnBypassSub; StreamSubscription? _callIncomingSub; + StreamSubscription? _serverErrorSub; Timer? _scheduleTimer; String? _lastVpnNotice; DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0); + String? _lastServerError; + DateTime _lastServerErrorAt = DateTime.fromMillisecondsSinceEpoch(0); late final ValueNotifier fpsOverlayEnabled = ValueNotifier( widget.initialFpsOverlay, ); @@ -325,6 +328,21 @@ class KometAppState extends State 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 _onIncomingCall(IncomingCall call) async { @@ -348,6 +366,7 @@ class KometAppState extends State _loginStatusSub?.cancel(); _vpnBypassSub?.cancel(); _callIncomingSub?.cancel(); + _serverErrorSub?.cancel(); _scheduleTimer?.cancel(); AppThemeModeConfig.current.removeListener(_onThemeModeChanged); AppAmoled.current.removeListener(_onAmoledChanged);