починил плеер, добавил отступ для панели сообщений (ДИНАМИЧЕСКИЙ СУКА, ТОЛЬКО ПОПРЛБУЙТЕ ВОЗРАЗИТИТЬ)
This commit is contained in:
@@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user