mirror of
				https://gitlab.com/comunic/comunicmobile
				synced 2025-11-04 12:14:11 +00:00 
			
		
		
		
	Show message files
This commit is contained in:
		@@ -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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user