Compare commits
4 Commits
38a73d2890
...
a63205d5e1
Author | SHA1 | Date | |
---|---|---|---|
a63205d5e1 | |||
e7a4e93366 | |||
2c23f40415 | |||
daf4ee79f2 |
@ -96,7 +96,7 @@ class Conversation with _$Conversation {
|
||||
'open': boolToInt(open),
|
||||
'muted': boolToInt(muted),
|
||||
'encrypted': boolToInt(encrypted),
|
||||
'lastMessage': lastMessage?.id,
|
||||
'lastMessageId': lastMessage?.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
_stopComposeTimer();
|
||||
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final r = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
SendMessageCommand(
|
||||
recipients: [state.conversation!.jid],
|
||||
body: state.messageText,
|
||||
@ -190,24 +190,21 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
editId: state.messageEditingId,
|
||||
editSid: state.messageEditingSid,
|
||||
),
|
||||
awaitable: false,
|
||||
);
|
||||
|
||||
if (!state.messageEditing) {
|
||||
final result = r! as events.MessageAddedEvent;
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: List<Message>.from(<Message>[ ...state.messages, result.message ]),
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
showSendButton: false,
|
||||
emojiPickerVisible: false,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
showSendButton: false,
|
||||
emojiPickerVisible: false,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onMessageQuoted(MessageQuotedEvent event, Emitter<ConversationState> emit) async {
|
||||
|
@ -83,7 +83,7 @@ class SharedMediaPage extends StatelessWidget {
|
||||
spacing: 5,
|
||||
runSpacing: 5,
|
||||
children: row.map((medium) {
|
||||
return buildSharedMediaWidget(medium, state.jid);
|
||||
return buildSharedMediaWidget(medium, state.jid);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
|
@ -216,7 +216,7 @@ class ChatBubbleState extends State<ChatBubble>
|
||||
GestureDetector(
|
||||
onLongPressStart: widget.onLongPressed,
|
||||
child: widget.bubble,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
228
lib/ui/widgets/chat/media/audio.dart
Normal file
228
lib/ui/widgets/chat/media/audio.dart
Normal file
@ -0,0 +1,228 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:audiofileplayer/audiofileplayer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/shared/commands.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/bottom.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/downloadbutton.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/base.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/file.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/progress.dart';
|
||||
|
||||
String doubleToTimestamp(double p) {
|
||||
if (p < 60) {
|
||||
return '0:${padInt(p.floor())}';
|
||||
}
|
||||
|
||||
final minutes = (p / 60).floor();
|
||||
final seconds = padInt((p - minutes * 60).floor());
|
||||
return '$minutes:$seconds';
|
||||
}
|
||||
|
||||
enum _AudioPlaybackState {
|
||||
playing,
|
||||
paused,
|
||||
stopped
|
||||
}
|
||||
|
||||
class AudioChatWidget extends StatefulWidget {
|
||||
const AudioChatWidget(
|
||||
this.message,
|
||||
this.radius,
|
||||
this.maxWidth,
|
||||
this.sent,
|
||||
{
|
||||
super.key,
|
||||
}
|
||||
);
|
||||
final Message message;
|
||||
final BorderRadius radius;
|
||||
final double maxWidth;
|
||||
final bool sent;
|
||||
|
||||
@override
|
||||
AudioChatState createState() => AudioChatState();
|
||||
}
|
||||
|
||||
class AudioChatState extends State<AudioChatWidget> {
|
||||
_AudioPlaybackState _playState = _AudioPlaybackState.stopped;
|
||||
double? _duration;
|
||||
double? _position;
|
||||
Audio? _audioFile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
}
|
||||
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
_audioFile = Audio.loadFromAbsolutePath(
|
||||
widget.message.mediaUrl!,
|
||||
onDuration: (double seconds) {
|
||||
setState(() {
|
||||
_duration = seconds;
|
||||
});
|
||||
},
|
||||
onPosition: (double seconds) {
|
||||
setState(() {
|
||||
_position = seconds;
|
||||
});
|
||||
},
|
||||
onComplete: () {
|
||||
setState(() {
|
||||
_playState = _AudioPlaybackState.stopped;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_audioFile?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildUploading() {
|
||||
// TODO(PapaTutuWawa): Fix
|
||||
return FileChatWidget(
|
||||
widget.message,
|
||||
widget.radius,
|
||||
widget.sent,
|
||||
extra: ProgressWidget(id: widget.message.id),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDownloading() {
|
||||
// TODO(PapaTutuWawa): Fix
|
||||
return FileChatBaseWidget(
|
||||
widget.message,
|
||||
Icons.image,
|
||||
widget.message.isFileUploadNotification ?
|
||||
(widget.message.filename ?? '') :
|
||||
filenameFromUrl(widget.message.srcUrl!),
|
||||
widget.radius,
|
||||
widget.sent,
|
||||
extra: ProgressWidget(id: widget.message.id),
|
||||
);
|
||||
}
|
||||
|
||||
/// The audio file exists locally
|
||||
Widget _buildAudio() {
|
||||
return MediaBaseChatWidget(
|
||||
SizedBox(
|
||||
width: widget.maxWidth,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (_playState != _AudioPlaybackState.playing) {
|
||||
if (_playState == _AudioPlaybackState.paused) {
|
||||
_audioFile?.resume();
|
||||
} else if (_playState == _AudioPlaybackState.stopped) {
|
||||
_audioFile?.play();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_playState = _AudioPlaybackState.playing;
|
||||
});
|
||||
} else {
|
||||
_audioFile?.pause();
|
||||
setState(() {
|
||||
_playState = _AudioPlaybackState.paused;
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 4,
|
||||
),
|
||||
child: _playState == _AudioPlaybackState.playing ?
|
||||
const Icon(Icons.pause) :
|
||||
const Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Slider(
|
||||
onChanged: (p) {
|
||||
if (_duration == null || _audioFile == null) return;
|
||||
|
||||
setState(() {
|
||||
_position = p * _duration!;
|
||||
_audioFile!.seek(_position!);
|
||||
});
|
||||
},
|
||||
value: (_position != null && _duration != null) ?
|
||||
(_position! / _duration!).clamp(0, 1) :
|
||||
0,
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: widget.maxWidth - 80,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(doubleToTimestamp(_position ?? 0)),
|
||||
const Spacer(),
|
||||
Text(doubleToTimestamp(_duration ?? 0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MessageBubbleBottom(widget.message, widget.sent),
|
||||
widget.radius,
|
||||
gradient: false,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDownloadable() {
|
||||
return FileChatBaseWidget(
|
||||
widget.message,
|
||||
Icons.image,
|
||||
widget.message.isFileUploadNotification ?
|
||||
(widget.message.filename ?? '') :
|
||||
filenameFromUrl(widget.message.srcUrl!),
|
||||
widget.radius,
|
||||
widget.sent,
|
||||
extra: DownloadButton(
|
||||
onPressed: () {
|
||||
MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
RequestDownloadCommand(message: widget.message),
|
||||
awaitable: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.message.isUploading) return _buildUploading();
|
||||
if (widget.message.isFileUploadNotification || widget.message.isDownloading) return _buildDownloading();
|
||||
|
||||
// TODO(PapaTutuWawa): Maybe use an async builder
|
||||
if (widget.message.mediaUrl != null && File(widget.message.mediaUrl!).existsSync()) return _buildAudio();
|
||||
|
||||
return _buildDownloadable();
|
||||
}
|
||||
}
|
@ -26,30 +26,38 @@ class MediaBaseChatWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: InkResponse(
|
||||
onTap: onTap,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: background,
|
||||
),
|
||||
...gradient ? [BottomGradient(radius)] : [],
|
||||
...extra != null ? [ extra! ] : [],
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3, right: 6),
|
||||
child: bottom,
|
||||
),
|
||||
)
|
||||
],
|
||||
final content = Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: background,
|
||||
),
|
||||
),
|
||||
...gradient ? [BottomGradient(radius)] : [],
|
||||
...extra != null ? [ extra! ] : [],
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 3, right: 6),
|
||||
child: bottom,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
return IntrinsicWidth(
|
||||
child: InkResponse(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return IntrinsicWidth(
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/media.dart';
|
||||
import 'package:moxxyv2/shared/models/message.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/audio.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/file.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/image.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/media/video.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/playbutton.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/quote/base.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/audio.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/file.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/image.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/video.dart';
|
||||
@ -18,7 +20,7 @@ enum MessageType {
|
||||
text,
|
||||
image,
|
||||
video,
|
||||
// audio
|
||||
audio,
|
||||
file
|
||||
}
|
||||
|
||||
@ -33,9 +35,9 @@ MessageType getMessageType(Message message) {
|
||||
return MessageType.image;
|
||||
} else if (mime.startsWith('video/')) {
|
||||
return MessageType.video;
|
||||
} else if (mime.startsWith('audio/')) {
|
||||
return MessageType.audio;
|
||||
}
|
||||
// TODO(Unknown): Implement audio
|
||||
//else if (mime.startswith("audio/")) return MessageType.audio;
|
||||
|
||||
return MessageType.file;
|
||||
}
|
||||
@ -70,8 +72,8 @@ Widget buildMessageWidget(Message message, double maxWidth, BorderRadius radius,
|
||||
case MessageType.video: {
|
||||
return VideoChatWidget(message, radius, maxWidth, sent);
|
||||
}
|
||||
// TODO(Unknown): Implement audio
|
||||
//case MessageType.audio: return buildImageMessageWidget(message);
|
||||
case MessageType.audio:
|
||||
return AudioChatWidget(message, radius, maxWidth, sent);
|
||||
case MessageType.file: {
|
||||
return FileChatWidget(message, radius, sent);
|
||||
}
|
||||
@ -143,7 +145,7 @@ Widget buildQuoteMessageWidget(Message message, bool sent, { void Function()? re
|
||||
resetQuotedMessage: resetQuote,
|
||||
);
|
||||
// TODO(Unknown): Implement audio
|
||||
//case MessageType.audio: return const SizedBox();
|
||||
case MessageType.audio:
|
||||
case MessageType.file:
|
||||
return QuoteBaseWidget(
|
||||
message,
|
||||
@ -197,9 +199,12 @@ Widget buildSharedMediaWidget(SharedMedium medium, String conversationJid) {
|
||||
onTap: () => OpenFile.open(medium.path),
|
||||
child: const PlayButton(size: 32),
|
||||
);
|
||||
} else if (medium.mime!.startsWith('audio/')) {
|
||||
return SharedAudioWidget(
|
||||
medium.path,
|
||||
onTap: () => OpenFile.open(medium.path),
|
||||
);
|
||||
}
|
||||
// TODO(Unknown): Audio
|
||||
//if (message.mime!.startsWith("audio/")) return const SizedBox();
|
||||
|
||||
return SharedFileWidget(medium.path);
|
||||
}
|
||||
|
42
lib/ui/widgets/chat/shared/audio.dart
Normal file
42
lib/ui/widgets/chat/shared/audio.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moxxyv2/ui/widgets/chat/shared/base.dart';
|
||||
|
||||
class SharedAudioWidget extends StatelessWidget {
|
||||
const SharedAudioWidget(
|
||||
this.path, {
|
||||
this.onTap,
|
||||
this.borderColor,
|
||||
this.borderRadius = 10,
|
||||
this.size = sharedMediaContainerDimension,
|
||||
super.key,
|
||||
}
|
||||
);
|
||||
final String path;
|
||||
final Color? borderColor;
|
||||
final void Function()? onTap;
|
||||
final double borderRadius;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SharedMediaContainer(
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
color: Colors.white60,
|
||||
border: borderColor != null ? Border.all(
|
||||
color: borderColor!,
|
||||
width: 4,
|
||||
) : null,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: const Icon(
|
||||
Icons.music_note,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
size: size,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
@ -36,6 +36,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.9.0"
|
||||
audiofileplayer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audiofileplayer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
awesome_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -11,6 +11,7 @@ environment:
|
||||
|
||||
dependencies:
|
||||
archive: 3.3.0
|
||||
audiofileplayer: 2.1.1
|
||||
awesome_notifications: 0.7.4+1
|
||||
badges: 2.0.3
|
||||
better_open_file: 3.6.3
|
||||
|
Loading…
Reference in New Issue
Block a user