ыы анимашкэ светящкэ, в разделе фишки можно включить автопомощь при написании особых сообщений(цветное, косметика д.р) и кнопку слева в которой будет список особых сообщений. Затрахал досмерти руками и нашел пару багов, уже залатал, чето там бля делайте думойте
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:video_player/video_player.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';
|
||||
|
||||
bool _debugShowExactDate = false;
|
||||
@@ -123,6 +124,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
false; // TODO: hook real state later
|
||||
ChatEncryptionConfig? _encryptionConfigForCurrentChat;
|
||||
bool _sendEncryptedForCurrentChat = false;
|
||||
bool _specialMessagesEnabled = false;
|
||||
|
||||
bool _showKometColorPicker = false;
|
||||
String? _currentKometColorPrefix;
|
||||
|
||||
bool _isSearching = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
@@ -353,6 +358,214 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
null; // Будет установлено при получении CONTROL сообщения с event 'pin'
|
||||
_initializeChat();
|
||||
_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 {
|
||||
@@ -3350,83 +3563,146 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: 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,
|
||||
if (_specialMessagesEnabled)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
onTap: isBlocked ? null : _showSpecialMessagesPanel,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Icon(
|
||||
Icons.auto_fix_high,
|
||||
color: isBlocked
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.3)
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
|
||||
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),
|
||||
Material(
|
||||
@@ -3885,6 +4161,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
void dispose() {
|
||||
_typingTimer?.cancel();
|
||||
_apiSubscription?.cancel();
|
||||
_textController.removeListener(_handleTextChangedForKometColor);
|
||||
_textController.dispose();
|
||||
_textFocusNode.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 {
|
||||
final String initialText;
|
||||
final Function(String) onSave;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:gwid/utils/theme_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class BypassScreen extends StatefulWidget {
|
||||
final bool isModal;
|
||||
@@ -14,6 +15,42 @@ class BypassScreen extends StatefulWidget {
|
||||
class _BypassScreenState extends State<BypassScreen> {
|
||||
// 0 – обходы, 1 – фишки
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
@@ -206,7 +243,7 @@ class _BypassScreenState extends State<BypassScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"В будущих версиях можно будет подсвечивать отдельные буквы и слова в нике с помощью простого синтаксиса.",
|
||||
"В будущих версиях можно будет подсвечивать отдельные буквы и слова в нике с помощью простого синтаксиса, а также добавлять визуальные эффекты к сообщениям.",
|
||||
style: TextStyle(
|
||||
color: colors.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
@@ -227,7 +264,7 @@ class _BypassScreenState extends State<BypassScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Простой пример:",
|
||||
"Простой пример (цветники):",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colors.onSurface,
|
||||
@@ -262,6 +299,68 @@ class _BypassScreenState extends State<BypassScreen> {
|
||||
],
|
||||
),
|
||||
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(
|
||||
"Сложный пример:",
|
||||
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 {
|
||||
final bool selected;
|
||||
final String label;
|
||||
|
||||
Reference in New Issue
Block a user