mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-22 21:09:21 +00:00
Show message files
This commit is contained in:
parent
2989e98c50
commit
6c00e0bcab
46
lib/helpers/conversation_files_helper.dart
Normal file
46
lib/helpers/conversation_files_helper.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Conversation files helper
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class ConversationFilesHelper {
|
||||||
|
/// Get the path for chat file
|
||||||
|
static Future<File> getPathForChatFile(
|
||||||
|
int msgID, ConversationMessageFile fileInfo) async {
|
||||||
|
Directory basePath = await getTemporaryDirectory();
|
||||||
|
|
||||||
|
final storageDir = path.join(basePath.absolute.path, "conversation-files");
|
||||||
|
final fileName = "$msgID";
|
||||||
|
|
||||||
|
return File(path.join(storageDir, fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download chat file
|
||||||
|
static Future<void> download({
|
||||||
|
@required int msgID,
|
||||||
|
@required ConversationMessageFile fileInfo,
|
||||||
|
@required Function(double) onProgress,
|
||||||
|
@required CancelToken cancelToken,
|
||||||
|
}) async {
|
||||||
|
final target = await getPathForChatFile(msgID, fileInfo);
|
||||||
|
|
||||||
|
// Create parent directory if required
|
||||||
|
if (!await target.parent.exists()) {
|
||||||
|
await target.parent.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Dio().download(
|
||||||
|
fileInfo.url,
|
||||||
|
target.path,
|
||||||
|
cancelToken: cancelToken,
|
||||||
|
onReceiveProgress: (p, t) => onProgress(p / fileInfo.size.toDouble()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,8 @@ class Conversation extends SerializableElement<Conversation> {
|
|||||||
lastActivity = map["lastActivity"],
|
lastActivity = map["lastActivity"],
|
||||||
members = map["members"]
|
members = map["members"]
|
||||||
.map((el) => ConversationMember.fromJSON(el))
|
.map((el) => ConversationMember.fromJSON(el))
|
||||||
.toList(),
|
.toList()
|
||||||
|
.cast<ConversationMember>(),
|
||||||
canEveryoneAddMembers = map["canEveryoneAddMembers"],
|
canEveryoneAddMembers = map["canEveryoneAddMembers"],
|
||||||
|
|
||||||
// By default, we can not do any call
|
// By default, we can not do any call
|
||||||
|
@ -3,12 +3,33 @@ import 'package:comunic/lists/users_list.dart';
|
|||||||
import 'package:comunic/models/displayed_content.dart';
|
import 'package:comunic/models/displayed_content.dart';
|
||||||
import 'package:comunic/utils/account_utils.dart' as account;
|
import 'package:comunic/utils/account_utils.dart' as account;
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
/// Single conversation message
|
/// Single conversation message
|
||||||
///
|
///
|
||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
enum ConversationMessageFileType {
|
||||||
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
AUDIO,
|
||||||
|
PDF,
|
||||||
|
ZIP,
|
||||||
|
OTHER,
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ConversationFileMimeTypeMapping = {
|
||||||
|
"image/jpeg": ConversationMessageFileType.IMAGE,
|
||||||
|
"image/png": ConversationMessageFileType.IMAGE,
|
||||||
|
"image/gif": ConversationMessageFileType.IMAGE,
|
||||||
|
"video/mp4": ConversationMessageFileType.VIDEO,
|
||||||
|
"audio/m4a": ConversationMessageFileType.AUDIO,
|
||||||
|
"audio/mpeg": ConversationMessageFileType.AUDIO,
|
||||||
|
"application/pdf": ConversationMessageFileType.PDF,
|
||||||
|
"application/zip": ConversationMessageFileType.ZIP,
|
||||||
|
};
|
||||||
|
|
||||||
class ConversationMessageFile {
|
class ConversationMessageFile {
|
||||||
final String url;
|
final String url;
|
||||||
final int size;
|
final int size;
|
||||||
@ -27,6 +48,38 @@ class ConversationMessageFile {
|
|||||||
assert(name != null),
|
assert(name != null),
|
||||||
assert(type != null);
|
assert(type != null);
|
||||||
|
|
||||||
|
/// Get the type of file
|
||||||
|
ConversationMessageFileType get fileType {
|
||||||
|
if (type != null && _ConversationFileMimeTypeMapping.containsKey(type))
|
||||||
|
return _ConversationFileMimeTypeMapping[type];
|
||||||
|
else
|
||||||
|
return ConversationMessageFileType.OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the icon associated with file type
|
||||||
|
IconData get icon {
|
||||||
|
switch (fileType) {
|
||||||
|
case ConversationMessageFileType.IMAGE:
|
||||||
|
return Icons.image;
|
||||||
|
case ConversationMessageFileType.VIDEO:
|
||||||
|
return Icons.video_library;
|
||||||
|
case ConversationMessageFileType.AUDIO:
|
||||||
|
return Icons.audiotrack;
|
||||||
|
case ConversationMessageFileType.PDF:
|
||||||
|
return Icons.picture_as_pdf;
|
||||||
|
|
||||||
|
case ConversationMessageFileType.ZIP:
|
||||||
|
return Icons.archive;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Icons.insert_drive_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasThumbnail => thumbnail != null;
|
||||||
|
|
||||||
|
bool get downloadable => fileType == ConversationMessageFileType.AUDIO;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"url": url,
|
"url": url,
|
||||||
"size": size,
|
"size": size,
|
||||||
@ -165,10 +218,6 @@ class ConversationMessage extends SerializableElement<ConversationMessage> {
|
|||||||
|
|
||||||
bool get hasFile => file != null;
|
bool get hasFile => file != null;
|
||||||
|
|
||||||
bool get hasThumbnail => hasFile && file.thumbnail != null;
|
|
||||||
|
|
||||||
bool get hasImage => hasFile && file.type.startsWith("image/");
|
|
||||||
|
|
||||||
bool get isOwner => account.userID() == userID;
|
bool get isOwner => account.userID() == userID;
|
||||||
|
|
||||||
bool get isServerMessage => serverMessage != null;
|
bool get isServerMessage => serverMessage != null;
|
||||||
@ -203,6 +252,10 @@ class ConversationMessage extends SerializableElement<ConversationMessage> {
|
|||||||
userID = map["userID"],
|
userID = map["userID"],
|
||||||
timeSent = map["timeSent"],
|
timeSent = map["timeSent"],
|
||||||
message = DisplayedString(map["message"]),
|
message = DisplayedString(map["message"]),
|
||||||
file = map["file"],
|
file = map["file"] == null
|
||||||
serverMessage = map["serverMessage"];
|
? null
|
||||||
|
: ConversationMessageFile.fromJson(map["file"]),
|
||||||
|
serverMessage = map["serverMessage"] == null
|
||||||
|
? null
|
||||||
|
: ConversationServerMessage.fromJson(map["serverMessage"]);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
/// Full screen image details
|
/// Full screen image details
|
||||||
///
|
///
|
||||||
@ -22,6 +23,10 @@ class _FullScreenImageRouteState extends State<FullScreenImageRoute> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(tr("Image")),
|
title: Text(tr("Image")),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.launch), onPressed: () => launch(widget.url))
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: PhotoView(imageProvider: CachedNetworkImageProvider(widget.url)),
|
body: PhotoView(imageProvider: CachedNetworkImageProvider(widget.url)),
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:comunic/models/conversation_message.dart';
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
import 'package:comunic/models/user.dart';
|
import 'package:comunic/models/user.dart';
|
||||||
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
import 'package:comunic/ui/widgets/account_image_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/conversation_file_tile.dart';
|
||||||
import 'package:comunic/ui/widgets/text_widget.dart';
|
import 'package:comunic/ui/widgets/text_widget.dart';
|
||||||
import 'package:comunic/utils/date_utils.dart';
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
@ -70,19 +71,10 @@ class ConversationMessageTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build widget image
|
/// Build widget image
|
||||||
Widget _buildMessageImage(BuildContext context) {
|
Widget _buildMessageFile(BuildContext context) => ConversationFileWidget(
|
||||||
return Text("");
|
messageID: message.id,
|
||||||
// TODO : fix file
|
file: message.file,
|
||||||
/*return Container(
|
);
|
||||||
margin: EdgeInsets.only(bottom: 2),
|
|
||||||
child: NetworkImageWidget(
|
|
||||||
url: message.imageURL,
|
|
||||||
allowFullScreen: true,
|
|
||||||
width: 200,
|
|
||||||
height: 200,
|
|
||||||
),
|
|
||||||
);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build message date
|
/// Build message date
|
||||||
Widget _buildMessageDate() {
|
Widget _buildMessageDate() {
|
||||||
@ -113,9 +105,8 @@ class ConversationMessageTile extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Text image
|
// Text image
|
||||||
Container(
|
Container(
|
||||||
child: message.hasImage
|
child:
|
||||||
? _buildMessageImage(context)
|
message.hasFile ? _buildMessageFile(context) : null,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Text message
|
// Text message
|
||||||
@ -190,8 +181,7 @@ class ConversationMessageTile extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Text image
|
// Text image
|
||||||
Container(
|
Container(
|
||||||
child:
|
child: message.hasFile ? _buildMessageFile(context) : null,
|
||||||
message.hasImage ? _buildMessageImage(context) : null,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Text message
|
// Text message
|
||||||
|
135
lib/ui/widgets/audio_player_widget.dart
Normal file
135
lib/ui/widgets/audio_player_widget.dart
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
|
import 'package:comunic/utils/date_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Audio player widget
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class AudioPlayerWidget extends StatefulWidget {
|
||||||
|
final File file;
|
||||||
|
|
||||||
|
const AudioPlayerWidget(this.file);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AudioPlayerWidgetState createState() => _AudioPlayerWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioPlayerWidgetState extends State<AudioPlayerWidget> {
|
||||||
|
AudioPlayer _player;
|
||||||
|
|
||||||
|
Duration _mediaDuration;
|
||||||
|
Duration _mediaPosition;
|
||||||
|
|
||||||
|
double get _max => _mediaDuration?.inMilliseconds?.toDouble() ?? 0.0;
|
||||||
|
|
||||||
|
double get _value => _mediaPosition?.inMilliseconds?.toDouble() ?? 0.0;
|
||||||
|
|
||||||
|
bool get _playing =>
|
||||||
|
_player != null && _player.state == AudioPlayerState.PLAYING;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
|
||||||
|
if (_player != null) _player.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
textStyle: TextStyle(color: Colors.white),
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Spacer(),
|
||||||
|
Icon(Icons.audiotrack, color: Colors.white),
|
||||||
|
Spacer(),
|
||||||
|
Slider(
|
||||||
|
value: _value,
|
||||||
|
onChanged: (newValue) =>
|
||||||
|
_player.seek(Duration(milliseconds: newValue.toInt())),
|
||||||
|
max: _max,
|
||||||
|
activeColor: Colors.white,
|
||||||
|
min: 0,
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Spacer(),
|
||||||
|
Text(formatDuration(_mediaPosition ?? Duration())),
|
||||||
|
Spacer(),
|
||||||
|
_AudioButton(
|
||||||
|
icon: Icons.play_arrow, onTap: _play, visible: !_playing),
|
||||||
|
_AudioButton(icon: Icons.pause, onTap: _pause, visible: _playing),
|
||||||
|
_AudioButton(icon: Icons.stop, onTap: _stop, visible: _playing),
|
||||||
|
Spacer(),
|
||||||
|
Text(formatDuration(_mediaDuration ?? Duration())),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _play() async {
|
||||||
|
if (_player == null) {
|
||||||
|
_player = AudioPlayer();
|
||||||
|
|
||||||
|
_player.onDurationChanged.listen((newDuration) {
|
||||||
|
setState(() => _mediaDuration = newDuration);
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.onAudioPositionChanged.listen((newDuration) {
|
||||||
|
setState(() => _mediaPosition = newDuration);
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.onPlayerStateChanged.listen((event) => setState(() {}));
|
||||||
|
|
||||||
|
_player.onSeekComplete.listen((event) => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_player.state != AudioPlayerState.PAUSED)
|
||||||
|
_player.play(widget.file.absolute.path, isLocal: true);
|
||||||
|
else
|
||||||
|
_player.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pause() async {
|
||||||
|
_player.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stop() {
|
||||||
|
_player.stop();
|
||||||
|
_player.seek(Duration());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final void Function() onTap;
|
||||||
|
final bool visible;
|
||||||
|
|
||||||
|
const _AudioButton({
|
||||||
|
Key key,
|
||||||
|
@required this.icon,
|
||||||
|
@required this.onTap,
|
||||||
|
@required this.visible,
|
||||||
|
}) : assert(icon != null),
|
||||||
|
assert(onTap != null),
|
||||||
|
assert(visible != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!visible) return Container();
|
||||||
|
|
||||||
|
return IconButton(icon: Icon(icon, color: Colors.white), onPressed: onTap);
|
||||||
|
}
|
||||||
|
}
|
200
lib/ui/widgets/conversation_file_tile.dart
Normal file
200
lib/ui/widgets/conversation_file_tile.dart
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/// Chat file tile
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:comunic/helpers/conversation_files_helper.dart';
|
||||||
|
import 'package:comunic/models/conversation_message.dart';
|
||||||
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/audio_player_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/log_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:filesize/filesize.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
const _AreaSize = 150.0;
|
||||||
|
|
||||||
|
class ConversationFileWidget extends StatefulWidget {
|
||||||
|
final int messageID;
|
||||||
|
final ConversationMessageFile file;
|
||||||
|
|
||||||
|
const ConversationFileWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.messageID,
|
||||||
|
@required this.file,
|
||||||
|
}) : assert(messageID != null),
|
||||||
|
assert(file != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ConversationFileWidgetState createState() => _ConversationFileWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConversationFileWidgetState extends State<ConversationFileWidget> {
|
||||||
|
final _refreshKey = GlobalKey<AsyncScreenWidgetState>();
|
||||||
|
|
||||||
|
File _targetFile;
|
||||||
|
|
||||||
|
bool _isDownloaded;
|
||||||
|
|
||||||
|
bool _downloading = false;
|
||||||
|
var _downloadProgress = 0.0;
|
||||||
|
CancelToken _cancelDownloadToken;
|
||||||
|
|
||||||
|
ConversationMessageFile get file => widget.file;
|
||||||
|
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
_targetFile = await ConversationFilesHelper.getPathForChatFile(
|
||||||
|
widget.messageID, file);
|
||||||
|
|
||||||
|
_isDownloaded = await _targetFile.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: _AreaSize,
|
||||||
|
height: _AreaSize,
|
||||||
|
child: AsyncScreenWidget(
|
||||||
|
key: _refreshKey,
|
||||||
|
onReload: _refresh,
|
||||||
|
onBuild: _buildContent,
|
||||||
|
errorMessage: tr("Error!"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() => _isDownloaded || !file.downloadable
|
||||||
|
? _buildFileWidget()
|
||||||
|
: _buildDownloadWidget();
|
||||||
|
|
||||||
|
Widget _buildDownloadWidget() => Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
// Thumbnail, if possible
|
||||||
|
!file.hasThumbnail
|
||||||
|
? Container()
|
||||||
|
: CachedNetworkImage(
|
||||||
|
imageUrl: file.thumbnail,
|
||||||
|
width: _AreaSize,
|
||||||
|
height: _AreaSize,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: _AreaSize,
|
||||||
|
color: Color(0x66000000),
|
||||||
|
child: DefaultTextStyle(
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Spacer(),
|
||||||
|
Icon(file.icon, color: Colors.white),
|
||||||
|
Spacer(),
|
||||||
|
_buildDownloadArea(),
|
||||||
|
Spacer(),
|
||||||
|
Text(filesize(file.size)),
|
||||||
|
Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildDownloadArea() => _downloading
|
||||||
|
? _buildDownloadingWidget()
|
||||||
|
: Material(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(2.0)),
|
||||||
|
color: Colors.green,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.file_download),
|
||||||
|
onPressed: _downloadFile,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildDownloadingWidget() => Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
CircularProgressIndicator(value: _downloadProgress),
|
||||||
|
Center(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _cancelDownloadToken.cancel(),
|
||||||
|
child: Icon(Icons.cancel, color: Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> _downloadFile() async {
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_cancelDownloadToken = CancelToken();
|
||||||
|
_downloading = true;
|
||||||
|
_downloadProgress = 0.0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await ConversationFilesHelper.download(
|
||||||
|
msgID: widget.messageID,
|
||||||
|
fileInfo: file,
|
||||||
|
onProgress: (p) => setState(() => _downloadProgress = p),
|
||||||
|
cancelToken: _cancelDownloadToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _refreshKey.currentState.refresh();
|
||||||
|
} catch (e, s) {
|
||||||
|
logError(e, s);
|
||||||
|
showSimpleSnack(context, tr("Failed to download file!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_downloading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFileWidget() {
|
||||||
|
switch (file.fileType) {
|
||||||
|
// Images
|
||||||
|
case ConversationMessageFileType.IMAGE:
|
||||||
|
return Center(
|
||||||
|
child: NetworkImageWidget(
|
||||||
|
url: file.url,
|
||||||
|
thumbnailURL: file.thumbnail,
|
||||||
|
allowFullScreen: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Audio player
|
||||||
|
case ConversationMessageFileType.AUDIO:
|
||||||
|
return AudioPlayerWidget(_targetFile);
|
||||||
|
|
||||||
|
// The file is not downloadable, we open it in the browser
|
||||||
|
default:
|
||||||
|
return Center(
|
||||||
|
child: MaterialButton(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Spacer(flex: 2),
|
||||||
|
Icon(file.icon, color: Colors.white),
|
||||||
|
Spacer(),
|
||||||
|
Text(file.name, textAlign: TextAlign.center),
|
||||||
|
Spacer(flex: 2),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () => launch(file.url),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class NetworkImageWidget extends StatelessWidget {
|
class NetworkImageWidget extends StatelessWidget {
|
||||||
final String url;
|
final String url;
|
||||||
|
final String thumbnailURL;
|
||||||
final bool allowFullScreen;
|
final bool allowFullScreen;
|
||||||
final bool roundedEdges;
|
final bool roundedEdges;
|
||||||
final double width;
|
final double width;
|
||||||
@ -19,6 +20,7 @@ class NetworkImageWidget extends StatelessWidget {
|
|||||||
const NetworkImageWidget({
|
const NetworkImageWidget({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.url,
|
@required this.url,
|
||||||
|
this.thumbnailURL,
|
||||||
this.allowFullScreen = false,
|
this.allowFullScreen = false,
|
||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
@ -42,7 +44,7 @@ class NetworkImageWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: thumbnailURL ?? url,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
@ -39,16 +39,20 @@ String diffTimeToStr(int amount) {
|
|||||||
|
|
||||||
// Years
|
// Years
|
||||||
final years = (amount / (60 * 60 * 24 * 365)).floor();
|
final years = (amount / (60 * 60 * 24 * 365)).floor();
|
||||||
return years == 1 ? tr("1 year") : tr("%years% years",
|
return years == 1
|
||||||
args: {"years": years.toString()});
|
? tr("1 year")
|
||||||
|
: tr("%years% years", args: {"years": years.toString()});
|
||||||
}
|
}
|
||||||
|
|
||||||
String diffTimeFromNowToStr(int date) {
|
String diffTimeFromNowToStr(int date) {
|
||||||
return diffTimeToStr(time() - date);
|
return diffTimeToStr(time() - date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Return properly formatted date and time
|
/// Return properly formatted date and time
|
||||||
String dateTimeToString(DateTime time) {
|
String dateTimeToString(DateTime time) {
|
||||||
return "${time.day}:${time.month}:${time.year} ${time.hour}:${time.minute}";
|
return "${time.day}:${time.month}:${time.year} ${time.hour}:${time.minute}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format a [Duration] in the form "MM:SS"
|
||||||
|
String formatDuration(Duration d) =>
|
||||||
|
"${d.inMinutes < 10 ? "0" + d.inMinutes.toString() : d.inMinutes.toString()}:${d.inSeconds % 60 < 10 ? "0" + (d.inSeconds % 60).toString() : (d.inSeconds % 60).toString()}";
|
||||||
|
11
lib/utils/log_utils.dart
Normal file
11
lib/utils/log_utils.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/// Log utilities
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
void logError(dynamic e, StackTrace s) {
|
||||||
|
if (e is Exception) {
|
||||||
|
print("Exception: $e\n$s");
|
||||||
|
} else {
|
||||||
|
print("Error: $e\n$s");
|
||||||
|
}
|
||||||
|
}
|
14
pubspec.lock
14
pubspec.lock
@ -22,6 +22,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0-nullsafety.1"
|
version: "2.5.0-nullsafety.1"
|
||||||
|
audioplayers:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: audioplayers
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -183,6 +190,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.8"
|
version: "4.2.8"
|
||||||
|
filesize:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: filesize
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -91,8 +91,15 @@ dependencies:
|
|||||||
# Version manager
|
# Version manager
|
||||||
version: ^1.2.0
|
version: ^1.2.0
|
||||||
|
|
||||||
|
# Play audio files
|
||||||
|
audioplayers: ^0.15.1
|
||||||
|
|
||||||
|
# Get path to temporary files
|
||||||
path_provider: ^1.6.27
|
path_provider: ^1.6.27
|
||||||
|
|
||||||
|
# Format file size
|
||||||
|
filesize: ^1.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
Loading…
Reference in New Issue
Block a user