mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-22 12:59: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"],
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user