Добавил отображение форматированных сообщений, возможность их форматировать
This commit is contained in:
@@ -129,7 +129,6 @@ extension ApiServiceAuth on ApiService {
|
||||
await prefs.setString('userId', userId);
|
||||
}
|
||||
|
||||
// Полный сброс сессии как при переключении аккаунта
|
||||
_messageQueue.clear();
|
||||
_lastChatsPayload = null;
|
||||
_chatsFetchedInThisSession = false;
|
||||
@@ -231,9 +230,37 @@ extension ApiServiceAuth on ApiService {
|
||||
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
// Удаляем текущий аккаунт из AccountManager / prefs
|
||||
final accountManager = AccountManager();
|
||||
await accountManager.initialize();
|
||||
final currentAccount = accountManager.currentAccount;
|
||||
|
||||
if (currentAccount != null) {
|
||||
try {
|
||||
if (accountManager.accounts.length > 1) {
|
||||
await accountManager.removeAccount(currentAccount.id);
|
||||
} else {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
await prefs.remove('multi_accounts');
|
||||
await prefs.remove('current_account_id');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка при удалении аккаунта: $e');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
}
|
||||
} else {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
await prefs.remove('multi_accounts');
|
||||
await prefs.remove('current_account_id');
|
||||
}
|
||||
|
||||
// Чистим in-memory состояние и разрываем соединение
|
||||
authToken = null;
|
||||
userId = null;
|
||||
_messageCache.clear();
|
||||
@@ -242,7 +269,20 @@ extension ApiServiceAuth on ApiService {
|
||||
_pingTimer?.cancel();
|
||||
await _channel?.sink.close(status.goingAway);
|
||||
_channel = null;
|
||||
} catch (_) {}
|
||||
|
||||
clearAllCaches();
|
||||
|
||||
_isSessionOnline = false;
|
||||
_isSessionReady = false;
|
||||
_handshakeSent = false;
|
||||
_reconnectAttempts = 0;
|
||||
_currentUrlIndex = 0;
|
||||
|
||||
_messageQueue.clear();
|
||||
_presenceData.clear();
|
||||
} catch (e) {
|
||||
print('Ошибка logout(): $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllData() async {
|
||||
|
||||
@@ -1031,6 +1031,7 @@ extension ApiServiceChats on ApiService {
|
||||
String text, {
|
||||
String? replyToMessageId,
|
||||
int? cid,
|
||||
List<Map<String, dynamic>>? elements,
|
||||
}) {
|
||||
final int clientMessageId = cid ?? DateTime.now().millisecondsSinceEpoch;
|
||||
final payload = {
|
||||
@@ -1038,7 +1039,7 @@ extension ApiServiceChats on ApiService {
|
||||
"message": {
|
||||
"text": text,
|
||||
"cid": clientMessageId,
|
||||
"elements": [],
|
||||
"elements": elements ?? [],
|
||||
"attaches": [],
|
||||
if (replyToMessageId != null)
|
||||
"link": {"type": "REPLY", "messageId": replyToMessageId},
|
||||
|
||||
@@ -9,6 +9,7 @@ class Message {
|
||||
final int? cid; // клиентский id (timestamp)
|
||||
final Map<String, dynamic>? reactionInfo; // Информация о реакциях
|
||||
final Map<String, dynamic>? link; // Информация об ответе на сообщение
|
||||
final List<Map<String, dynamic>> elements; // Форматирование текста
|
||||
|
||||
Message({
|
||||
required this.id,
|
||||
@@ -21,6 +22,7 @@ class Message {
|
||||
this.cid,
|
||||
this.reactionInfo,
|
||||
this.link,
|
||||
this.elements = const [],
|
||||
});
|
||||
|
||||
factory Message.fromJson(Map<String, dynamic> json) {
|
||||
@@ -55,6 +57,11 @@ class Message {
|
||||
cid: json['cid'],
|
||||
reactionInfo: json['reactionInfo'],
|
||||
link: json['link'],
|
||||
elements:
|
||||
(json['elements'] as List?)
|
||||
?.map((e) => (e as Map).cast<String, dynamic>())
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +76,7 @@ class Message {
|
||||
int? cid,
|
||||
Map<String, dynamic>? reactionInfo,
|
||||
Map<String, dynamic>? link,
|
||||
List<Map<String, dynamic>>? elements,
|
||||
}) {
|
||||
return Message(
|
||||
id: id ?? this.id,
|
||||
@@ -81,6 +89,7 @@ class Message {
|
||||
cid: cid ?? this.cid,
|
||||
reactionInfo: reactionInfo ?? this.reactionInfo,
|
||||
link: link ?? this.link,
|
||||
elements: elements ?? this.elements,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,6 +126,7 @@ class Message {
|
||||
'attaches': attaches,
|
||||
'link': link,
|
||||
'reactionInfo': reactionInfo,
|
||||
'elements': elements,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ 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:lottie/lottie.dart';
|
||||
import 'package:gwid/widgets/formatted_text_controller.dart';
|
||||
|
||||
bool _debugShowExactDate = false;
|
||||
|
||||
@@ -230,7 +231,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
bool _isLoadingHistory = true;
|
||||
Map<String, dynamic>? _emptyChatSticker;
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final FormattedTextController _textController = FormattedTextController();
|
||||
final FocusNode _textFocusNode = FocusNode();
|
||||
StreamSubscription? _apiSubscription;
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
@@ -1946,6 +1947,28 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _applyTextFormat(String type) {
|
||||
final selection = _textController.selection;
|
||||
if (!selection.isValid || selection.isCollapsed) return;
|
||||
final from = selection.start;
|
||||
final length = selection.end - selection.start;
|
||||
if (length <= 0) return;
|
||||
|
||||
setState(() {
|
||||
_textController.elements.add({
|
||||
'type': type,
|
||||
'from': from,
|
||||
'length': length,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _resetDraftFormattingIfNeeded(String newText) {
|
||||
if (newText.isEmpty) {
|
||||
_textController.elements.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
final originalText = _textController.text.trim();
|
||||
if (originalText.isNotEmpty) {
|
||||
@@ -1988,7 +2011,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
if (textToSend != originalText) {
|
||||
_textController.elements.clear();
|
||||
}
|
||||
|
||||
final int tempCid = DateTime.now().millisecondsSinceEpoch;
|
||||
final List<Map<String, dynamic>> tempElements =
|
||||
List<Map<String, dynamic>>.from(_textController.elements);
|
||||
final tempMessageJson = {
|
||||
'id': 'local_$tempCid',
|
||||
'text': textToSend,
|
||||
@@ -1997,6 +2026,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
'cid': tempCid,
|
||||
'type': 'USER',
|
||||
'attaches': [],
|
||||
'elements': tempElements,
|
||||
'link': _replyingToMessage != null
|
||||
? {
|
||||
'type': 'REPLY',
|
||||
@@ -2026,6 +2056,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
textToSend,
|
||||
replyToMessageId: _replyingToMessage?.id,
|
||||
cid: tempCid, // Передаем тот же CID в API
|
||||
elements: tempElements,
|
||||
);
|
||||
|
||||
final readSettings = await ChatReadSettingsService.instance.getSettings(
|
||||
@@ -2045,6 +2076,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
setState(() {
|
||||
_replyingToMessage = null;
|
||||
_textController.elements.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4233,52 +4265,115 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
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,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
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) {
|
||||
_resetDraftFormattingIfNeeded(v);
|
||||
if (v.isNotEmpty) {
|
||||
_scheduleTypingPing();
|
||||
}
|
||||
},
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide.none,
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.format_bold),
|
||||
onPressed: isBlocked
|
||||
? null
|
||||
: () => _applyTextFormat('STRONG'),
|
||||
tooltip: 'Жирный',
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.format_italic),
|
||||
onPressed: isBlocked
|
||||
? null
|
||||
: () => _applyTextFormat(
|
||||
'EMPHASIZED',
|
||||
),
|
||||
tooltip: 'Курсив',
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(
|
||||
Icons.format_underline,
|
||||
),
|
||||
onPressed: isBlocked
|
||||
? null
|
||||
: () =>
|
||||
_applyTextFormat('UNDERLINE'),
|
||||
tooltip: 'Подчеркнуть',
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(
|
||||
Icons.format_strikethrough,
|
||||
),
|
||||
onPressed: isBlocked
|
||||
? null
|
||||
: () => _applyTextFormat(
|
||||
'STRIKETHROUGH',
|
||||
),
|
||||
tooltip: 'Зачеркнуть',
|
||||
),
|
||||
],
|
||||
),
|
||||
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();
|
||||
}
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3950,39 +3950,8 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
|
||||
Future<void> _logout() async {
|
||||
try {
|
||||
ApiService.instance.disconnect();
|
||||
|
||||
final accountManager = AccountManager();
|
||||
await accountManager.initialize();
|
||||
final currentAccount = accountManager.currentAccount;
|
||||
|
||||
if (currentAccount != null) {
|
||||
try {
|
||||
if (accountManager.accounts.length > 1) {
|
||||
await accountManager.removeAccount(currentAccount.id);
|
||||
} else {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
await prefs.remove('multi_accounts');
|
||||
await prefs.remove('current_account_id');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка при удалении аккаунта: $e');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
}
|
||||
} else {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('authToken');
|
||||
await prefs.remove('userId');
|
||||
}
|
||||
|
||||
await ApiService.instance.logout();
|
||||
|
||||
ApiService.instance.clearAllCaches();
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => const PhoneEntryScreen()),
|
||||
|
||||
@@ -4097,6 +4097,7 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
defaultTextStyle,
|
||||
linkStyle,
|
||||
onOpenLink,
|
||||
elements: message.elements,
|
||||
)
|
||||
else if (message.text.contains("welcome.saved.dialog.message"))
|
||||
Linkify(
|
||||
@@ -4115,15 +4116,15 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
defaultTextStyle,
|
||||
linkStyle,
|
||||
onOpenLink,
|
||||
elements: message.elements,
|
||||
)
|
||||
else
|
||||
Linkify(
|
||||
text: message.text,
|
||||
style: defaultTextStyle,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: onOpenLink,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
textAlign: TextAlign.left,
|
||||
_buildMixedMessageContent(
|
||||
message.text,
|
||||
defaultTextStyle,
|
||||
linkStyle,
|
||||
onOpenLink,
|
||||
elements: message.elements,
|
||||
),
|
||||
if (message.reactionInfo != null) const SizedBox(height: 4),
|
||||
],
|
||||
@@ -4380,8 +4381,9 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
String text,
|
||||
TextStyle baseStyle,
|
||||
TextStyle linkStyle,
|
||||
Future<void> Function(LinkableElement) onOpenLink,
|
||||
) {
|
||||
Future<void> Function(LinkableElement) onOpenLink, {
|
||||
List<Map<String, dynamic>> elements = const [],
|
||||
}) {
|
||||
final segments = _parseMixedMessageSegments(text);
|
||||
|
||||
return Wrap(
|
||||
@@ -4389,21 +4391,22 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
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),
|
||||
);
|
||||
final baseForSeg = seg.type == _KometSegmentType.colored
|
||||
? baseStyle.copyWith(color: seg.color)
|
||||
: baseStyle;
|
||||
|
||||
if (elements.isEmpty) {
|
||||
return Linkify(
|
||||
text: seg.text,
|
||||
style: baseForSeg,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: onOpenLink,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
);
|
||||
} else {
|
||||
return _buildFormattedRichText(seg.text, baseForSeg, elements);
|
||||
}
|
||||
case _KometSegmentType.galaxy:
|
||||
return _GalaxyAnimatedText(text: seg.text);
|
||||
case _KometSegmentType.pulse:
|
||||
@@ -4444,6 +4447,78 @@ class ChatMessageBubble extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Строит RichText с учётом elements (STRONG, EMPHASIZED, UNDERLINE, STRIKETHROUGH).
|
||||
Widget _buildFormattedRichText(
|
||||
String text,
|
||||
TextStyle baseStyle,
|
||||
List<Map<String, dynamic>> elements,
|
||||
) {
|
||||
if (text.isEmpty || elements.isEmpty) {
|
||||
return Text(text, style: baseStyle);
|
||||
}
|
||||
|
||||
final bold = List<bool>.filled(text.length, false);
|
||||
final italic = List<bool>.filled(text.length, false);
|
||||
final underline = List<bool>.filled(text.length, false);
|
||||
final strike = List<bool>.filled(text.length, false);
|
||||
|
||||
for (final el in elements) {
|
||||
final type = el['type'] as String?;
|
||||
final from = (el['from'] as int?) ?? 0;
|
||||
final length = (el['length'] as int?) ?? 0;
|
||||
if (type == null || length <= 0) continue;
|
||||
final start = from.clamp(0, text.length);
|
||||
final end = (from + length).clamp(0, text.length);
|
||||
for (int i = start; i < end; i++) {
|
||||
switch (type) {
|
||||
case 'STRONG':
|
||||
bold[i] = true;
|
||||
break;
|
||||
case 'EMPHASIZED':
|
||||
italic[i] = true;
|
||||
break;
|
||||
case 'UNDERLINE':
|
||||
underline[i] = true;
|
||||
break;
|
||||
case 'STRIKETHROUGH':
|
||||
strike[i] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final spans = <TextSpan>[];
|
||||
int start = 0;
|
||||
|
||||
TextStyle _styleForIndex(int i) {
|
||||
var s = baseStyle;
|
||||
if (bold[i]) s = s.copyWith(fontWeight: FontWeight.w600);
|
||||
if (italic[i]) s = s.copyWith(fontStyle: FontStyle.italic);
|
||||
final line = <TextDecoration>[];
|
||||
if (underline[i]) line.add(TextDecoration.underline);
|
||||
if (strike[i]) line.add(TextDecoration.lineThrough);
|
||||
if (line.isNotEmpty) {
|
||||
s = s.copyWith(decoration: TextDecoration.combine(line));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
while (start < text.length) {
|
||||
int end = start + 1;
|
||||
final style = _styleForIndex(start);
|
||||
while (end < text.length && _styleForIndex(end) == style) {
|
||||
end++;
|
||||
}
|
||||
spans.add(TextSpan(text: text.substring(start, end), style: style));
|
||||
start = end;
|
||||
}
|
||||
|
||||
return RichText(
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(children: spans, style: baseStyle),
|
||||
);
|
||||
}
|
||||
|
||||
List<_KometColoredSegment> _parseKometColorSegments(
|
||||
String text,
|
||||
Color? fallbackColor,
|
||||
|
||||
79
lib/widgets/formatted_text_controller.dart
Normal file
79
lib/widgets/formatted_text_controller.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FormattedTextController extends TextEditingController {
|
||||
final List<Map<String, dynamic>> elements = [];
|
||||
|
||||
FormattedTextController({String? text}) : super(text: text);
|
||||
|
||||
@override
|
||||
TextSpan buildTextSpan({
|
||||
required BuildContext context,
|
||||
TextStyle? style,
|
||||
bool withComposing = false,
|
||||
}) {
|
||||
final baseStyle = style ?? DefaultTextStyle.of(context).style;
|
||||
final text = value.text;
|
||||
|
||||
if (text.isEmpty || elements.isEmpty) {
|
||||
return TextSpan(text: text, style: baseStyle);
|
||||
}
|
||||
|
||||
final bold = List<bool>.filled(text.length, false);
|
||||
final italic = List<bool>.filled(text.length, false);
|
||||
final underline = List<bool>.filled(text.length, false);
|
||||
final strike = List<bool>.filled(text.length, false);
|
||||
|
||||
for (final el in elements) {
|
||||
final type = el['type'] as String?;
|
||||
final from = (el['from'] as int?) ?? 0;
|
||||
final length = (el['length'] as int?) ?? 0;
|
||||
if (type == null || length <= 0) continue;
|
||||
final start = from.clamp(0, text.length);
|
||||
final end = (from + length).clamp(0, text.length);
|
||||
for (int i = start; i < end; i++) {
|
||||
switch (type) {
|
||||
case 'STRONG':
|
||||
bold[i] = true;
|
||||
break;
|
||||
case 'EMPHASIZED':
|
||||
italic[i] = true;
|
||||
break;
|
||||
case 'UNDERLINE':
|
||||
underline[i] = true;
|
||||
break;
|
||||
case 'STRIKETHROUGH':
|
||||
strike[i] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final spans = <InlineSpan>[];
|
||||
int start = 0;
|
||||
|
||||
TextStyle styleForIndex(int i) {
|
||||
var s = baseStyle;
|
||||
if (bold[i]) s = s.copyWith(fontWeight: FontWeight.w600);
|
||||
if (italic[i]) s = s.copyWith(fontStyle: FontStyle.italic);
|
||||
final decos = <TextDecoration>[];
|
||||
if (underline[i]) decos.add(TextDecoration.underline);
|
||||
if (strike[i]) decos.add(TextDecoration.lineThrough);
|
||||
if (decos.isNotEmpty) {
|
||||
s = s.copyWith(decoration: TextDecoration.combine(decos));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
while (start < text.length) {
|
||||
int end = start + 1;
|
||||
final base = styleForIndex(start);
|
||||
while (end < text.length && styleForIndex(end) == base) {
|
||||
end++;
|
||||
}
|
||||
spans.add(TextSpan(text: text.substring(start, end), style: base));
|
||||
start = end;
|
||||
}
|
||||
|
||||
return TextSpan(style: baseStyle, children: spans);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user