ыы анимашкэ светящкэ, в разделе фишки можно включить автопомощь при написании особых сообщений(цветное, косметика д.р) и кнопку слева в которой будет список особых сообщений. Затрахал досмерти руками и нашел пару багов, уже залатал, чето там бля делайте думойте
This commit is contained in:
@@ -28,6 +28,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import 'package:gwid/screens/chat_encryption_settings_screen.dart';
|
import 'package:gwid/screens/chat_encryption_settings_screen.dart';
|
||||||
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:gwid/services/chat_encryption_service.dart';
|
import 'package:gwid/services/chat_encryption_service.dart';
|
||||||
|
|
||||||
bool _debugShowExactDate = false;
|
bool _debugShowExactDate = false;
|
||||||
@@ -123,6 +124,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
false; // TODO: hook real state later
|
false; // TODO: hook real state later
|
||||||
ChatEncryptionConfig? _encryptionConfigForCurrentChat;
|
ChatEncryptionConfig? _encryptionConfigForCurrentChat;
|
||||||
bool _sendEncryptedForCurrentChat = false;
|
bool _sendEncryptedForCurrentChat = false;
|
||||||
|
bool _specialMessagesEnabled = false;
|
||||||
|
|
||||||
|
bool _showKometColorPicker = false;
|
||||||
|
String? _currentKometColorPrefix;
|
||||||
|
|
||||||
bool _isSearching = false;
|
bool _isSearching = false;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
@@ -353,6 +358,214 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
null; // Будет установлено при получении CONTROL сообщения с event 'pin'
|
null; // Будет установлено при получении CONTROL сообщения с event 'pin'
|
||||||
_initializeChat();
|
_initializeChat();
|
||||||
_loadEncryptionConfig();
|
_loadEncryptionConfig();
|
||||||
|
_loadSpecialMessagesSetting();
|
||||||
|
|
||||||
|
_textController.addListener(() {
|
||||||
|
_handleTextChangedForKometColor();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSpecialMessagesSetting() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
setState(() {
|
||||||
|
_specialMessagesEnabled =
|
||||||
|
prefs.getBool('special_messages_enabled') ?? false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSpecialMessagesPanel() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (context) => Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Особые сообщения',
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_SpecialMessageButton(
|
||||||
|
label: 'Цветной текст',
|
||||||
|
template: "komet.color_#''",
|
||||||
|
icon: Icons.color_lens,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Future.microtask(() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final currentText = _textController.text;
|
||||||
|
final cursorPos = _textController.selection.baseOffset.clamp(
|
||||||
|
0,
|
||||||
|
currentText.length,
|
||||||
|
);
|
||||||
|
final template = "komet.color_#";
|
||||||
|
final newText =
|
||||||
|
currentText.substring(0, cursorPos) +
|
||||||
|
template +
|
||||||
|
currentText.substring(cursorPos);
|
||||||
|
_textController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: cursorPos + template.length - 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_SpecialMessageButton(
|
||||||
|
label: 'Переливающийся текст',
|
||||||
|
template: "komet.cosmetic.galaxy' ваш текст '",
|
||||||
|
icon: Icons.auto_awesome,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Future.microtask(() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final currentText = _textController.text;
|
||||||
|
final cursorPos = _textController.selection.baseOffset.clamp(
|
||||||
|
0,
|
||||||
|
currentText.length,
|
||||||
|
);
|
||||||
|
final template = "komet.cosmetic.galaxy' ваш текст '";
|
||||||
|
final newText =
|
||||||
|
currentText.substring(0, cursorPos) +
|
||||||
|
template +
|
||||||
|
currentText.substring(cursorPos);
|
||||||
|
_textController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: cursorPos + template.length - 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_SpecialMessageButton(
|
||||||
|
label: 'Пульсирующий текст',
|
||||||
|
template: "komet.cosmetic.pulse#",
|
||||||
|
icon: Icons.radio_button_checked,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Future.microtask(() {
|
||||||
|
if (!mounted) return;
|
||||||
|
final currentText = _textController.text;
|
||||||
|
final cursorPos = _textController.selection.baseOffset.clamp(
|
||||||
|
0,
|
||||||
|
currentText.length,
|
||||||
|
);
|
||||||
|
final template = "komet.cosmetic.pulse#";
|
||||||
|
final newText =
|
||||||
|
currentText.substring(0, cursorPos) +
|
||||||
|
template +
|
||||||
|
currentText.substring(cursorPos);
|
||||||
|
_textController.value = TextEditingValue(
|
||||||
|
text: newText,
|
||||||
|
selection: TextSelection.collapsed(
|
||||||
|
offset: cursorPos + template.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleTextChangedForKometColor() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final autoCompleteEnabled =
|
||||||
|
prefs.getBool('komet_auto_complete_enabled') ?? false;
|
||||||
|
|
||||||
|
if (!autoCompleteEnabled) {
|
||||||
|
if (_showKometColorPicker) {
|
||||||
|
setState(() {
|
||||||
|
_showKometColorPicker = false;
|
||||||
|
_currentKometColorPrefix = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final text = _textController.text;
|
||||||
|
final cursorPos = _textController.selection.baseOffset;
|
||||||
|
const prefix1 = 'komet.color_#';
|
||||||
|
const prefix2 = 'komet.cosmetic.pulse#';
|
||||||
|
|
||||||
|
// Ищем префикс в позиции курсора или перед ним
|
||||||
|
String? detectedPrefix;
|
||||||
|
int? prefixStartPos;
|
||||||
|
|
||||||
|
// Проверяем, находится ли курсор сразу после префикса
|
||||||
|
for (final prefix in [prefix1, prefix2]) {
|
||||||
|
// Ищем последнее вхождение префикса перед курсором
|
||||||
|
int searchStart = 0;
|
||||||
|
int lastFound = -1;
|
||||||
|
while (true) {
|
||||||
|
final found = text.indexOf(prefix, searchStart);
|
||||||
|
if (found == -1 || found > cursorPos) break;
|
||||||
|
if (found + prefix.length <= cursorPos) {
|
||||||
|
lastFound = found;
|
||||||
|
}
|
||||||
|
searchStart = found + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFound != -1) {
|
||||||
|
final afterPrefix = text.substring(
|
||||||
|
lastFound + prefix.length,
|
||||||
|
cursorPos,
|
||||||
|
);
|
||||||
|
// Если после префикса до курсора ничего нет (или только пробелы) - показываем панель
|
||||||
|
if (afterPrefix.isEmpty || afterPrefix.trim().isEmpty) {
|
||||||
|
// Проверяем, что после курсора нет завершенного блока (нет HEX и кавычек)
|
||||||
|
final afterCursor = cursorPos < text.length
|
||||||
|
? text.substring(cursorPos)
|
||||||
|
: '';
|
||||||
|
// Если после курсора сразу идет HEX код (6 символов) и кавычка - не показываем
|
||||||
|
if (afterCursor.length < 7 ||
|
||||||
|
!RegExp(r"^[0-9A-Fa-f]{6}'").hasMatch(afterCursor)) {
|
||||||
|
detectedPrefix = prefix;
|
||||||
|
prefixStartPos = lastFound;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detectedPrefix != null && prefixStartPos != null) {
|
||||||
|
final after = text.substring(
|
||||||
|
prefixStartPos + detectedPrefix.length,
|
||||||
|
cursorPos,
|
||||||
|
);
|
||||||
|
// Если после # до курсора ничего нет — показываем панельку
|
||||||
|
if (after.isEmpty || after.trim().isEmpty) {
|
||||||
|
if (!_showKometColorPicker ||
|
||||||
|
_currentKometColorPrefix != detectedPrefix) {
|
||||||
|
setState(() {
|
||||||
|
_showKometColorPicker = true;
|
||||||
|
_currentKometColorPrefix = detectedPrefix;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_showKometColorPicker) {
|
||||||
|
setState(() {
|
||||||
|
_showKometColorPicker = false;
|
||||||
|
_currentKometColorPrefix = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadEncryptionConfig() async {
|
Future<void> _loadEncryptionConfig() async {
|
||||||
@@ -3350,83 +3563,146 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
if (_specialMessagesEnabled)
|
||||||
child: Focus(
|
Material(
|
||||||
focusNode:
|
color: Colors.transparent,
|
||||||
_textFocusNode, // 2. focusNode теперь здесь
|
child: InkWell(
|
||||||
onKeyEvent: (node, event) {
|
borderRadius: BorderRadius.circular(24),
|
||||||
if (event is KeyDownEvent) {
|
onTap: isBlocked ? null : _showSpecialMessagesPanel,
|
||||||
if (event.logicalKey ==
|
child: Padding(
|
||||||
LogicalKeyboardKey.enter) {
|
padding: const EdgeInsets.all(6.0),
|
||||||
final bool isShiftPressed =
|
child: Icon(
|
||||||
HardwareKeyboard.instance.logicalKeysPressed
|
Icons.auto_fix_high,
|
||||||
.contains(
|
color: isBlocked
|
||||||
LogicalKeyboardKey.shiftLeft,
|
? Theme.of(
|
||||||
) ||
|
context,
|
||||||
HardwareKeyboard.instance.logicalKeysPressed
|
).colorScheme.onSurface.withOpacity(0.3)
|
||||||
.contains(
|
: Theme.of(context).colorScheme.primary,
|
||||||
LogicalKeyboardKey.shiftRight,
|
size: 24,
|
||||||
);
|
|
||||||
|
|
||||||
if (!isShiftPressed) {
|
|
||||||
_sendMessage();
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
},
|
|
||||||
|
|
||||||
child: TextField(
|
|
||||||
controller: _textController,
|
|
||||||
|
|
||||||
enabled: !isBlocked,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
textInputAction: TextInputAction.newline,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: 5,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: isBlocked
|
|
||||||
? 'Пользователь заблокирован'
|
|
||||||
: 'Сообщение...',
|
|
||||||
filled: true,
|
|
||||||
isDense: true,
|
|
||||||
fillColor: isBlocked
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHighest
|
|
||||||
.withOpacity(0.25)
|
|
||||||
: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHighest
|
|
||||||
.withOpacity(0.4),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 18.0,
|
|
||||||
vertical: 12.0,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
onChanged: isBlocked
|
|
||||||
? null
|
|
||||||
: (v) {
|
|
||||||
if (v.isNotEmpty) {
|
|
||||||
_scheduleTypingPing();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_specialMessagesEnabled) const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_showKometColorPicker)
|
||||||
|
_KometColorPickerBar(
|
||||||
|
onColorSelected: (color) {
|
||||||
|
if (_currentKometColorPrefix == null) return;
|
||||||
|
final hex = color.value
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(8, '0')
|
||||||
|
.substring(2)
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
String newText;
|
||||||
|
int cursorOffset;
|
||||||
|
|
||||||
|
if (_currentKometColorPrefix ==
|
||||||
|
'komet.color_#') {
|
||||||
|
newText =
|
||||||
|
'$_currentKometColorPrefix$hex\'ваш текст\'';
|
||||||
|
final textLength = newText.length;
|
||||||
|
cursorOffset = textLength - 12;
|
||||||
|
} else if (_currentKometColorPrefix ==
|
||||||
|
'komet.cosmetic.pulse#') {
|
||||||
|
newText =
|
||||||
|
'$_currentKometColorPrefix$hex\'ваш текст\'';
|
||||||
|
final textLength = newText.length;
|
||||||
|
cursorOffset = textLength - 12;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_textController.text = newText;
|
||||||
|
_textController.selection = TextSelection(
|
||||||
|
baseOffset: cursorOffset,
|
||||||
|
extentOffset: newText.length - 1,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Focus(
|
||||||
|
focusNode:
|
||||||
|
_textFocusNode, // 2. focusNode теперь здесь
|
||||||
|
onKeyEvent: (node, event) {
|
||||||
|
if (event is KeyDownEvent) {
|
||||||
|
if (event.logicalKey ==
|
||||||
|
LogicalKeyboardKey.enter) {
|
||||||
|
final bool isShiftPressed =
|
||||||
|
HardwareKeyboard
|
||||||
|
.instance
|
||||||
|
.logicalKeysPressed
|
||||||
|
.contains(
|
||||||
|
LogicalKeyboardKey.shiftLeft,
|
||||||
|
) ||
|
||||||
|
HardwareKeyboard
|
||||||
|
.instance
|
||||||
|
.logicalKeysPressed
|
||||||
|
.contains(
|
||||||
|
LogicalKeyboardKey.shiftRight,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isShiftPressed) {
|
||||||
|
_sendMessage();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
},
|
||||||
|
child: TextField(
|
||||||
|
controller: _textController,
|
||||||
|
enabled: !isBlocked,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
textInputAction: TextInputAction.newline,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 5,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: isBlocked
|
||||||
|
? 'Пользователь заблокирован'
|
||||||
|
: 'Сообщение...',
|
||||||
|
filled: true,
|
||||||
|
isDense: true,
|
||||||
|
fillColor: isBlocked
|
||||||
|
? Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest
|
||||||
|
.withOpacity(0.25)
|
||||||
|
: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest
|
||||||
|
.withOpacity(0.4),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 18.0,
|
||||||
|
vertical: 12.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: isBlocked
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
|
if (v.isNotEmpty) {
|
||||||
|
_scheduleTypingPing();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Material(
|
Material(
|
||||||
@@ -3885,6 +4161,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_typingTimer?.cancel();
|
_typingTimer?.cancel();
|
||||||
_apiSubscription?.cancel();
|
_apiSubscription?.cancel();
|
||||||
|
_textController.removeListener(_handleTextChangedForKometColor);
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
_textFocusNode.dispose();
|
_textFocusNode.dispose();
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
@@ -4002,6 +4279,193 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SpecialMessageButton extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String template;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _SpecialMessageButton({
|
||||||
|
required this.label,
|
||||||
|
required this.template,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: colors.primary),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
template,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(Icons.chevron_right, color: colors.onSurfaceVariant),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KometColorPickerBar extends StatefulWidget {
|
||||||
|
final ValueChanged<Color> onColorSelected;
|
||||||
|
|
||||||
|
const _KometColorPickerBar({required this.onColorSelected});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_KometColorPickerBar> createState() => _KometColorPickerBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KometColorPickerBarState extends State<_KometColorPickerBar> {
|
||||||
|
Color _currentColor = Colors.red;
|
||||||
|
|
||||||
|
void _showColorPickerDialog(BuildContext context) {
|
||||||
|
Color pickedColor = _currentColor;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Выберите цвет'),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return ColorPicker(
|
||||||
|
pickerColor: pickedColor,
|
||||||
|
onColorChanged: (color) {
|
||||||
|
setState(() => pickedColor = color);
|
||||||
|
},
|
||||||
|
enableAlpha: false,
|
||||||
|
pickerAreaHeightPercent: 0.8,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Отмена'),
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Готово'),
|
||||||
|
onPressed: () {
|
||||||
|
widget.onColorSelected(pickedColor);
|
||||||
|
setState(() {
|
||||||
|
_currentColor = pickedColor;
|
||||||
|
});
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = Theme.of(context).colorScheme;
|
||||||
|
const double diameter = 32;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 6),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceContainerHighest.withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.12),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Выберите цвет для komet.color',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
_showColorPickerDialog(context);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: diameter,
|
||||||
|
height: diameter,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: const SweepGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.red,
|
||||||
|
Colors.yellow,
|
||||||
|
Colors.green,
|
||||||
|
Colors.cyan,
|
||||||
|
Colors.blue,
|
||||||
|
Colors.purple,
|
||||||
|
Colors.red,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: diameter - 12,
|
||||||
|
height: diameter - 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _currentColor,
|
||||||
|
border: Border.all(color: colors.surface, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _EditMessageDialog extends StatefulWidget {
|
class _EditMessageDialog extends StatefulWidget {
|
||||||
final String initialText;
|
final String initialText;
|
||||||
final Function(String) onSave;
|
final Function(String) onSave;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:gwid/utils/theme_provider.dart';
|
import 'package:gwid/utils/theme_provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class BypassScreen extends StatefulWidget {
|
class BypassScreen extends StatefulWidget {
|
||||||
final bool isModal;
|
final bool isModal;
|
||||||
@@ -14,6 +15,42 @@ class BypassScreen extends StatefulWidget {
|
|||||||
class _BypassScreenState extends State<BypassScreen> {
|
class _BypassScreenState extends State<BypassScreen> {
|
||||||
// 0 – обходы, 1 – фишки
|
// 0 – обходы, 1 – фишки
|
||||||
int _selectedTab = 0;
|
int _selectedTab = 0;
|
||||||
|
bool _kometAutoCompleteEnabled = false;
|
||||||
|
bool _specialMessagesEnabled = false;
|
||||||
|
bool _isLoadingSettings = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
setState(() {
|
||||||
|
_kometAutoCompleteEnabled =
|
||||||
|
prefs.getBool('komet_auto_complete_enabled') ?? false;
|
||||||
|
_specialMessagesEnabled =
|
||||||
|
prefs.getBool('special_messages_enabled') ?? false;
|
||||||
|
_isLoadingSettings = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveSpecialMessages(bool value) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool('special_messages_enabled', value);
|
||||||
|
setState(() {
|
||||||
|
_specialMessagesEnabled = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveKometAutoComplete(bool value) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool('komet_auto_complete_enabled', value);
|
||||||
|
setState(() {
|
||||||
|
_kometAutoCompleteEnabled = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -206,7 +243,7 @@ class _BypassScreenState extends State<BypassScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
"В будущих версиях можно будет подсвечивать отдельные буквы и слова в нике с помощью простого синтаксиса.",
|
"В будущих версиях можно будет подсвечивать отдельные буквы и слова в нике с помощью простого синтаксиса, а также добавлять визуальные эффекты к сообщениям.",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: colors.onSurfaceVariant,
|
color: colors.onSurfaceVariant,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -227,7 +264,7 @@ class _BypassScreenState extends State<BypassScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Простой пример:",
|
"Простой пример (цветники):",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: colors.onSurface,
|
color: colors.onSurface,
|
||||||
@@ -262,6 +299,68 @@ class _BypassScreenState extends State<BypassScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Пример (пульсирующий текст):",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SelectableText(
|
||||||
|
"komet.cosmetic.pulse#FF0000'пульсирующий текст'",
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: colors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Отображение: текст «пульсирующий текст» в пузыре сообщения пульсирует указанным цветом (в данном случае красным).",
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Пример (переливающийся ч/б текст в сообщении):",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SelectableText(
|
||||||
|
"komet.cosmetic.galaxy'тестовое сообщение'",
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: colors.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Отображение:",
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.surfaceVariant.withOpacity(0.6),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const _GalaxyDemoText(
|
||||||
|
text: "тестовое сообщение",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
"Сложный пример:",
|
"Сложный пример:",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -361,6 +460,64 @@ class _BypassScreenState extends State<BypassScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (!_isLoadingSettings) ...[
|
||||||
|
Consumer<ThemeProvider>(
|
||||||
|
builder: (context, themeProvider, child) {
|
||||||
|
return Card(
|
||||||
|
child: SwitchListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Авто-дополнение уникальных сообщений',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Показывать панель выбора цвета при вводе komet.color#',
|
||||||
|
),
|
||||||
|
value: _kometAutoCompleteEnabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
_saveKometAutoComplete(value);
|
||||||
|
},
|
||||||
|
secondary: Icon(
|
||||||
|
_kometAutoCompleteEnabled
|
||||||
|
? Icons.auto_awesome
|
||||||
|
: Icons.auto_awesome_outlined,
|
||||||
|
color: _kometAutoCompleteEnabled
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Consumer<ThemeProvider>(
|
||||||
|
builder: (context, themeProvider, child) {
|
||||||
|
return Card(
|
||||||
|
child: SwitchListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Включить список особых сообщений',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Показывать кнопку для быстрой вставки шаблонов особых сообщений',
|
||||||
|
),
|
||||||
|
value: _specialMessagesEnabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
_saveSpecialMessages(value);
|
||||||
|
},
|
||||||
|
secondary: Icon(
|
||||||
|
_specialMessagesEnabled
|
||||||
|
? Icons.auto_fix_high
|
||||||
|
: Icons.auto_fix_high_outlined,
|
||||||
|
color: _specialMessagesEnabled
|
||||||
|
? colors.primary
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -506,6 +663,67 @@ class _BypassScreenState extends State<BypassScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GalaxyDemoText extends StatefulWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const _GalaxyDemoText({required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_GalaxyDemoText> createState() => _GalaxyDemoTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GalaxyDemoTextState extends State<_GalaxyDemoText>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final t = _controller.value;
|
||||||
|
final color = Color.lerp(Colors.black, Colors.white, t)!;
|
||||||
|
|
||||||
|
return ShaderMask(
|
||||||
|
shaderCallback: (Rect bounds) {
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
color,
|
||||||
|
Color.lerp(Colors.white, Colors.black, t)!,
|
||||||
|
],
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
blendMode: BlendMode.srcIn,
|
||||||
|
child: Text(
|
||||||
|
widget.text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SegmentButton extends StatelessWidget {
|
class _SegmentButton extends StatelessWidget {
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final String label;
|
final String label;
|
||||||
|
|||||||
@@ -176,6 +176,183 @@ class _KometColoredSegment {
|
|||||||
_KometColoredSegment(this.text, this.color);
|
_KometColoredSegment(this.text, this.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _KometSegmentType {
|
||||||
|
normal,
|
||||||
|
colored,
|
||||||
|
galaxy,
|
||||||
|
pulse,
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KometSegment {
|
||||||
|
final String text;
|
||||||
|
final _KometSegmentType type;
|
||||||
|
final Color? color; // Для colored и pulse
|
||||||
|
|
||||||
|
_KometSegment(this.text, this.type, {this.color});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GalaxyAnimatedText extends StatefulWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const _GalaxyAnimatedText({required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_GalaxyAnimatedText> createState() => _GalaxyAnimatedTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GalaxyAnimatedTextState extends State<_GalaxyAnimatedText>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final t = _controller.value;
|
||||||
|
final color = Color.lerp(Colors.black, Colors.white, t)!;
|
||||||
|
|
||||||
|
return ShaderMask(
|
||||||
|
shaderCallback: (Rect bounds) {
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
color,
|
||||||
|
Color.lerp(Colors.white, Colors.black, t)!,
|
||||||
|
],
|
||||||
|
).createShader(bounds);
|
||||||
|
},
|
||||||
|
blendMode: BlendMode.srcIn,
|
||||||
|
child: Text(
|
||||||
|
widget.text,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PulseAnimatedText extends StatefulWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const _PulseAnimatedText({required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PulseAnimatedText> createState() => _PulseAnimatedTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PulseAnimatedTextState extends State<_PulseAnimatedText>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
Color? _pulseColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_parseColor();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1000),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _parseColor() {
|
||||||
|
final text = widget.text;
|
||||||
|
const prefix = "komet.cosmetic.pulse#";
|
||||||
|
if (!text.startsWith(prefix)) {
|
||||||
|
_pulseColor = Colors.red;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final afterHash = text.substring(prefix.length);
|
||||||
|
final quoteIndex = afterHash.indexOf("'");
|
||||||
|
if (quoteIndex == -1) {
|
||||||
|
_pulseColor = Colors.red;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hexStr = afterHash.substring(0, quoteIndex).trim();
|
||||||
|
_pulseColor = _parseHexColor(hexStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _parseHexColor(String hex) {
|
||||||
|
String hexClean = hex.trim();
|
||||||
|
if (hexClean.startsWith('#')) {
|
||||||
|
hexClean = hexClean.substring(1);
|
||||||
|
}
|
||||||
|
if (hexClean.length == 6) {
|
||||||
|
try {
|
||||||
|
return Color(int.parse('FF$hexClean', radix: 16));
|
||||||
|
} catch (e) {
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final text = widget.text;
|
||||||
|
const prefix = "komet.cosmetic.pulse#";
|
||||||
|
if (!text.startsWith(prefix) || !text.endsWith("'")) {
|
||||||
|
return Text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
final afterHash = text.substring(prefix.length);
|
||||||
|
final quoteIndex = afterHash.indexOf("'");
|
||||||
|
if (quoteIndex == -1 || quoteIndex + 1 >= afterHash.length) {
|
||||||
|
return Text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
final messageText = afterHash.substring(quoteIndex + 1, afterHash.length - 1);
|
||||||
|
final baseColor = _pulseColor ?? Colors.red;
|
||||||
|
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) {
|
||||||
|
final t = _controller.value;
|
||||||
|
final opacity = 0.5 + (t * 0.5);
|
||||||
|
final color = baseColor.withOpacity(opacity);
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
messageText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessageBubble extends StatelessWidget {
|
class ChatMessageBubble extends StatelessWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
final bool isMe;
|
final bool isMe;
|
||||||
@@ -3871,14 +4048,7 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (decryptedText != null)
|
else if (decryptedText != null)
|
||||||
Linkify(
|
_buildMixedMessageContent(decryptedText!, defaultTextStyle, linkStyle, onOpenLink)
|
||||||
text: decryptedText!,
|
|
||||||
style: defaultTextStyle,
|
|
||||||
linkStyle: linkStyle,
|
|
||||||
onOpen: onOpenLink,
|
|
||||||
options: const LinkifyOptions(humanize: false),
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
)
|
|
||||||
else if (message.text.contains("welcome.saved.dialog.message"))
|
else if (message.text.contains("welcome.saved.dialog.message"))
|
||||||
Linkify(
|
Linkify(
|
||||||
text:
|
text:
|
||||||
@@ -3889,8 +4059,9 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
options: const LinkifyOptions(humanize: false),
|
options: const LinkifyOptions(humanize: false),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
)
|
)
|
||||||
else if (message.text.contains("komet.color_"))
|
else if (message.text.contains("komet.cosmetic.") ||
|
||||||
_buildKometColorRichText(message.text, defaultTextStyle)
|
message.text.contains("komet.color_"))
|
||||||
|
_buildMixedMessageContent(message.text, defaultTextStyle, linkStyle, onOpenLink)
|
||||||
else
|
else
|
||||||
Linkify(
|
Linkify(
|
||||||
text: message.text,
|
text: message.text,
|
||||||
@@ -4018,6 +4189,144 @@ class ChatMessageBubble extends StatelessWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Парсит сообщение на сегменты с разными эффектами
|
||||||
|
List<_KometSegment> _parseMixedMessageSegments(String text) {
|
||||||
|
final segments = <_KometSegment>[];
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
while (index < text.length) {
|
||||||
|
// Ищем ближайший маркер
|
||||||
|
int nextPulse = text.indexOf("komet.cosmetic.pulse#", index);
|
||||||
|
int nextGalaxy = text.indexOf("komet.cosmetic.galaxy'", index);
|
||||||
|
int nextColor = text.indexOf("komet.color_", index);
|
||||||
|
|
||||||
|
// Находим ближайший маркер
|
||||||
|
int nextMarker = text.length;
|
||||||
|
String? markerType;
|
||||||
|
if (nextPulse != -1 && nextPulse < nextMarker) {
|
||||||
|
nextMarker = nextPulse;
|
||||||
|
markerType = "pulse";
|
||||||
|
}
|
||||||
|
if (nextGalaxy != -1 && nextGalaxy < nextMarker) {
|
||||||
|
nextMarker = nextGalaxy;
|
||||||
|
markerType = "galaxy";
|
||||||
|
}
|
||||||
|
if (nextColor != -1 && nextColor < nextMarker) {
|
||||||
|
nextMarker = nextColor;
|
||||||
|
markerType = "color";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если маркер не найден, добавляем оставшийся текст как обычный
|
||||||
|
if (markerType == null) {
|
||||||
|
if (index < text.length) {
|
||||||
|
segments.add(_KometSegment(text.substring(index), _KometSegmentType.normal));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем текст до маркера как обычный
|
||||||
|
if (nextMarker > index) {
|
||||||
|
segments.add(_KometSegment(text.substring(index, nextMarker), _KometSegmentType.normal));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем найденный маркер
|
||||||
|
if (markerType == "pulse") {
|
||||||
|
const prefix = "komet.cosmetic.pulse#";
|
||||||
|
final afterHash = text.substring(nextMarker + prefix.length);
|
||||||
|
final quoteIndex = afterHash.indexOf("'");
|
||||||
|
if (quoteIndex != -1 && quoteIndex >= 6) {
|
||||||
|
final hexStr = afterHash.substring(0, quoteIndex).trim();
|
||||||
|
final textStart = quoteIndex + 1;
|
||||||
|
final secondQuote = afterHash.indexOf("'", textStart);
|
||||||
|
if (secondQuote != -1) {
|
||||||
|
final segmentText = afterHash.substring(textStart, secondQuote);
|
||||||
|
final color = _parseKometHexColor(hexStr, null);
|
||||||
|
segments.add(_KometSegment(segmentText, _KometSegmentType.pulse, color: color));
|
||||||
|
index = nextMarker + prefix.length + secondQuote + 2; // +2 для двух кавычек
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если парсинг не удался, добавляем как обычный текст
|
||||||
|
segments.add(_KometSegment(text.substring(nextMarker, nextMarker + prefix.length + 10), _KometSegmentType.normal));
|
||||||
|
index = nextMarker + prefix.length + 10;
|
||||||
|
} else if (markerType == "galaxy") {
|
||||||
|
const prefix = "komet.cosmetic.galaxy'";
|
||||||
|
final textStart = nextMarker + prefix.length;
|
||||||
|
final quoteIndex = text.indexOf("'", textStart);
|
||||||
|
if (quoteIndex != -1) {
|
||||||
|
final segmentText = text.substring(textStart, quoteIndex);
|
||||||
|
segments.add(_KometSegment(segmentText, _KometSegmentType.galaxy));
|
||||||
|
index = quoteIndex + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Если парсинг не удался, добавляем как обычный текст
|
||||||
|
segments.add(_KometSegment(text.substring(nextMarker, textStart + 10), _KometSegmentType.normal));
|
||||||
|
index = textStart + 10;
|
||||||
|
} else if (markerType == "color") {
|
||||||
|
const marker = 'komet.color_';
|
||||||
|
final colorStart = nextMarker + marker.length;
|
||||||
|
final firstQuote = text.indexOf("'", colorStart);
|
||||||
|
if (firstQuote != -1) {
|
||||||
|
final colorStr = text.substring(colorStart, firstQuote);
|
||||||
|
final textStart = firstQuote + 1;
|
||||||
|
final secondQuote = text.indexOf("'", textStart);
|
||||||
|
if (secondQuote != -1) {
|
||||||
|
final segmentText = text.substring(textStart, secondQuote);
|
||||||
|
final color = _parseKometHexColor(colorStr, null);
|
||||||
|
segments.add(_KometSegment(segmentText, _KometSegmentType.colored, color: color));
|
||||||
|
index = secondQuote + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Если парсинг не удался, добавляем как обычный текст
|
||||||
|
segments.add(_KometSegment(text.substring(nextMarker, colorStart + 10), _KometSegmentType.normal));
|
||||||
|
index = colorStart + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Строит виджет для смешанного сообщения с разными эффектами
|
||||||
|
Widget _buildMixedMessageContent(
|
||||||
|
String text,
|
||||||
|
TextStyle baseStyle,
|
||||||
|
TextStyle linkStyle,
|
||||||
|
Future<void> Function(LinkableElement) onOpenLink,
|
||||||
|
) {
|
||||||
|
final segments = _parseMixedMessageSegments(text);
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: segments.map((seg) {
|
||||||
|
switch (seg.type) {
|
||||||
|
case _KometSegmentType.normal:
|
||||||
|
return Linkify(
|
||||||
|
text: seg.text,
|
||||||
|
style: baseStyle,
|
||||||
|
linkStyle: linkStyle,
|
||||||
|
onOpen: onOpenLink,
|
||||||
|
options: const LinkifyOptions(humanize: false),
|
||||||
|
);
|
||||||
|
case _KometSegmentType.colored:
|
||||||
|
return Linkify(
|
||||||
|
text: seg.text,
|
||||||
|
style: baseStyle.copyWith(color: seg.color),
|
||||||
|
linkStyle: linkStyle,
|
||||||
|
onOpen: onOpenLink,
|
||||||
|
options: const LinkifyOptions(humanize: false),
|
||||||
|
);
|
||||||
|
case _KometSegmentType.galaxy:
|
||||||
|
return _GalaxyAnimatedText(text: seg.text);
|
||||||
|
case _KometSegmentType.pulse:
|
||||||
|
// Создаем строку в правильном формате для _PulseAnimatedText
|
||||||
|
final hexStr = seg.color!.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase();
|
||||||
|
return _PulseAnimatedText(text: "komet.cosmetic.pulse#$hexStr'${seg.text}'");
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Строит раскрашенный текст на основе синтаксиса komet.color_#HEX'текст'.
|
/// Строит раскрашенный текст на основе синтаксиса komet.color_#HEX'текст'.
|
||||||
/// Если цвет некорректный, используется красный.
|
/// Если цвет некорректный, используется красный.
|
||||||
Widget _buildKometColorRichText(String rawText, TextStyle baseStyle) {
|
Widget _buildKometColorRichText(String rawText, TextStyle baseStyle) {
|
||||||
|
|||||||
Reference in New Issue
Block a user