feat: / команды
This commit is contained in:
@@ -649,16 +649,19 @@ class MessagesModule {
|
||||
int chatId,
|
||||
String messageId, {
|
||||
required String text,
|
||||
List<Map<String, dynamic>> elements = const [],
|
||||
bool sendAttachments = false,
|
||||
}) async {
|
||||
final id = int.tryParse(messageId);
|
||||
if (id == null) return false;
|
||||
|
||||
final payload = {
|
||||
final payload = <String, dynamic>{
|
||||
'messageId': id,
|
||||
'chatId': chatId,
|
||||
'elements': <dynamic>[],
|
||||
'elements': elements,
|
||||
'text': text,
|
||||
};
|
||||
if (sendAttachments) payload['attachments'] = const <dynamic>[];
|
||||
|
||||
final response = await _api.sendRequest(Opcode.msgEdit, payload);
|
||||
return response.isOk;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AppCommands {
|
||||
static const prefKey = 'dev_commands';
|
||||
static const bool defaultValue = false;
|
||||
|
||||
static final ValueNotifier<bool> current = ValueNotifier(defaultValue);
|
||||
|
||||
static Future<bool> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(prefKey) ?? defaultValue;
|
||||
}
|
||||
|
||||
static Future<void> save(bool value) async {
|
||||
current.value = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(prefKey, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'crush_command.dart';
|
||||
import 'info_command.dart';
|
||||
import 'slash_command.dart';
|
||||
|
||||
const List<SlashCommand> kSlashCommands = [
|
||||
SlashCommand('/test', '12345 test отображение'),
|
||||
SlashCommand('/info', 'сводка данных о человеке', run: runInfo),
|
||||
SlashCommand(
|
||||
'/crush',
|
||||
'Тест устойчивости веб клиента макса',
|
||||
run: runCrush,
|
||||
hidden: true,
|
||||
),
|
||||
];
|
||||
|
||||
SlashCommand? findSlashCommand(String text) {
|
||||
for (final c in kSlashCommands) {
|
||||
if (c.name == text) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'slash_command.dart';
|
||||
|
||||
const int _cycles = 40;
|
||||
const Duration _delay = Duration(milliseconds: 400);
|
||||
|
||||
const String _text =
|
||||
'burmaldaburmaldaburmaldaburmaldaburmaldaburmaldaburmaldaburmalda';
|
||||
|
||||
const List<Map<String, dynamic>> _elements = [
|
||||
{'type': 'STRONG', 'from': 0, 'length': 1},
|
||||
{'type': 'EMPHASIZED', 'from': 1, 'length': 1},
|
||||
{'type': 'UNDERLINE', 'from': 2, 'length': 1},
|
||||
{'type': 'STRIKETHROUGH', 'from': 3, 'length': 1},
|
||||
{'type': 'STRONG', 'from': 4, 'length': 2},
|
||||
{'type': 'STRONG', 'from': 6, 'length': 1},
|
||||
{'type': 'EMPHASIZED', 'from': 6, 'length': 1},
|
||||
{'type': 'STRONG', 'from': 7, 'length': 1},
|
||||
{'type': 'EMPHASIZED', 'from': 7, 'length': 1},
|
||||
{'type': 'STRIKETHROUGH', 'from': 7, 'length': 1},
|
||||
{'type': 'STRONG', 'from': 8, 'length': 1},
|
||||
{'type': 'EMPHASIZED', 'from': 8, 'length': 1},
|
||||
{'type': 'UNDERLINE', 'from': 8, 'length': 1},
|
||||
{'type': 'STRIKETHROUGH', 'from': 8, 'length': 1},
|
||||
{'type': 'STRONG', 'from': 9, 'length': 1},
|
||||
{'type': 'EMPHASIZED', 'from': 9, 'length': 1},
|
||||
{'type': 'STRIKETHROUGH', 'from': 9, 'length': 1},
|
||||
{'type': 'STRIKETHROUGH', 'from': 13, 'length': 3},
|
||||
{'type': 'STRONG', 'from': 16, 'length': 2},
|
||||
{'type': 'STRIKETHROUGH', 'from': 16, 'length': 2},
|
||||
{'type': 'STRONG', 'from': 18, 'length': 5},
|
||||
{'type': 'EMPHASIZED', 'from': 18, 'length': 5},
|
||||
{'type': 'STRIKETHROUGH', 'from': 18, 'length': 5},
|
||||
{'type': 'STRONG', 'from': 23, 'length': 2},
|
||||
{'type': 'EMPHASIZED', 'from': 23, 'length': 2},
|
||||
{'type': 'UNDERLINE', 'from': 23, 'length': 2},
|
||||
{'type': 'STRIKETHROUGH', 'from': 23, 'length': 2},
|
||||
{'type': 'STRONG', 'from': 25, 'length': 1},
|
||||
{'type': 'EMPHASIZED', 'from': 25, 'length': 1},
|
||||
{'type': 'UNDERLINE', 'from': 25, 'length': 1},
|
||||
{'type': 'STRONG', 'from': 26, 'length': 2},
|
||||
{'type': 'UNDERLINE', 'from': 26, 'length': 2},
|
||||
{'type': 'STRONG', 'from': 28, 'length': 3},
|
||||
{'type': 'EMPHASIZED', 'from': 28, 'length': 3},
|
||||
{'type': 'UNDERLINE', 'from': 28, 'length': 3},
|
||||
{'type': 'EMPHASIZED', 'from': 31, 'length': 2},
|
||||
{'type': 'EMPHASIZED', 'from': 33, 'length': 2},
|
||||
{'type': 'UNDERLINE', 'from': 33, 'length': 2},
|
||||
{'type': 'STRONG', 'from': 35, 'length': 6},
|
||||
{'type': 'EMPHASIZED', 'from': 35, 'length': 6},
|
||||
{'type': 'UNDERLINE', 'from': 35, 'length': 6},
|
||||
{'type': 'STRONG', 'from': 41, 'length': 2},
|
||||
{'type': 'UNDERLINE', 'from': 41, 'length': 2},
|
||||
{'type': 'STRONG', 'from': 43, 'length': 8},
|
||||
{'type': 'UNDERLINE', 'from': 43, 'length': 8},
|
||||
{'type': 'STRIKETHROUGH', 'from': 43, 'length': 8},
|
||||
{'type': 'UNDERLINE', 'from': 51, 'length': 7},
|
||||
{'type': 'STRIKETHROUGH', 'from': 51, 'length': 7},
|
||||
{'type': 'STRIKETHROUGH', 'from': 58, 'length': 5},
|
||||
{'type': 'MONOSPACED', 'from': 0, 'length': 4},
|
||||
{'type': 'MONOSPACED', 'from': 9, 'length': 4},
|
||||
{'type': 'MONOSPACED', 'from': 16, 'length': 7},
|
||||
{'type': 'MONOSPACED', 'from': 25, 'length': 6},
|
||||
{'type': 'MONOSPACED', 'from': 33, 'length': 8},
|
||||
{'type': 'MONOSPACED', 'from': 43, 'length': 8},
|
||||
{'type': 'MONOSPACED', 'from': 51, 'length': 7},
|
||||
{'type': 'MONOSPACED', 'from': 58, 'length': 5},
|
||||
{
|
||||
'type': 'LINK',
|
||||
'from': 0,
|
||||
'length': 8,
|
||||
'attributes': {'url': 'https://vk.com'},
|
||||
},
|
||||
{
|
||||
'type': 'LINK',
|
||||
'from': 24,
|
||||
'length': 16,
|
||||
'attributes': {'url': 'https://max.ru'},
|
||||
},
|
||||
{
|
||||
'type': 'LINK',
|
||||
'from': 48,
|
||||
'length': 16,
|
||||
'attributes': {'url': 'https://web.max.ru'},
|
||||
},
|
||||
];
|
||||
|
||||
Future<void> runCrush(CommandContext ctx) async {
|
||||
if (!ctx.isOnline()) {
|
||||
ctx.notify('Нет соединения');
|
||||
return;
|
||||
}
|
||||
ctx.notify('Crush: запуск ($_cycles циклов)');
|
||||
for (var i = 0; i < _cycles; i++) {
|
||||
if (!ctx.isActive()) return;
|
||||
unawaited(_cycle(ctx));
|
||||
await Future.delayed(_delay);
|
||||
}
|
||||
if (ctx.isActive()) ctx.notify('Crush: завершено');
|
||||
}
|
||||
|
||||
Future<void> _cycle(CommandContext ctx) async {
|
||||
try {
|
||||
final id = await ctx.messages.sendMessage(ctx.accountId, ctx.chatId, _text);
|
||||
if (id.isEmpty) return;
|
||||
await Future.wait([
|
||||
ctx.messages.editMessage(
|
||||
ctx.chatId,
|
||||
id,
|
||||
text: _text,
|
||||
elements: _elements,
|
||||
sendAttachments: true,
|
||||
),
|
||||
ctx.messages.deleteMessages(ctx.chatId, [id], forEveryone: true),
|
||||
]);
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import '../../core/cache/info_cache.dart';
|
||||
import '../../core/utils/format.dart';
|
||||
import 'slash_command.dart';
|
||||
|
||||
Future<void> runInfo(CommandContext ctx) async {
|
||||
final targetId = ctx.otherUserId;
|
||||
if (targetId == null) {
|
||||
ctx.notify('Команда доступна только в диалоге');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.isOnline()) {
|
||||
ctx.notify('Нет соединения');
|
||||
return;
|
||||
}
|
||||
|
||||
final messageId = await ctx.postMessage('сбор данных...');
|
||||
if (messageId.isEmpty) return;
|
||||
|
||||
final contact = await ContactInfoFetch.get(targetId, forceRefresh: true);
|
||||
if (!ctx.isActive()) return;
|
||||
|
||||
await ctx.updateMessage(
|
||||
messageId,
|
||||
contact == null ? 'Данные не получены' : _summary(contact, targetId),
|
||||
);
|
||||
}
|
||||
|
||||
String _summary(Map<String, dynamic> c, int targetId) {
|
||||
final flags = (c['options'] as List?)?.whereType<String>().toList() ?? const [];
|
||||
final region = (c['country'] as String?)?.trim();
|
||||
|
||||
return 'Никнейм: ${_nick(c)}\n'
|
||||
'Дата регистрации: ${_date(c['registrationTime'])}\n'
|
||||
'Дата последнего изменения профиля: ${_date(c['updateTime'])}\n'
|
||||
'id: ${c['id'] ?? targetId}\n'
|
||||
'Регион: ${region == null || region.isEmpty ? '—' : region}\n'
|
||||
'Флаги: ${flags.isEmpty ? '—' : flags.join(', ')}\n'
|
||||
'ip: not fetched';
|
||||
}
|
||||
|
||||
String _nick(Map<String, dynamic> c) {
|
||||
final names = c['names'];
|
||||
if (names is List && names.isNotEmpty && names.first is Map) {
|
||||
final n = names.first as Map;
|
||||
final name = (n['name'] as String?) ??
|
||||
'${n['firstName'] ?? ''} ${n['lastName'] ?? ''}'.trim();
|
||||
if (name.isNotEmpty) return name;
|
||||
}
|
||||
return '—';
|
||||
}
|
||||
|
||||
String _date(dynamic ms) {
|
||||
if (ms is! int || ms <= 0) return '—';
|
||||
return formatDateTimeWords(DateTime.fromMillisecondsSinceEpoch(ms));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import '../../backend/modules/messages.dart';
|
||||
|
||||
class CommandContext {
|
||||
final int accountId;
|
||||
final int chatId;
|
||||
final int? otherUserId;
|
||||
final MessagesModule messages;
|
||||
final bool Function() isOnline;
|
||||
final bool Function() isActive;
|
||||
final void Function(String message) notify;
|
||||
final Future<String> Function(String text) postMessage;
|
||||
final Future<void> Function(String id, String text) updateMessage;
|
||||
|
||||
const CommandContext({
|
||||
required this.accountId,
|
||||
required this.chatId,
|
||||
required this.otherUserId,
|
||||
required this.messages,
|
||||
required this.isOnline,
|
||||
required this.isActive,
|
||||
required this.notify,
|
||||
required this.postMessage,
|
||||
required this.updateMessage,
|
||||
});
|
||||
}
|
||||
|
||||
typedef CommandRunner = Future<void> Function(CommandContext ctx);
|
||||
|
||||
class SlashCommand {
|
||||
final String name;
|
||||
final String description;
|
||||
final CommandRunner? run;
|
||||
final bool hidden;
|
||||
|
||||
const SlashCommand(
|
||||
this.name,
|
||||
this.description, {
|
||||
this.run,
|
||||
this.hidden = false,
|
||||
});
|
||||
}
|
||||
@@ -34,10 +34,14 @@ import '../../../core/config/app_cache_extent.dart';
|
||||
import '../../../core/config/app_message_actions_style.dart';
|
||||
import '../../../core/config/app_swipe_back_desktop.dart';
|
||||
import '../../../core/config/app_pranks.dart';
|
||||
import '../../../core/config/app_commands.dart';
|
||||
import '../../../core/config/app_visual_style.dart';
|
||||
import '../../../core/config/komet_settings.dart';
|
||||
import '../../../models/attachment.dart';
|
||||
import '../../commands/command_registry.dart';
|
||||
import '../../commands/slash_command.dart';
|
||||
import '../../widgets/glossy_pill.dart';
|
||||
import '../../widgets/command_suggestions_panel.dart';
|
||||
import '../../widgets/online_dot.dart';
|
||||
import '../../widgets/connection_status.dart';
|
||||
import '../../widgets/message_bubble.dart';
|
||||
@@ -158,6 +162,8 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
|
||||
int _tempIdCounter = 0;
|
||||
late final AnimationController _attachAnim;
|
||||
late final AnimationController _commandAnim;
|
||||
bool _commandPanelVisible = false;
|
||||
|
||||
String _nextTempId() =>
|
||||
'temp_${++_tempIdCounter}_${DateTime.now().microsecondsSinceEpoch}';
|
||||
@@ -201,6 +207,11 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
reverseDuration: const Duration(milliseconds: 240),
|
||||
);
|
||||
_showAttachmentPanel.addListener(_onAttachPanelToggle);
|
||||
_commandAnim = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
AppCommands.current.addListener(_updateCommandPanel);
|
||||
_pushSub = api.pushStream
|
||||
.where(
|
||||
(p) =>
|
||||
@@ -581,6 +592,8 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
_finishPrankReveal();
|
||||
_uploadStatus.dispose();
|
||||
_attachAnim.dispose();
|
||||
AppCommands.current.removeListener(_updateCommandPanel);
|
||||
_commandAnim.dispose();
|
||||
_messageController.dispose();
|
||||
_messageFocusNode.dispose();
|
||||
_scrollController.dispose();
|
||||
@@ -594,6 +607,28 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
if (newHasText != _hasText.value) {
|
||||
_hasText.value = newHasText;
|
||||
}
|
||||
_updateCommandPanel();
|
||||
}
|
||||
|
||||
void _updateCommandPanel() {
|
||||
final show =
|
||||
AppCommands.current.value && _messageController.text.startsWith('/');
|
||||
if (show == _commandPanelVisible) return;
|
||||
_commandPanelVisible = show;
|
||||
if (show) {
|
||||
_commandAnim.forward();
|
||||
} else {
|
||||
_commandAnim.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void _onCommandSelected(SlashCommand c) {
|
||||
final text = '${c.name} ';
|
||||
_messageController.value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
);
|
||||
_messageFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
void _restoreDraft() {
|
||||
@@ -620,6 +655,26 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCommandPanel() {
|
||||
return AnimatedBuilder(
|
||||
animation: _commandAnim,
|
||||
builder: (context, _) {
|
||||
final t = _commandAnim.value;
|
||||
if (t == 0) return const SizedBox.shrink();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
child: IgnorePointer(
|
||||
ignoring: t < 1,
|
||||
child: Opacity(
|
||||
opacity: t,
|
||||
child: CommandSuggestionsPanel(onSelected: _onCommandSelected),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int _computeOtherReadTime() {
|
||||
final c = chat;
|
||||
if (c == null) return 0;
|
||||
@@ -1442,6 +1497,16 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final text = _messageController.text.trim();
|
||||
if (text.isEmpty || _myId == 0) return;
|
||||
|
||||
if (AppCommands.current.value) {
|
||||
final command = findSlashCommand(text);
|
||||
if (command?.run != null) {
|
||||
_messageController.clear();
|
||||
_hasText.value = false;
|
||||
unawaited(command!.run!(_commandContext()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final tempId = _nextTempId();
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final online = api.state == SessionState.online;
|
||||
@@ -1550,6 +1615,111 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
int? _resolveOtherId() {
|
||||
if (widget.chatType != 'DIALOG' || _myId == 0) return null;
|
||||
final id = widget.chatId ^ _myId;
|
||||
return id > 0 ? id : null;
|
||||
}
|
||||
|
||||
CachedMessage _replaceMessage(
|
||||
int index, {
|
||||
String? id,
|
||||
String? text,
|
||||
String? status,
|
||||
}) {
|
||||
final old = _messages[index];
|
||||
final updated = CachedMessage(
|
||||
id: id ?? old.id,
|
||||
accountId: old.accountId,
|
||||
chatId: old.chatId,
|
||||
senderId: old.senderId,
|
||||
text: text ?? old.text,
|
||||
time: old.time,
|
||||
status: status ?? old.status,
|
||||
payload: old.payload,
|
||||
attachments: old.attachments,
|
||||
isControl: old.isControl,
|
||||
);
|
||||
_messages[index] = updated;
|
||||
_bumpMessages();
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<String> _postCommandMessage(String text) async {
|
||||
if (!mounted || _myId == 0) return '';
|
||||
final tempId = _nextTempId();
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final online = api.state == SessionState.online;
|
||||
final composed = CachedMessage(
|
||||
id: tempId,
|
||||
accountId: _myId,
|
||||
chatId: widget.chatId,
|
||||
senderId: _myId,
|
||||
text: text,
|
||||
time: now,
|
||||
status: online ? 'sending' : 'pending',
|
||||
);
|
||||
_messages.add(composed);
|
||||
_bumpMessages();
|
||||
_scrollToBottom();
|
||||
unawaited(_persistOutgoing(composed));
|
||||
unawaited(ChatsModule.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: tempId,
|
||||
time: now,
|
||||
text: text,
|
||||
status: composed.status ?? 'sending',
|
||||
));
|
||||
if (!online) return tempId;
|
||||
try {
|
||||
final actualId = await messagesModule.sendMessage(_myId, widget.chatId, text);
|
||||
final realId = actualId.isNotEmpty ? actualId : tempId;
|
||||
final i = _messages.indexWhere((m) => m.id == tempId);
|
||||
if (i != -1) {
|
||||
final sent = _replaceMessage(i, id: realId, status: 'sent');
|
||||
unawaited(_persistOutgoing(sent, removeId: tempId));
|
||||
unawaited(ChatsModule.applyOutgoing(
|
||||
_myId,
|
||||
widget.chatId,
|
||||
messageId: realId,
|
||||
time: now,
|
||||
text: text,
|
||||
status: 'sent',
|
||||
));
|
||||
}
|
||||
return realId;
|
||||
} catch (_) {
|
||||
return tempId;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateCommandMessage(String id, String text) async {
|
||||
if (id.isEmpty) return;
|
||||
final i = _messages.indexWhere((m) => m.id == id);
|
||||
if (i != -1) {
|
||||
final edited = _replaceMessage(i, text: text, status: 'EDITED');
|
||||
unawaited(_persistOutgoing(edited));
|
||||
}
|
||||
if (!id.startsWith('temp_')) {
|
||||
await messagesModule.editMessage(widget.chatId, id, text: text);
|
||||
}
|
||||
}
|
||||
|
||||
CommandContext _commandContext() => CommandContext(
|
||||
accountId: _myId,
|
||||
chatId: widget.chatId,
|
||||
otherUserId: _resolveOtherId(),
|
||||
messages: messagesModule,
|
||||
isOnline: () => api.state == SessionState.online,
|
||||
isActive: () => mounted,
|
||||
notify: (message) {
|
||||
if (mounted) showCustomNotification(context, message);
|
||||
},
|
||||
postMessage: _postCommandMessage,
|
||||
updateMessage: _updateCommandMessage,
|
||||
);
|
||||
|
||||
Future<void> _scheduleMessage() async {
|
||||
final text = _messageController.text.trim();
|
||||
if (text.isEmpty || _myId == 0) return;
|
||||
@@ -2066,9 +2236,21 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _isLoading && _messages.isEmpty
|
||||
? _buildShimmerLoading()
|
||||
: _buildMessagesList(),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: _isLoading && _messages.isEmpty
|
||||
? _buildShimmerLoading()
|
||||
: _buildMessagesList(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: _buildCommandPanel(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: _attachAnim,
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../../backend/modules/chats.dart';
|
||||
import '../../../core/config/app_swipe_back_desktop.dart';
|
||||
import '../../../core/config/app_pranks.dart';
|
||||
import '../../../core/config/app_stories.dart';
|
||||
import '../../../core/config/app_commands.dart';
|
||||
import '../../../core/config/app_link_preview.dart';
|
||||
import '../../../core/config/app_digital_id_mode.dart';
|
||||
import '../../../core/config/app_media_cache.dart';
|
||||
@@ -786,6 +787,65 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: AppCommands.current,
|
||||
builder: (context, commandsOn, _) {
|
||||
return GlossyPill(
|
||||
color: cs.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
depth: 6,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 17,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.terminal,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 22,
|
||||
weight: 400,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Команды',
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Панель команд по вводу «/» в строке сообщения',
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: commandsOn,
|
||||
onChanged: (v) {
|
||||
AppCommands.save(v);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../commands/command_registry.dart';
|
||||
import '../commands/slash_command.dart';
|
||||
|
||||
class CommandSuggestionsPanel extends StatelessWidget {
|
||||
final List<SlashCommand> commands;
|
||||
final double maxHeight;
|
||||
final ValueChanged<SlashCommand>? onSelected;
|
||||
|
||||
const CommandSuggestionsPanel({
|
||||
super.key,
|
||||
this.commands = kSlashCommands,
|
||||
this.maxHeight = 220,
|
||||
this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final visible = commands.where((c) => !c.hidden).toList(growable: false);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
itemCount: visible.length,
|
||||
separatorBuilder: (_, _) => Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
indent: 14,
|
||||
endIndent: 14,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.18),
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
final c = visible[i];
|
||||
return InkWell(
|
||||
onTap: onSelected == null ? null : () => onSelected!(c),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 84,
|
||||
child: Text(
|
||||
c.name,
|
||||
style: TextStyle(
|
||||
color: cs.primary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
c.description,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import 'core/config/app_message_actions_style.dart';
|
||||
import 'core/config/app_swipe_back_desktop.dart';
|
||||
import 'core/config/app_pranks.dart';
|
||||
import 'core/config/app_stories.dart';
|
||||
import 'core/config/app_commands.dart';
|
||||
import 'core/config/app_link_preview.dart';
|
||||
import 'core/config/app_media_cache.dart';
|
||||
import 'core/config/app_pill_gradient.dart';
|
||||
@@ -112,6 +113,7 @@ void main() async {
|
||||
final swipeBackFuture = AppSwipeBackDesktop.load();
|
||||
final pranksFuture = AppPranks.load();
|
||||
final storiesFuture = AppStories.load();
|
||||
final commandsFuture = AppCommands.load();
|
||||
final linkPreviewFuture = AppLinkPreview.load();
|
||||
final cacheLimitFuture = AppMediaCacheLimit.load();
|
||||
final digitalIdNativeFuture = AppDigitalIdNative.load();
|
||||
@@ -150,6 +152,7 @@ void main() async {
|
||||
AppSwipeBackDesktop.current.value = await swipeBackFuture;
|
||||
AppPranks.current.value = await pranksFuture;
|
||||
AppStories.current.value = await storiesFuture;
|
||||
AppCommands.current.value = await commandsFuture;
|
||||
AppLinkPreview.current.value = await linkPreviewFuture;
|
||||
AppMediaCacheLimit.current.value = await cacheLimitFuture;
|
||||
AppDigitalIdNative.current.value = await digitalIdNativeFuture;
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
"""mitmproxy addon: dump okcdn call signaling (ws2 WebSocket + HTTP) to a log."""
|
||||
import json
|
||||
import time
|
||||
|
||||
from mitmproxy import http, ctx
|
||||
|
||||
LOG = r"C:\Users\klockky\Komet\docs\ws2_capture.log"
|
||||
HOSTS = ("okcdn.ru", "videowebrtc")
|
||||
|
||||
|
||||
def _interesting(host: str) -> bool:
|
||||
return any(h in host for h in HOSTS)
|
||||
|
||||
|
||||
def _w(line: str) -> None:
|
||||
with open(LOG, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
def _fmt(content: bytes) -> str:
|
||||
try:
|
||||
text = content.decode("utf-8")
|
||||
try:
|
||||
return json.dumps(json.loads(text), ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
return text
|
||||
except Exception:
|
||||
return "HEX " + content.hex()
|
||||
|
||||
|
||||
def websocket_start(flow: http.HTTPFlow) -> None:
|
||||
if not _interesting(flow.request.pretty_host):
|
||||
return
|
||||
_w("=" * 70)
|
||||
_w(f"# WS OPEN {flow.request.pretty_host} {flow.request.path}")
|
||||
_w(f" headers: {dict(flow.request.headers)}")
|
||||
_w("=" * 70)
|
||||
|
||||
|
||||
def websocket_message(flow: http.HTTPFlow) -> None:
|
||||
if not _interesting(flow.request.pretty_host):
|
||||
return
|
||||
msg = flow.websocket.messages[-1]
|
||||
arrow = "TX (client->server)" if msg.from_client else "RX (server->client)"
|
||||
ts = time.strftime("%H:%M:%S")
|
||||
_w(f"\n--- {arrow} {ts} {len(msg.content)} B host={flow.request.pretty_host} ---")
|
||||
_w(_fmt(msg.content))
|
||||
|
||||
|
||||
def websocket_end(flow: http.HTTPFlow) -> None:
|
||||
if not _interesting(flow.request.pretty_host):
|
||||
return
|
||||
_w(f"\n# WS CLOSE {flow.request.pretty_host}\n")
|
||||
|
||||
|
||||
def response(flow: http.HTTPFlow) -> None:
|
||||
host = flow.request.pretty_host
|
||||
if not _interesting(host):
|
||||
return
|
||||
if flow.websocket is not None:
|
||||
return
|
||||
_w("\n" + "#" * 70)
|
||||
_w(f"# HTTP {flow.request.method} {host}{flow.request.path} -> {flow.response.status_code}")
|
||||
if flow.request.content:
|
||||
_w(" REQ: " + _fmt(flow.request.content)[:2000])
|
||||
if flow.response.content:
|
||||
_w(" RES: " + _fmt(flow.response.content)[:2000])
|
||||
|
||||
|
||||
def load(loader) -> None:
|
||||
_w(f"\n\n########## capture session start {time.strftime('%Y-%m-%d %H:%M:%S')} ##########")
|
||||
ctx.log.info("ws2_dump addon loaded")
|
||||
Reference in New Issue
Block a user