mirror of
				https://gitlab.com/comunic/comunicmobile
				synced 2025-11-04 04:04:18 +00:00 
			
		
		
		
	Show message files
This commit is contained in:
		
							
								
								
									
										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"],
 | 
			
		||||
        members = map["members"]
 | 
			
		||||
            .map((el) => ConversationMember.fromJSON(el))
 | 
			
		||||
            .toList(),
 | 
			
		||||
            .toList()
 | 
			
		||||
            .cast<ConversationMember>(),
 | 
			
		||||
        canEveryoneAddMembers = map["canEveryoneAddMembers"],
 | 
			
		||||
 | 
			
		||||
        // 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/utils/account_utils.dart' as account;
 | 
			
		||||
import 'package:comunic/utils/intl_utils.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:meta/meta.dart';
 | 
			
		||||
 | 
			
		||||
/// Single conversation message
 | 
			
		||||
///
 | 
			
		||||
/// @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 {
 | 
			
		||||
  final String url;
 | 
			
		||||
  final int size;
 | 
			
		||||
@@ -27,6 +48,38 @@ class ConversationMessageFile {
 | 
			
		||||
        assert(name != 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() => {
 | 
			
		||||
        "url": url,
 | 
			
		||||
        "size": size,
 | 
			
		||||
@@ -165,10 +218,6 @@ class ConversationMessage extends SerializableElement<ConversationMessage> {
 | 
			
		||||
 | 
			
		||||
  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 isServerMessage => serverMessage != null;
 | 
			
		||||
@@ -203,6 +252,10 @@ class ConversationMessage extends SerializableElement<ConversationMessage> {
 | 
			
		||||
        userID = map["userID"],
 | 
			
		||||
        timeSent = map["timeSent"],
 | 
			
		||||
        message = DisplayedString(map["message"]),
 | 
			
		||||
        file = map["file"],
 | 
			
		||||
        serverMessage = map["serverMessage"];
 | 
			
		||||
        file = map["file"] == null
 | 
			
		||||
            ? 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:flutter/material.dart';
 | 
			
		||||
import 'package:photo_view/photo_view.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher.dart';
 | 
			
		||||
 | 
			
		||||
/// Full screen image details
 | 
			
		||||
///
 | 
			
		||||
@@ -22,6 +23,10 @@ class _FullScreenImageRouteState extends State<FullScreenImageRoute> {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(tr("Image")),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
              icon: Icon(Icons.launch), onPressed: () => launch(widget.url))
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: PhotoView(imageProvider: CachedNetworkImageProvider(widget.url)),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:comunic/models/conversation_message.dart';
 | 
			
		||||
import 'package:comunic/models/user.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/utils/date_utils.dart';
 | 
			
		||||
import 'package:comunic/utils/intl_utils.dart';
 | 
			
		||||
@@ -70,19 +71,10 @@ class ConversationMessageTile extends StatelessWidget {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Build widget image
 | 
			
		||||
  Widget _buildMessageImage(BuildContext context) {
 | 
			
		||||
    return Text("");
 | 
			
		||||
    // TODO : fix file
 | 
			
		||||
    /*return Container(
 | 
			
		||||
      margin: EdgeInsets.only(bottom: 2),
 | 
			
		||||
      child: NetworkImageWidget(
 | 
			
		||||
        url: message.imageURL,
 | 
			
		||||
        allowFullScreen: true,
 | 
			
		||||
        width: 200,
 | 
			
		||||
        height: 200,
 | 
			
		||||
      ),
 | 
			
		||||
    );*/
 | 
			
		||||
  }
 | 
			
		||||
  Widget _buildMessageFile(BuildContext context) => ConversationFileWidget(
 | 
			
		||||
        messageID: message.id,
 | 
			
		||||
        file: message.file,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  /// Build message date
 | 
			
		||||
  Widget _buildMessageDate() {
 | 
			
		||||
@@ -113,9 +105,8 @@ class ConversationMessageTile extends StatelessWidget {
 | 
			
		||||
                    children: <Widget>[
 | 
			
		||||
                      // Text image
 | 
			
		||||
                      Container(
 | 
			
		||||
                        child: message.hasImage
 | 
			
		||||
                            ? _buildMessageImage(context)
 | 
			
		||||
                            : null,
 | 
			
		||||
                        child:
 | 
			
		||||
                            message.hasFile ? _buildMessageFile(context) : null,
 | 
			
		||||
                      ),
 | 
			
		||||
 | 
			
		||||
                      // Text message
 | 
			
		||||
@@ -190,8 +181,7 @@ class ConversationMessageTile extends StatelessWidget {
 | 
			
		||||
                children: <Widget>[
 | 
			
		||||
                  // Text image
 | 
			
		||||
                  Container(
 | 
			
		||||
                    child:
 | 
			
		||||
                        message.hasImage ? _buildMessageImage(context) : null,
 | 
			
		||||
                    child: message.hasFile ? _buildMessageFile(context) : null,
 | 
			
		||||
                  ),
 | 
			
		||||
 | 
			
		||||
                  // 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 {
 | 
			
		||||
  final String url;
 | 
			
		||||
  final String thumbnailURL;
 | 
			
		||||
  final bool allowFullScreen;
 | 
			
		||||
  final bool roundedEdges;
 | 
			
		||||
  final double width;
 | 
			
		||||
@@ -19,6 +20,7 @@ class NetworkImageWidget extends StatelessWidget {
 | 
			
		||||
  const NetworkImageWidget({
 | 
			
		||||
    Key key,
 | 
			
		||||
    @required this.url,
 | 
			
		||||
    this.thumbnailURL,
 | 
			
		||||
    this.allowFullScreen = false,
 | 
			
		||||
    this.width,
 | 
			
		||||
    this.height,
 | 
			
		||||
@@ -42,7 +44,7 @@ class NetworkImageWidget extends StatelessWidget {
 | 
			
		||||
              }
 | 
			
		||||
            : null,
 | 
			
		||||
        child: CachedNetworkImage(
 | 
			
		||||
          imageUrl: url,
 | 
			
		||||
          imageUrl: thumbnailURL ?? url,
 | 
			
		||||
          width: width,
 | 
			
		||||
          height: height,
 | 
			
		||||
          fit: BoxFit.cover,
 | 
			
		||||
 
 | 
			
		||||
@@ -39,16 +39,20 @@ String diffTimeToStr(int amount) {
 | 
			
		||||
 | 
			
		||||
  // Years
 | 
			
		||||
  final years = (amount / (60 * 60 * 24 * 365)).floor();
 | 
			
		||||
  return years == 1 ? tr("1 year") : tr("%years% years",
 | 
			
		||||
      args: {"years": years.toString()});
 | 
			
		||||
  return years == 1
 | 
			
		||||
      ? tr("1 year")
 | 
			
		||||
      : tr("%years% years", args: {"years": years.toString()});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
String diffTimeFromNowToStr(int date) {
 | 
			
		||||
  return diffTimeToStr(time() - date);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Return properly formatted date and time
 | 
			
		||||
String dateTimeToString(DateTime time) {
 | 
			
		||||
  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"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    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:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -183,6 +190,13 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2.8"
 | 
			
		||||
  filesize:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: filesize
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.0.4"
 | 
			
		||||
  flutter:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description: flutter
 | 
			
		||||
 
 | 
			
		||||
@@ -91,8 +91,15 @@ dependencies:
 | 
			
		||||
  # Version manager
 | 
			
		||||
  version: ^1.2.0
 | 
			
		||||
 | 
			
		||||
  # Play audio files
 | 
			
		||||
  audioplayers: ^0.15.1
 | 
			
		||||
 | 
			
		||||
  # Get path to temporary files
 | 
			
		||||
  path_provider: ^1.6.27
 | 
			
		||||
 | 
			
		||||
  # Format file size
 | 
			
		||||
  filesize: ^1.0.4
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user