починил плеер, добавил отступ для панели сообщений (ДИНАМИЧЕСКИЙ СУКА, ТОЛЬКО ПОПРЛБУЙТЕ ВОЗРАЗИТИТЬ)

This commit is contained in:
needle10
2025-12-01 22:35:13 +03:00
parent 4166f1c868
commit d84e91748a
4 changed files with 464 additions and 379 deletions

View File

@@ -60,8 +60,10 @@ class ChatScreen extends StatefulWidget {
final int chatId; final int chatId;
final Contact contact; final Contact contact;
final int myId; final int myId;
/// Колбэк для мягких обновлений списка чатов (например, после редактирования сообщения). /// Колбэк для мягких обновлений списка чатов (например, после редактирования сообщения).
final VoidCallback? onChatUpdated; final VoidCallback? onChatUpdated;
/// Колбэк, который вызывается, когда чат нужно убрать из списка (удаление / выход из группы). /// Колбэк, который вызывается, когда чат нужно убрать из списка (удаление / выход из группы).
final VoidCallback? onChatRemoved; final VoidCallback? onChatRemoved;
final bool isGroupChat; final bool isGroupChat;
@@ -382,7 +384,8 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} else if (_actualMyId == null) { } else if (_actualMyId == null) {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
_actualMyId = int.parse(await prefs.getString('userId')!);; _actualMyId = int.parse(await prefs.getString('userId')!);
;
print( print(
'⚠️ [_initializeChat] ID не найден, используется из виджета: $_actualMyId', '⚠️ [_initializeChat] ID не найден, используется из виджета: $_actualMyId',
); );
@@ -1235,7 +1238,6 @@ class _ChatScreenState extends State<ChatScreen> {
} }
void _testSlideAnimation() { void _testSlideAnimation() {
final myMessage = Message( final myMessage = Message(
id: 'test_my_${DateTime.now().millisecondsSinceEpoch}', id: 'test_my_${DateTime.now().millisecondsSinceEpoch}',
text: 'Тест моё сообщение (должно выехать справа)', text: 'Тест моё сообщение (должно выехать справа)',
@@ -1789,8 +1791,9 @@ class _ChatScreenState extends State<ChatScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Ошибка очистки истории: $e'), content: Text('Ошибка очистки истории: $e'),
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.error, context,
).colorScheme.error,
), ),
); );
} }
@@ -1889,8 +1892,9 @@ class _ChatScreenState extends State<ChatScreen> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Ошибка удаления чата: $e'), content: Text('Ошибка удаления чата: $e'),
backgroundColor: backgroundColor: Theme.of(
Theme.of(context).colorScheme.error, context,
).colorScheme.error,
), ),
); );
} }
@@ -2522,7 +2526,10 @@ class _ChatScreenState extends State<ChatScreen> {
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
right: 16, right: 16,
bottom: MediaQuery.of(context).viewInsets.bottom + 100, bottom:
MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).padding.bottom +
100,
child: AnimatedScale( child: AnimatedScale(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack, curve: Curves.easeOutBack,
@@ -2551,7 +2558,10 @@ class _ChatScreenState extends State<ChatScreen> {
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
left: 8, left: 8,
right: 8, right: 8,
bottom: MediaQuery.of(context).viewInsets.bottom + 12, bottom:
MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).padding.bottom +
12,
child: _buildTextInput(), child: _buildTextInput(),
), ),
], ],

View File

@@ -40,6 +40,9 @@ class MusicTrack {
final token = attach['token'] as String?; final token = attach['token'] as String?;
final name = attach['name'] as String? ?? 'Unknown'; final name = attach['name'] as String? ?? 'Unknown';
final durationSeconds = preview?['duration'] as int?;
final duration = durationSeconds != null ? durationSeconds * 1000 : null;
return MusicTrack( return MusicTrack(
id: id:
fileId?.toString() ?? fileId?.toString() ??
@@ -48,7 +51,7 @@ class MusicTrack {
artist: preview?['artistName'] as String? ?? 'Unknown Artist', artist: preview?['artistName'] as String? ?? 'Unknown Artist',
album: preview?['albumName'] as String?, album: preview?['albumName'] as String?,
albumArtUrl: preview?['baseUrl'] as String?, albumArtUrl: preview?['baseUrl'] as String?,
duration: preview?['duration'] as int?, duration: duration,
fileId: fileId, fileId: fileId,
token: token, token: token,
); );

View File

@@ -43,7 +43,9 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
BottomSheetMusicPlayer.isExpandedNotifier.addListener(_onExpandedChanged); BottomSheetMusicPlayer.isExpandedNotifier.addListener(_onExpandedChanged);
BottomSheetMusicPlayer.isFullscreenNotifier.addListener(_onFullscreenChanged); BottomSheetMusicPlayer.isFullscreenNotifier.addListener(
_onFullscreenChanged,
);
} }
void _onExpandedChanged() { void _onExpandedChanged() {
@@ -63,7 +65,8 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
} }
void _onFullscreenChanged() { void _onFullscreenChanged() {
final shouldBeFullscreen = BottomSheetMusicPlayer.isFullscreenNotifier.value; final shouldBeFullscreen =
BottomSheetMusicPlayer.isFullscreenNotifier.value;
if (shouldBeFullscreen && _currentState != _PlayerState.fullscreen) { if (shouldBeFullscreen && _currentState != _PlayerState.fullscreen) {
setState(() { setState(() {
_currentState = _PlayerState.fullscreen; _currentState = _PlayerState.fullscreen;
@@ -175,7 +178,8 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
} }
// Interpolate between collapsed and target height // Interpolate between collapsed and target height
final currentHeight = collapsedHeight + final currentHeight =
collapsedHeight +
(targetHeight - collapsedHeight) * _heightAnimation.value; (targetHeight - collapsedHeight) * _heightAnimation.value;
return Material( return Material(
@@ -251,7 +255,8 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
return FadeTransition( return FadeTransition(
opacity: animation, opacity: animation,
child: SlideTransition( child: SlideTransition(
position: Tween<Offset>( position:
Tween<Offset>(
begin: const Offset(0.0, 0.05), begin: const Offset(0.0, 0.05),
end: Offset.zero, end: Offset.zero,
).animate( ).animate(
@@ -285,7 +290,7 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
onTap: _toggleExpand, onTap: _toggleExpand,
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row( child: Row(
children: [ children: [
Hero( Hero(
@@ -300,9 +305,12 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
? Image.network( ? Image.network(
track.albumArtUrl!, track.albumArtUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) { loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return _buildAlbumArtPlaceholder(colorScheme); return _buildAlbumArtPlaceholder(
colorScheme,
);
}, },
errorBuilder: (context, error, stackTrace) => errorBuilder: (context, error, stackTrace) =>
_buildAlbumArtPlaceholder(colorScheme), _buildAlbumArtPlaceholder(colorScheme),
@@ -318,25 +326,31 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Flexible(
child: Text(
track.title, track.title,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium
?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
letterSpacing: -0.2, letterSpacing: -0.2,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 6), ),
Text( const SizedBox(height: 3),
Flexible(
child: Text(
track.artist, track.artist,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: colorScheme.onSurface.withOpacity(0.65), color: colorScheme.onSurface.withOpacity(0.65),
fontSize: 13, fontSize: 13,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
),
], ],
), ),
), ),
@@ -371,7 +385,9 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
), ),
) )
: Icon( : Icon(
musicPlayer.isPlaying ? Icons.pause : Icons.play_arrow, musicPlayer.isPlaying
? Icons.pause
: Icons.play_arrow,
size: 26, size: 26,
color: colorScheme.onPrimary, color: colorScheme.onPrimary,
), ),
@@ -486,8 +502,7 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
// Track info // Track info
Text( Text(
track.title, track.title,
style: Theme.of(context).textTheme.headlineMedium style: Theme.of(context).textTheme.headlineMedium?.copyWith(
?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
letterSpacing: -0.5, letterSpacing: -0.5,
), ),
@@ -506,7 +521,7 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
if (track.album != null) ...[ if (track.album != null && track.album!.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
track.album!, track.album!,
@@ -518,6 +533,17 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
if (track.duration != null) ...[
const SizedBox(height: 8),
Text(
_formatDuration(Duration(milliseconds: track.duration!)),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface.withOpacity(0.55),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
const SizedBox(height: 40), const SizedBox(height: 40),
// Progress slider // Progress slider
Column( Column(
@@ -525,11 +551,9 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
SliderTheme( SliderTheme(
data: SliderTheme.of(context).copyWith( data: SliderTheme.of(context).copyWith(
activeTrackColor: colorScheme.primary, activeTrackColor: colorScheme.primary,
inactiveTrackColor: inactiveTrackColor: colorScheme.surfaceContainerHigh,
colorScheme.surfaceContainerHigh,
thumbColor: colorScheme.primary, thumbColor: colorScheme.primary,
overlayColor: overlayColor: colorScheme.primary.withOpacity(0.1),
colorScheme.primary.withOpacity(0.1),
thumbShape: const RoundSliderThumbShape( thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8, enabledThumbRadius: 8,
), ),
@@ -544,8 +568,8 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
onChanged: (value) { onChanged: (value) {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
final newPosition = Duration( final newPosition = Duration(
milliseconds: (value * milliseconds:
musicPlayer.duration.inMilliseconds) (value * musicPlayer.duration.inMilliseconds)
.round(), .round(),
); );
musicPlayer.seek(newPosition); musicPlayer.seek(newPosition);
@@ -559,24 +583,22 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
children: [ children: [
Text( Text(
_formatDuration(musicPlayer.position), _formatDuration(musicPlayer.position),
style: Theme.of(context) style: Theme.of(context).textTheme.bodyMedium
.textTheme
.bodyMedium
?.copyWith( ?.copyWith(
color: colorScheme.onSurface color: colorScheme.onSurface.withOpacity(
.withOpacity(0.7), 0.7,
),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 13, fontSize: 13,
), ),
), ),
Text( Text(
_formatDuration(musicPlayer.duration), _formatDuration(musicPlayer.duration),
style: Theme.of(context) style: Theme.of(context).textTheme.bodyMedium
.textTheme
.bodyMedium
?.copyWith( ?.copyWith(
color: colorScheme.onSurface color: colorScheme.onSurface.withOpacity(
.withOpacity(0.7), 0.7,
),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 13, fontSize: 13,
), ),
@@ -639,8 +661,7 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 3, strokeWidth: 3,
valueColor: valueColor: AlwaysStoppedAnimation<Color>(
AlwaysStoppedAnimation<Color>(
colorScheme.onPrimary, colorScheme.onPrimary,
), ),
), ),
@@ -681,13 +702,10 @@ class _BottomSheetMusicPlayerState extends State<BottomSheetMusicPlayer>
), ),
], ],
), ),
SizedBox( SizedBox(height: MediaQuery.of(context).padding.bottom + 24),
height: MediaQuery.of(context).padding.bottom + 24,
),
], ],
), ),
), ),
), ),
], ],
), ),

View File

@@ -30,7 +30,8 @@ import 'package:platform_info/platform_info.dart';
bool _currentIsDark = false; bool _currentIsDark = false;
bool isMobile = Platform.instance.operatingSystem.iOS || bool isMobile =
Platform.instance.operatingSystem.iOS ||
Platform.instance.operatingSystem.android; Platform.instance.operatingSystem.android;
enum MessageReadStatus { enum MessageReadStatus {
@@ -1044,9 +1045,7 @@ class ChatMessageBubble extends StatelessWidget {
// чтобы визуально не было "бабла" вокруг карточек файлов. // чтобы визуально не было "бабла" вокруг карточек файлов.
BoxDecoration bubbleDecoration; BoxDecoration bubbleDecoration;
if (isFileOnly) { if (isFileOnly) {
bubbleDecoration = const BoxDecoration( bubbleDecoration = const BoxDecoration(color: Colors.transparent);
color: Colors.transparent,
);
} else { } else {
bubbleDecoration = _createBubbleDecoration( bubbleDecoration = _createBubbleDecoration(
bubbleColor, bubbleColor,
@@ -1125,7 +1124,6 @@ class ChatMessageBubble extends StatelessWidget {
child: messageContent, child: messageContent,
); );
} }
} }
return Column( return Column(
@@ -2406,11 +2404,11 @@ class ChatMessageBubble extends StatelessWidget {
final artist = preview?['artistName'] as String? ?? 'Unknown Artist'; final artist = preview?['artistName'] as String? ?? 'Unknown Artist';
final album = preview?['albumName'] as String?; final album = preview?['albumName'] as String?;
final albumArtUrl = preview?['baseUrl'] as String?; final albumArtUrl = preview?['baseUrl'] as String?;
final duration = preview?['duration'] as int?; final durationSeconds = preview?['duration'] as int?;
final duration = durationSeconds != null ? durationSeconds * 1000 : null;
String durationText = ''; String durationText = '';
if (duration != null) { if (durationSeconds != null) {
final durationSeconds = (duration / 1000).round();
final minutes = durationSeconds ~/ 60; final minutes = durationSeconds ~/ 60;
final seconds = durationSeconds % 60; final seconds = durationSeconds % 60;
durationText = '$minutes:${seconds.toString().padLeft(2, '0')}'; durationText = '$minutes:${seconds.toString().padLeft(2, '0')}';
@@ -2441,7 +2439,14 @@ class ChatMessageBubble extends StatelessWidget {
} }
if (!isDownloaded) { if (!isDownloaded) {
await _handleFileDownload(context, fileId, token, fileName, chatId); await _handleFileDownload(
context,
fileId,
token,
fileName,
chatId,
preview: preview,
);
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
if (fileIdString != null) { if (fileIdString != null) {
final updatedFileIdMap = final updatedFileIdMap =
@@ -2708,6 +2713,7 @@ class ChatMessageBubble extends StatelessWidget {
token, token,
fileName, fileName,
chatId, chatId,
preview: preview,
); );
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
final updatedFileIdMap = final updatedFileIdMap =
@@ -2906,8 +2912,9 @@ class ChatMessageBubble extends StatelessWidget {
int? fileId, int? fileId,
String? token, String? token,
String fileName, String fileName,
int? chatId, int? chatId, {
) async { Map<String, dynamic>? preview,
}) async {
// 1. Проверяем fileId, он нужен в любом случае // 1. Проверяем fileId, он нужен в любом случае
if (fileId == null) { if (fileId == null) {
if (context.mounted) { if (context.mounted) {
@@ -3048,7 +3055,16 @@ class ChatMessageBubble extends StatelessWidget {
} }
// Download file to Downloads folder with progress // Download file to Downloads folder with progress
await _downloadFile(downloadUrl, fileName, fileId.toString(), context); await _downloadFile(
downloadUrl,
fileName,
fileId.toString(),
context,
preview: preview,
fileIdInt: fileId,
token: token,
chatId: chatId,
);
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -3065,10 +3081,23 @@ class ChatMessageBubble extends StatelessWidget {
String url, String url,
String fileName, String fileName,
String fileId, String fileId,
BuildContext context, BuildContext context, {
) async { Map<String, dynamic>? preview,
int? fileIdInt,
String? token,
int? chatId,
}) async {
// Download in background without blocking dialog // Download in background without blocking dialog
_startBackgroundDownload(url, fileName, fileId, context); _startBackgroundDownload(
url,
fileName,
fileId,
context,
preview: preview,
fileIdInt: fileIdInt,
token: token,
chatId: chatId,
);
// Show immediate success snackbar // Show immediate success snackbar
if (context.mounted) { if (context.mounted) {
@@ -3085,8 +3114,12 @@ class ChatMessageBubble extends StatelessWidget {
String url, String url,
String fileName, String fileName,
String fileId, String fileId,
BuildContext context, BuildContext context, {
) async { Map<String, dynamic>? preview,
int? fileIdInt,
String? token,
int? chatId,
}) async {
// Initialize progress // Initialize progress
FileDownloadProgressService().updateProgress(fileId, 0.0); FileDownloadProgressService().updateProgress(fileId, 0.0);
@@ -3167,6 +3200,40 @@ class ChatMessageBubble extends StatelessWidget {
await prefs.setStringList('file_id_to_path_map', fileIdMap); await prefs.setStringList('file_id_to_path_map', fileIdMap);
} }
// Save music metadata if preview is available and file is a music file
if (preview != null && fileIdInt != null) {
final extension = fileName.split('.').last.toLowerCase();
if (['mp3', 'wav', 'flac', 'm4a', 'aac', 'ogg'].contains(extension)) {
final title = preview['title'] as String? ?? fileName;
final artist = preview['artistName'] as String? ?? 'Unknown Artist';
final album = preview['albumName'] as String?;
final albumArtUrl = preview['baseUrl'] as String?;
final durationSeconds = preview['duration'] as int?;
final duration = durationSeconds != null
? durationSeconds * 1000
: null;
final track = MusicTrack(
id: fileId,
title: title,
artist: artist,
album: album,
albumArtUrl: albumArtUrl,
duration: duration,
filePath: file.path,
fileId: fileIdInt,
token: token,
chatId: chatId,
);
final musicMetadataJson = prefs.getString('music_metadata') ?? '{}';
final musicMetadata =
jsonDecode(musicMetadataJson) as Map<String, dynamic>;
musicMetadata[fileId] = track.toJson();
await prefs.setString('music_metadata', jsonEncode(musicMetadata));
}
}
// Show success message // Show success message
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -3780,18 +3847,14 @@ class ChatMessageBubble extends StatelessWidget {
Linkify( Linkify(
text: text:
'Привет! Это твои избранные. Все написанное сюда попадёт прямиком к дяде Майору.', 'Привет! Это твои избранные. Все написанное сюда попадёт прямиком к дяде Майору.',
style: style: TextStyle(color: textColor, fontStyle: FontStyle.italic),
TextStyle(color: textColor, fontStyle: FontStyle.italic),
linkStyle: linkStyle, linkStyle: linkStyle,
onOpen: onOpenLink, onOpen: onOpenLink,
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.color_"))
_buildKometColorRichText( _buildKometColorRichText(message.text, defaultTextStyle)
message.text,
defaultTextStyle,
)
else else
Linkify( Linkify(
text: message.text, text: message.text,
@@ -3921,10 +3984,7 @@ class ChatMessageBubble extends StatelessWidget {
/// Строит раскрашенный текст на основе синтаксиса komet.color_#HEX'текст'. /// Строит раскрашенный текст на основе синтаксиса komet.color_#HEX'текст'.
/// Если цвет некорректный, используется красный. /// Если цвет некорректный, используется красный.
Widget _buildKometColorRichText( Widget _buildKometColorRichText(String rawText, TextStyle baseStyle) {
String rawText,
TextStyle baseStyle,
) {
final segments = _parseKometColorSegments(rawText, baseStyle.color); final segments = _parseKometColorSegments(rawText, baseStyle.color);
return RichText( return RichText(
@@ -3956,25 +4016,19 @@ class ChatMessageBubble extends StatelessWidget {
while (index < text.length) { while (index < text.length) {
final start = text.indexOf(marker, index); final start = text.indexOf(marker, index);
if (start == -1) { if (start == -1) {
segments.add( segments.add(_KometColoredSegment(text.substring(index), null));
_KometColoredSegment(text.substring(index), null),
);
break; break;
} }
if (start > index) { if (start > index) {
segments.add( segments.add(_KometColoredSegment(text.substring(index, start), null));
_KometColoredSegment(text.substring(index, start), null),
);
} }
final colorStart = start + marker.length; final colorStart = start + marker.length;
final firstQuote = text.indexOf("'", colorStart); final firstQuote = text.indexOf("'", colorStart);
if (firstQuote == -1) { if (firstQuote == -1) {
// Кривой синтаксис — считаем всё остальное обычным текстом. // Кривой синтаксис — считаем всё остальное обычным текстом.
segments.add( segments.add(_KometColoredSegment(text.substring(start), null));
_KometColoredSegment(text.substring(start), null),
);
break; break;
} }
@@ -3982,9 +4036,7 @@ class ChatMessageBubble extends StatelessWidget {
final textStart = firstQuote + 1; final textStart = firstQuote + 1;
final secondQuote = text.indexOf("'", textStart); final secondQuote = text.indexOf("'", textStart);
if (secondQuote == -1) { if (secondQuote == -1) {
segments.add( segments.add(_KometColoredSegment(text.substring(start), null));
_KometColoredSegment(text.substring(start), null),
);
break; break;
} }
@@ -5254,7 +5306,7 @@ class _AudioPlayerWidgetState extends State<_AudioPlayerWidget> {
}); });
_audioPlayer.durationStream.listen((duration) { _audioPlayer.durationStream.listen((duration) {
if (mounted && duration != null) { if (mounted && duration != null && duration.inMilliseconds > 0) {
setState(() { setState(() {
_totalDuration = duration; _totalDuration = duration;
}); });
@@ -5515,7 +5567,9 @@ class _AudioPlayerWidgetState extends State<_AudioPlayerWidget> {
), ),
), ),
Text( Text(
widget.durationText, _totalDuration.inMilliseconds > 0
? _formatDuration(_totalDuration)
: widget.durationText,
style: TextStyle( style: TextStyle(
color: widget.textColor.withOpacity( color: widget.textColor.withOpacity(
0.7 * widget.messageTextOpacity, 0.7 * widget.messageTextOpacity,