расшифровка гс
This commit is contained in:
@@ -23,6 +23,34 @@ class ContactCache {
|
|||||||
static String? getAvatar(int id) => _avatarCache[id];
|
static String? getAvatar(int id) => _avatarCache[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TranscriptionResult {
|
||||||
|
final int status;
|
||||||
|
final String? text;
|
||||||
|
final String? messageId;
|
||||||
|
final int? chatId;
|
||||||
|
final int? mediaId;
|
||||||
|
|
||||||
|
TranscriptionResult({
|
||||||
|
required this.status,
|
||||||
|
this.text,
|
||||||
|
this.messageId,
|
||||||
|
this.chatId,
|
||||||
|
this.mediaId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranscriptionCache {
|
||||||
|
static final Map<String, TranscriptionResult> _cache = {};
|
||||||
|
|
||||||
|
static void put(String messageId, TranscriptionResult result) {
|
||||||
|
_cache[messageId] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TranscriptionResult? get(String messageId) => _cache[messageId];
|
||||||
|
|
||||||
|
static bool has(String messageId) => _cache.containsKey(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
class CachedMessage {
|
class CachedMessage {
|
||||||
final String id;
|
final String id;
|
||||||
final int accountId;
|
final int accountId;
|
||||||
@@ -247,6 +275,35 @@ class MessagesModule {
|
|||||||
await _api.sendRequest(Opcode.msgSend, payload);
|
await _api.sendRequest(Opcode.msgSend, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<TranscriptionResult> requestTranscription(
|
||||||
|
int chatId,
|
||||||
|
int messageId,
|
||||||
|
int mediaId,
|
||||||
|
) async {
|
||||||
|
final payload = {
|
||||||
|
'chatId': chatId,
|
||||||
|
'messageId': messageId,
|
||||||
|
'mediaId': mediaId,
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await _api.sendRequest(Opcode.audioTranscription, payload);
|
||||||
|
if (!response.isOk) return TranscriptionResult(status: -1);
|
||||||
|
|
||||||
|
final data = response.payload;
|
||||||
|
if (data is! Map) return TranscriptionResult(status: -1);
|
||||||
|
|
||||||
|
final transcriptionStatus = data['transcriptionStatus'] as int? ?? -1;
|
||||||
|
if (transcriptionStatus == 1) {
|
||||||
|
final text = data['transcription'] as String? ?? '';
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return TranscriptionResult(status: 1, text: 'не удалось распознать текст');
|
||||||
|
}
|
||||||
|
return TranscriptionResult(status: 1, text: text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TranscriptionResult(status: transcriptionStatus);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async {
|
Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async {
|
||||||
try {
|
try {
|
||||||
final response = await _api.sendRequest(Opcode.fileDownload, {
|
final response = await _api.sendRequest(Opcode.fileDownload, {
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ abstract class Opcode {
|
|||||||
static const int notifBanners = 292; // Баннеры
|
static const int notifBanners = 292; // Баннеры
|
||||||
static const int notifFolders = 277; // Обновление папок
|
static const int notifFolders = 277; // Обновление папок
|
||||||
|
|
||||||
|
// ── Transcription ───────────────────────────────────────────────────
|
||||||
|
static const int audioTranscription = 202; // Запрос транскрибации аудио
|
||||||
|
static const int transcriptionResult = 293; // Результат транскрибации (push)
|
||||||
|
|
||||||
// ── Misc ───────────────────────────────────────────────────────────
|
// ── Misc ───────────────────────────────────────────────────────────
|
||||||
static const int okToken = 158; // OK-токен
|
static const int okToken = 158; // OK-токен
|
||||||
static const int webAppInitData = 160; // Данные WebApp
|
static const int webAppInitData = 160; // Данные WebApp
|
||||||
@@ -332,6 +336,8 @@ abstract class Opcode {
|
|||||||
notifProfile: 'NOTIF_PROFILE',
|
notifProfile: 'NOTIF_PROFILE',
|
||||||
notifBanners: 'NOTIF_BANNERS',
|
notifBanners: 'NOTIF_BANNERS',
|
||||||
notifFolders: 'NOTIF_FOLDERS',
|
notifFolders: 'NOTIF_FOLDERS',
|
||||||
|
audioTranscription: 'AUDIO_TRANSCRIPTION',
|
||||||
|
transcriptionResult: 'TRANSCRIPTION_RESULT',
|
||||||
okToken: 'OK_TOKEN',
|
okToken: 'OK_TOKEN',
|
||||||
webAppInitData: 'WEB_APP_INIT_DATA',
|
webAppInitData: 'WEB_APP_INIT_DATA',
|
||||||
complain: 'COMPLAIN',
|
complain: 'COMPLAIN',
|
||||||
|
|||||||
@@ -53,8 +53,12 @@ class PacketDispatcher {
|
|||||||
if (packet.cmd == CmdType.ok ||
|
if (packet.cmd == CmdType.ok ||
|
||||||
packet.cmd == CmdType.error ||
|
packet.cmd == CmdType.error ||
|
||||||
packet.cmd == CmdType.notFound) {
|
packet.cmd == CmdType.notFound) {
|
||||||
|
final payloadStr = packet.payload.toString();
|
||||||
|
final displayPayload = packet.opcode == Opcode.login && payloadStr.length > 50
|
||||||
|
? '${payloadStr.substring(0, 50)}...'
|
||||||
|
: payloadStr;
|
||||||
logger.i(
|
logger.i(
|
||||||
'<= {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: ${packet.payload}}',
|
'<= {ver: ${packet.api}, cmd: ${packet.cmd}, seq: ${packet.seq}, opcode: ${packet.opcode}, payload: $displayPayload}',
|
||||||
);
|
);
|
||||||
|
|
||||||
final completer = _pendingRequests.remove(packet.seq);
|
final completer = _pendingRequests.remove(packet.seq);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ void showCustomNotificationOnOverlay(OverlayState overlay, String message) {
|
|||||||
builder: (context) => CustomNotification(message: message),
|
builder: (context) => CustomNotification(message: message),
|
||||||
);
|
);
|
||||||
overlay.insert(entry);
|
overlay.insert(entry);
|
||||||
Future.delayed(const Duration(milliseconds: 1900), () {
|
Future.delayed(const Duration(milliseconds: 2600), () {
|
||||||
entry.remove();
|
entry.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ class _CustomNotificationState extends State<CustomNotification>
|
|||||||
);
|
);
|
||||||
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
Future.delayed(const Duration(milliseconds: 1600), () {
|
Future.delayed(const Duration(milliseconds: 2300), () {
|
||||||
if (mounted) _controller.reverse();
|
if (mounted) _controller.reverse();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,19 +112,22 @@ class MessageBubble extends StatelessWidget {
|
|||||||
return MessageType.text;
|
return MessageType.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// скругление уже смешариков т.е сообщений, те которые isme ? .. Это наши, после : это чужие
|
|
||||||
BorderRadius get _borderRadius {
|
BorderRadius get _borderRadius {
|
||||||
final topRadius = Radius.circular(bubbleBorderRadius);
|
final topRadius = Radius.circular(bubbleBorderRadius);
|
||||||
final bottomRadius = Radius.circular(bubbleBorderRadius);
|
|
||||||
final smallRadius = const Radius.circular(4);
|
final smallRadius = const Radius.circular(4);
|
||||||
|
|
||||||
|
final cornerTL = isMe ? smallRadius : topRadius;
|
||||||
|
final cornerTR = isMe ? topRadius : smallRadius;
|
||||||
|
final cornerBL = isMe ? smallRadius : topRadius;
|
||||||
|
final cornerBR = isMe ? topRadius : smallRadius;
|
||||||
|
|
||||||
if (_hasPhotoWithCaption &&
|
if (_hasPhotoWithCaption &&
|
||||||
(shape == BubbleShape.singleTop ||
|
(shape == BubbleShape.singleTop ||
|
||||||
shape == BubbleShape.singleMiddle ||
|
shape == BubbleShape.singleMiddle ||
|
||||||
shape == BubbleShape.singleBottom)) {
|
shape == BubbleShape.singleBottom)) {
|
||||||
return BorderRadius.only(
|
return BorderRadius.only(
|
||||||
topLeft: topRadius,
|
topLeft: topRadius,
|
||||||
topRight: isMe ? topRadius : topRadius,
|
topRight: topRadius,
|
||||||
bottomLeft: smallRadius,
|
bottomLeft: smallRadius,
|
||||||
bottomRight: smallRadius,
|
bottomRight: smallRadius,
|
||||||
);
|
);
|
||||||
@@ -136,16 +139,16 @@ class MessageBubble extends StatelessWidget {
|
|||||||
return BorderRadius.only(
|
return BorderRadius.only(
|
||||||
topLeft: smallRadius,
|
topLeft: smallRadius,
|
||||||
topRight: smallRadius,
|
topRight: smallRadius,
|
||||||
bottomLeft: isMe ? smallRadius : smallRadius,
|
bottomLeft: smallRadius,
|
||||||
bottomRight: isMe ? smallRadius : bottomRadius,
|
bottomRight: isMe ? smallRadius : topRadius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (shape) {
|
switch (shape) {
|
||||||
case BubbleShape.singleTop:
|
case BubbleShape.singleTop:
|
||||||
return BorderRadius.only(
|
return BorderRadius.only(
|
||||||
topLeft: isMe ? topRadius : smallRadius,
|
topLeft: cornerTL,
|
||||||
topRight: isMe ? smallRadius : topRadius,
|
topRight: cornerTR,
|
||||||
bottomLeft: smallRadius,
|
bottomLeft: smallRadius,
|
||||||
bottomRight: smallRadius,
|
bottomRight: smallRadius,
|
||||||
);
|
);
|
||||||
@@ -153,22 +156,22 @@ class MessageBubble extends StatelessWidget {
|
|||||||
return BorderRadius.only(
|
return BorderRadius.only(
|
||||||
topLeft: smallRadius,
|
topLeft: smallRadius,
|
||||||
topRight: smallRadius,
|
topRight: smallRadius,
|
||||||
bottomLeft: isMe ? topRadius : smallRadius,
|
bottomLeft: cornerBL,
|
||||||
bottomRight: isMe ? smallRadius : topRadius,
|
bottomRight: cornerBR,
|
||||||
);
|
);
|
||||||
case BubbleShape.singleMiddle:
|
case BubbleShape.singleMiddle:
|
||||||
return BorderRadius.only(
|
return BorderRadius.only(
|
||||||
topLeft: topRadius,
|
topLeft: cornerTL,
|
||||||
topRight: topRadius,
|
topRight: cornerTR,
|
||||||
bottomLeft: isMe ? topRadius : smallRadius,
|
bottomLeft: cornerBL,
|
||||||
bottomRight: isMe ? smallRadius : topRadius,
|
bottomRight: cornerBR,
|
||||||
);
|
);
|
||||||
case BubbleShape.groupedMiddle:
|
case BubbleShape.groupedMiddle:
|
||||||
return BorderRadius.only(
|
return BorderRadius.only(
|
||||||
topLeft: isMe ? topRadius : smallRadius,
|
topLeft: cornerTL,
|
||||||
topRight: isMe ? smallRadius : smallRadius,
|
topRight: smallRadius,
|
||||||
bottomLeft: isMe ? topRadius : smallRadius,
|
bottomLeft: cornerBL,
|
||||||
bottomRight: isMe ? smallRadius : smallRadius,
|
bottomRight: smallRadius,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,13 +276,13 @@ class MessageBubble extends StatelessWidget {
|
|||||||
case MessageType.voice:
|
case MessageType.voice:
|
||||||
switch (shape) {
|
switch (shape) {
|
||||||
case BubbleShape.groupedMiddle:
|
case BubbleShape.groupedMiddle:
|
||||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 6);
|
return const EdgeInsets.symmetric(horizontal: 14, vertical: 4);
|
||||||
case BubbleShape.singleTop:
|
case BubbleShape.singleTop:
|
||||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
return const EdgeInsets.symmetric(horizontal: 14, vertical: 6);
|
||||||
case BubbleShape.singleBottom:
|
case BubbleShape.singleBottom:
|
||||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
return const EdgeInsets.symmetric(horizontal: 14, vertical: 6);
|
||||||
case BubbleShape.singleMiddle:
|
case BubbleShape.singleMiddle:
|
||||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
return const EdgeInsets.symmetric(horizontal: 14, vertical: 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
return const EdgeInsets.symmetric(horizontal: 14, vertical: 10);
|
||||||
@@ -1550,23 +1553,47 @@ class MessageBubble extends StatelessWidget {
|
|||||||
final textColor = isMe
|
final textColor = isMe
|
||||||
? Colors.white
|
? Colors.white
|
||||||
: (isDark ? cs.onSurface : const Color(0xFF1C1C1E));
|
: (isDark ? cs.onSurface : const Color(0xFF1C1C1E));
|
||||||
final payload = message.payload;
|
|
||||||
final voice = payload?['voice'] as Map<String, dynamic>?;
|
|
||||||
final duration = voice?['duration'] as int? ?? 0;
|
|
||||||
final url = voice?['url']?.toString() ?? '';
|
|
||||||
|
|
||||||
return Column(
|
int duration = 0;
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
String url = '';
|
||||||
children: [
|
String? waveData;
|
||||||
_VoiceMessageBubble(
|
int? audioId;
|
||||||
duration: duration,
|
|
||||||
url: url,
|
final attaches = message.attachments;
|
||||||
textColor: textColor,
|
if (attaches != null && attaches.isNotEmpty) {
|
||||||
isMe: isMe,
|
for (final a in attaches) {
|
||||||
),
|
if (a is AudioAttachment) {
|
||||||
const SizedBox(height: 6),
|
duration = ((a.duration ?? 0) / 1000).round();
|
||||||
_buildMeta(context),
|
url = a.fileUrl ?? a.baseUrl ?? '';
|
||||||
],
|
waveData = a.waveform;
|
||||||
|
audioId = a.audioId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration == 0 && url.isEmpty) {
|
||||||
|
final payload = message.payload;
|
||||||
|
final voice = payload?['voice'] as Map<String, dynamic>?;
|
||||||
|
duration = ((voice?['duration'] as int? ?? 0) / 1000).round();
|
||||||
|
url = voice?['url']?.toString() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final cachedTranscription = TranscriptionCache.get(message.id);
|
||||||
|
|
||||||
|
return _VoiceMessageBubble(
|
||||||
|
duration: duration,
|
||||||
|
url: url,
|
||||||
|
textColor: textColor,
|
||||||
|
isMe: isMe,
|
||||||
|
status: message.status,
|
||||||
|
time: message.time,
|
||||||
|
cs: cs,
|
||||||
|
waveData: waveData,
|
||||||
|
chatId: message.chatId,
|
||||||
|
messageId: message.id,
|
||||||
|
audioId: audioId,
|
||||||
|
preloadedText: cachedTranscription?.text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1666,12 +1693,28 @@ class _VoiceMessageBubble extends StatefulWidget {
|
|||||||
final String url;
|
final String url;
|
||||||
final Color textColor;
|
final Color textColor;
|
||||||
final bool isMe;
|
final bool isMe;
|
||||||
|
final String? status;
|
||||||
|
final int time;
|
||||||
|
final ColorScheme cs;
|
||||||
|
final String? waveData;
|
||||||
|
final int chatId;
|
||||||
|
final String messageId;
|
||||||
|
final int? audioId;
|
||||||
|
final String? preloadedText;
|
||||||
|
|
||||||
const _VoiceMessageBubble({
|
const _VoiceMessageBubble({
|
||||||
required this.duration,
|
required this.duration,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.textColor,
|
required this.textColor,
|
||||||
required this.isMe,
|
required this.isMe,
|
||||||
|
this.status,
|
||||||
|
required this.time,
|
||||||
|
required this.cs,
|
||||||
|
this.waveData,
|
||||||
|
required this.chatId,
|
||||||
|
required this.messageId,
|
||||||
|
this.audioId,
|
||||||
|
this.preloadedText,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1681,97 +1724,305 @@ class _VoiceMessageBubble extends StatefulWidget {
|
|||||||
class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
double _progress = 0.0;
|
double _progress = 0.0;
|
||||||
|
bool _transcriptionVisible = false;
|
||||||
|
String? _transcriptionText;
|
||||||
|
bool _transcriptionLoading = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
void initState() {
|
||||||
final cs = Theme.of(context).colorScheme;
|
super.initState();
|
||||||
final isDark = cs.brightness == Brightness.dark;
|
if (widget.preloadedText != null) {
|
||||||
|
_transcriptionText = widget.preloadedText;
|
||||||
|
_transcriptionVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
String _formatDuration(int seconds) {
|
||||||
width: 220,
|
final min = seconds ~/ 60;
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
final sec = seconds % 60;
|
||||||
child: Row(
|
return '$min:${sec.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(int timestamp) {
|
||||||
|
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||||
|
final hour = dt.hour.toString().padLeft(2, '0');
|
||||||
|
final minute = dt.minute.toString().padLeft(2, '0');
|
||||||
|
return '$hour:$minute';
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusIcon() {
|
||||||
|
final status = widget.status;
|
||||||
|
IconData icon;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
if (status == null || status == 'sending' || status == 'pending') {
|
||||||
|
icon = Symbols.check;
|
||||||
|
color = Colors.white54;
|
||||||
|
} else {
|
||||||
|
switch (status) {
|
||||||
|
case 'sent':
|
||||||
|
icon = Symbols.check;
|
||||||
|
color = Colors.white54;
|
||||||
|
case 'delivered':
|
||||||
|
icon = Symbols.done_all;
|
||||||
|
color = Colors.white54;
|
||||||
|
case 'read':
|
||||||
|
icon = Symbols.done_all;
|
||||||
|
color = const Color(0xFF34C759);
|
||||||
|
case 'error':
|
||||||
|
icon = Symbols.error;
|
||||||
|
color = Colors.redAccent;
|
||||||
|
default:
|
||||||
|
icon = Symbols.check;
|
||||||
|
color = Colors.white54;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon(icon, size: 14, color: color);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = widget.cs.brightness == Brightness.dark;
|
||||||
|
final waveInactiveColor = widget.isMe
|
||||||
|
? Colors.white.withValues(alpha: 0.35)
|
||||||
|
: (isDark
|
||||||
|
? widget.cs.surfaceContainerHighest
|
||||||
|
: const Color(0xFFD1D1D6));
|
||||||
|
final waveActiveColor = widget.isMe
|
||||||
|
? Colors.white.withValues(alpha: 0.7)
|
||||||
|
: widget.cs.primary;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: 240,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
Row(
|
||||||
onTap: _togglePlay,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Container(
|
children: [
|
||||||
width: 36,
|
GestureDetector(
|
||||||
height: 36,
|
onTap: _togglePlay,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: widget.isMe
|
width: 32,
|
||||||
? Colors.white.withValues(alpha: 0.2)
|
height: 32,
|
||||||
: cs.primaryContainer,
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
color: widget.isMe
|
||||||
|
? Colors.white.withValues(alpha: 0.2)
|
||||||
|
: widget.cs.primaryContainer,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
_isPlaying ? Symbols.pause : Symbols.play_arrow,
|
||||||
|
color: widget.isMe ? Colors.white : widget.cs.primary,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Icon(
|
const SizedBox(width: 10),
|
||||||
_isPlaying ? Symbols.pause : Symbols.play_arrow,
|
Expanded(
|
||||||
color: widget.isMe ? Colors.white : cs.primary,
|
child: LayoutBuilder(
|
||||||
size: 20,
|
builder: (context, constraints) {
|
||||||
),
|
return GestureDetector(
|
||||||
),
|
onTapDown: (details) {
|
||||||
),
|
setState(() {
|
||||||
const SizedBox(width: 10),
|
_progress = (details.localPosition.dx /
|
||||||
Expanded(
|
constraints.maxWidth)
|
||||||
child: Column(
|
.clamp(0.0, 1.0);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
});
|
||||||
children: [
|
},
|
||||||
Stack(
|
onHorizontalDragUpdate: (details) {
|
||||||
children: [
|
setState(() {
|
||||||
Container(
|
_progress = (details.localPosition.dx /
|
||||||
height: 24,
|
constraints.maxWidth)
|
||||||
decoration: BoxDecoration(
|
.clamp(0.0, 1.0);
|
||||||
color: widget.isMe
|
});
|
||||||
? Colors.white.withValues(alpha: 0.2)
|
},
|
||||||
: (isDark
|
|
||||||
? cs.surfaceContainerHighest
|
|
||||||
: const Color(0xFFD1D1D6)),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: _progress.clamp(0.0, 1.0),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 24,
|
height: 4,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: widget.isMe
|
color: waveInactiveColor,
|
||||||
? Colors.white.withValues(alpha: 0.5)
|
|
||||||
: cs.primary,
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
child: FractionallySizedBox(
|
||||||
),
|
alignment: Alignment.centerLeft,
|
||||||
SizedBox(
|
widthFactor: _progress.clamp(0.0, 1.0),
|
||||||
height: 24,
|
child: Container(
|
||||||
child: Center(
|
decoration: BoxDecoration(
|
||||||
child: Text(
|
color: waveActiveColor,
|
||||||
_formatDuration(widget.duration),
|
borderRadius: BorderRadius.circular(2),
|
||||||
style: TextStyle(
|
),
|
||||||
color: widget.textColor.withValues(alpha: 0.8),
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _requestTranscription,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 32,
|
||||||
|
child: Center(
|
||||||
|
child: _transcriptionLoading
|
||||||
|
? SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 1.5,
|
||||||
|
color: widget.textColor.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Т',
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.textColor.withValues(alpha: 0.6),
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatDuration(widget.duration),
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.textColor.withValues(alpha: 0.7),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: _transcriptionVisible
|
||||||
|
? Text(
|
||||||
|
_transcriptionText ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.textColor.withValues(alpha: 0.8),
|
||||||
|
fontSize: 12,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
maxLines: 10,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_transcriptionVisible) ...[
|
||||||
|
Text(
|
||||||
|
_formatTime(widget.time),
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.textColor.withValues(alpha: 0.6),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.isMe) ...[
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
_buildStatusIcon(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_transcriptionVisible) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatTime(widget.time),
|
||||||
|
style: TextStyle(
|
||||||
|
color: widget.textColor.withValues(alpha: 0.6),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.isMe) ...[
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
_buildStatusIcon(),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildProgressBar(Color inactive, Color active) {
|
||||||
|
return Container(
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: inactive,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _togglePlay() {
|
void _togglePlay() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isPlaying = !_isPlaying;
|
_isPlaying = !_isPlaying;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDuration(int seconds) {
|
Future<void> _requestTranscription() async {
|
||||||
final min = seconds ~/ 60;
|
if (widget.audioId == null) return;
|
||||||
final sec = seconds % 60;
|
|
||||||
return '$min:${sec.toString().padLeft(2, '0')}';
|
if (_transcriptionVisible && _transcriptionText != null) {
|
||||||
|
setState(() {
|
||||||
|
_transcriptionVisible = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TranscriptionCache.has(widget.messageId)) {
|
||||||
|
final cached = TranscriptionCache.get(widget.messageId)!;
|
||||||
|
setState(() {
|
||||||
|
_transcriptionText = cached.text ?? 'не удалось распознать текст';
|
||||||
|
_transcriptionVisible = true;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_transcriptionLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await messagesModule.requestTranscription(
|
||||||
|
widget.chatId,
|
||||||
|
int.tryParse(widget.messageId) ?? 0,
|
||||||
|
widget.audioId!,
|
||||||
|
);
|
||||||
|
|
||||||
|
TranscriptionCache.put(widget.messageId, result);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_transcriptionLoading = false;
|
||||||
|
if (result.status == 1) {
|
||||||
|
_transcriptionText = (result.text == null || result.text!.isEmpty)
|
||||||
|
? 'не удалось распознать текст'
|
||||||
|
: result.text;
|
||||||
|
_transcriptionVisible = true;
|
||||||
|
} else if (result.status == 0) {
|
||||||
|
_transcriptionText = 'транскрибация...';
|
||||||
|
_transcriptionVisible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_transcriptionLoading = false;
|
||||||
|
_transcriptionText = 'ошибка транскрибации';
|
||||||
|
_transcriptionVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,14 +198,27 @@ class AudioAttachment extends MessageAttachment {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? waveStr;
|
||||||
|
final waveRaw = map['wave'];
|
||||||
|
if (waveRaw is String) {
|
||||||
|
waveStr = waveRaw;
|
||||||
|
} else if (waveRaw is List) {
|
||||||
|
try {
|
||||||
|
final bytes = List<int>.from(waveRaw);
|
||||||
|
final base64 = String.fromCharCodes(bytes);
|
||||||
|
waveStr = 'data:image/webp;base64,$base64';
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
return AudioAttachment(
|
return AudioAttachment(
|
||||||
previewData: previewStr,
|
previewData: previewStr,
|
||||||
baseUrl: map['baseUrl'] as String?,
|
baseUrl: map['baseUrl']?.toString(),
|
||||||
|
fileUrl: map['url']?.toString(),
|
||||||
audioId: map['audioId'] as int?,
|
audioId: map['audioId'] as int?,
|
||||||
audioToken: map['audioToken'] as String?,
|
audioToken: map['token']?.toString(),
|
||||||
duration: map['duration'] as int?,
|
duration: map['duration'] as int?,
|
||||||
size: map['size'] as int?,
|
size: map['size'] as int?,
|
||||||
waveform: map['waveform'] as String?,
|
waveform: waveStr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user