feat(commands): еще немного команд

This commit is contained in:
torvalds
2026-06-22 16:37:56 +07:00
parent 7b73f1335a
commit 655cae0a95
11 changed files with 712 additions and 47 deletions
+1
View File
@@ -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;
+25 -27
View File
@@ -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)',
+26 -1
View File
@@ -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();
}
}
+88
View File
@@ -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();
}
+19 -1
View File
@@ -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);
}
+34 -1
View File
@@ -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);
+390
View File
@@ -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();
}
}
+45 -10
View File
@@ -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,
+28 -7
View File
@@ -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();
});
}
+19
View File
@@ -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);