Merge branch 'feature/FullStack' of https://github.com/KometTeam/Komet into feature/FullStack

# Conflicts:
#	lib/frontend/screens/chats/chat_screen.dart
#	lib/frontend/widgets/message_bubble.dart
This commit is contained in:
klockky
2026-06-22 17:32:33 +03:00
14 changed files with 1866 additions and 444 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();
}
}
File diff suppressed because it is too large Load Diff
+217
View File
@@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../core/utils/haptics.dart';
class ChatMenuItem {
final IconData icon;
final String label;
final VoidCallback? onTap;
final bool showChevron;
final bool dividerAfter;
final bool destructive;
const ChatMenuItem({
required this.icon,
required this.label,
this.onTap,
this.showChevron = false,
this.dividerAfter = false,
this.destructive = false,
});
}
void showChatMenu({
required BuildContext context,
required Rect anchorRect,
required List<ChatMenuItem> items,
}) {
final overlay = Overlay.of(context, rootOverlay: true);
late OverlayEntry entry;
entry = OverlayEntry(
builder: (ctx) => _ChatMenuLayer(
anchorRect: anchorRect,
items: items,
onDismiss: () {
if (entry.mounted) entry.remove();
},
),
);
overlay.insert(entry);
Haptics.medium();
}
class _ChatMenuLayer extends StatefulWidget {
final Rect anchorRect;
final List<ChatMenuItem> items;
final VoidCallback onDismiss;
const _ChatMenuLayer({
required this.anchorRect,
required this.items,
required this.onDismiss,
});
@override
State<_ChatMenuLayer> createState() => _ChatMenuLayerState();
}
class _ChatMenuLayerState extends State<_ChatMenuLayer>
with SingleTickerProviderStateMixin {
static const double _menuWidth = 290.0;
static const double _hMargin = 8.0;
static const double _gap = 6.0;
late final AnimationController _animController;
late final Animation<double> _animation;
bool _closing = false;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 220),
reverseDuration: const Duration(milliseconds: 160),
);
_animation = CurvedAnimation(
parent: _animController,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
_animController.forward();
}
@override
void dispose() {
_animController.dispose();
super.dispose();
}
Future<void> _close() async {
if (!mounted || _closing) return;
_closing = true;
try {
await _animController.reverse();
} catch (_) {}
if (!mounted) return;
widget.onDismiss();
}
void _onItemTap(ChatMenuItem item) {
Haptics.tap();
_close().then((_) => item.onTap?.call());
}
Rect _resolveRect(Size screen) {
double left = widget.anchorRect.right - _menuWidth;
left = left.clamp(_hMargin, screen.width - _menuWidth - _hMargin);
double top = widget.anchorRect.bottom + _gap;
return Rect.fromLTWH(left, top, _menuWidth, 0);
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final screen = MediaQuery.sizeOf(context);
final rect = _resolveRect(screen);
return AnimatedBuilder(
animation: _animation,
builder: (ctx, child) {
final t = _animation.value.clamp(0.0, 1.0);
final scale = 0.9 + 0.1 * t;
return Stack(
children: [
Positioned.fill(
child: GestureDetector(
onTap: _close,
behavior: HitTestBehavior.opaque,
child: const SizedBox.expand(),
),
),
Positioned(
left: rect.left,
top: rect.top,
width: rect.width,
child: Opacity(
opacity: t,
child: Transform.scale(
scale: scale,
alignment: Alignment.topRight,
child: child,
),
),
),
],
);
},
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
clipBehavior: Clip.antiAlias,
elevation: 12,
shadowColor: Colors.black.withValues(alpha: 0.45),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 6),
for (final item in widget.items) ...[
_ChatMenuRow(item: item, onTap: () => _onItemTap(item)),
if (item.dividerAfter)
Divider(
height: 1,
thickness: 1,
color: cs.onSurface.withValues(alpha: 0.07),
),
],
const SizedBox(height: 6),
],
),
),
);
}
}
class _ChatMenuRow extends StatelessWidget {
final ChatMenuItem item;
final VoidCallback onTap;
const _ChatMenuRow({required this.item, required this.onTap});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final fg = item.destructive ? cs.error : cs.onSurface;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 15),
child: Row(
children: [
Icon(item.icon, size: 24, weight: 350, color: fg),
const SizedBox(width: 18),
Expanded(
child: Text(
item.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: fg,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
if (item.showChevron)
Icon(
Symbols.chevron_right,
size: 22,
weight: 400,
color: cs.onSurface.withValues(alpha: 0.7),
),
],
),
),
);
}
}
+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();
});
}
@@ -10,7 +10,7 @@ import '../../core/config/app_message_actions_style.dart';
import '../../core/utils/haptics.dart';
import 'custom_notification.dart';
enum MessageActionsInteraction { dragAndRelease, click }
enum MessageActionsInteraction { dragAndRelease, click, tap }
class MessageActionsController extends ChangeNotifier {
Offset? pointer;
@@ -247,7 +247,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
final menuHeight = n * itemHeight + vPad * 2;
late double menuX;
late double menuY;
if (widget.interaction == MessageActionsInteraction.click) {
if (widget.interaction != MessageActionsInteraction.dragAndRelease) {
final spaceBelow = screenSize.height - widget.tapPoint.dy - 8;
_showBelow = spaceBelow >= menuHeight || widget.tapPoint.dy < menuHeight;
final rawY = _showBelow
@@ -446,7 +446,8 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
final cs = Theme.of(context).colorScheme;
final eased = Curves.easeOutCubic.transform(t);
final scale = 0.88 + 0.12 * eased;
final isClick = widget.interaction == MessageActionsInteraction.click;
final tapAnchored =
widget.interaction != MessageActionsInteraction.dragAndRelease;
return Positioned(
left: _menuRect.left,
top: _menuRect.top,
@@ -456,7 +457,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
opacity: eased,
child: Transform.scale(
scale: scale,
alignment: isClick
alignment: tapAnchored
? Alignment(-1.0, _showBelow ? -1.0 : 1.0)
: Alignment(
widget.isMe ? 1.0 : -1.0,
@@ -476,7 +477,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer>
_ListMenuItem(
action: _actions[i],
highlighted: _hoveredIndex == i,
onHoverChanged: isClick
onHoverChanged: tapAnchored
? (hovered) {
if (hovered) {
if (_hoveredIndex != i) {
+2 -5
View File
@@ -386,9 +386,7 @@ class MessageBubble extends StatelessWidget {
);
}
return GestureDetector(
onTap: Haptics.tap,
child: Padding(
return Padding(
padding: EdgeInsets.only(
left: 12,
right: 12,
@@ -453,8 +451,7 @@ class MessageBubble extends StatelessWidget {
],
),
),
),
);
);
}
Map? _resolveReactionInfo() {
+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);