From 19976940a01b6cbee1303a6553f1b206dba05841 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 16 Apr 2020 18:35:33 +0900 Subject: [PATCH] thumbnail: cancel queued image loading on dispose --- .../aves/channelhandlers/ImageDecodeTask.java | 17 ++- .../ImageDecodeTaskManager.java | 45 ------- .../channelhandlers/ImageFileHandler.java | 15 +-- lib/model/image_entry.dart | 4 +- lib/services/image_file_service.dart | 17 +-- lib/services/metadata_service.dart | 4 +- lib/services/service_policy.dart | 84 ++++++++---- lib/widgets/album/grid/header_generic.dart | 1 - .../album/grid/list_section_layout.dart | 4 +- lib/widgets/album/grid/list_sliver.dart | 2 +- lib/widgets/album/grid/scaling.dart | 6 +- lib/widgets/album/thumbnail.dart | 127 +++++++++++++----- .../app_icon_image_provider.dart | 7 +- .../image_providers/thumbnail_provider.dart | 47 ++++++- .../image_providers/uri_image_provider.dart | 7 +- lib/widgets/fullscreen/fullscreen_body.dart | 2 +- pubspec.lock | 7 + pubspec.yaml | 5 +- 18 files changed, 229 insertions(+), 172 deletions(-) delete mode 100644 android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTaskManager.java diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java index 203705860..15e9ebd18 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageDecodeTask.java @@ -23,7 +23,6 @@ import com.bumptech.glide.signature.ObjectKey; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.concurrent.ExecutionException; -import java.util.function.Consumer; import deckers.thibault.aves.decoder.VideoThumbnail; import deckers.thibault.aves.model.ImageEntry; @@ -37,14 +36,12 @@ public class ImageDecodeTask extends AsyncTask complete; - Params(ImageEntry entry, int width, int height, MethodChannel.Result result, Consumer complete) { + Params(ImageEntry entry, int width, int height, MethodChannel.Result result) { this.entry = entry; this.width = width; this.height = height; this.result = result; - this.complete = complete; } } @@ -127,10 +124,13 @@ public class ImageDecodeTask extends AsyncTask taskParamsQueue; - private boolean running = true; - - ImageDecodeTaskManager(Activity activity) { - taskParamsQueue = new LinkedBlockingDeque<>(); - new Thread(() -> { - try { - while (running) { - ImageDecodeTask.Params params = taskParamsQueue.take(); - new ImageDecodeTask(activity).execute(params); - Thread.sleep(10); - } - } catch (InterruptedException ex) { - Log.w(LOG_TAG, ex); - } - }).start(); - } - - void fetch(MethodChannel.Result result, ImageEntry entry, Integer width, Integer height) { - taskParamsQueue.addFirst(new ImageDecodeTask.Params(entry, width, height, result, this::complete)); - } - - void cancel(String uri) { - boolean removed = taskParamsQueue.removeIf(p -> uri.equals(p.entry.uri.toString())); - if (removed) Log.d(LOG_TAG, "cancelled uri=" + uri); - } - - private void complete(String uri) { - // nothing for now - } -} diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java index f89ca956e..a0c6d7ec9 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java @@ -34,12 +34,10 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { public static final String CHANNEL = "deckers.thibault/aves/image"; private Activity activity; - private ImageDecodeTaskManager imageDecodeTaskManager; private MediaStoreStreamHandler mediaStoreStreamHandler; public ImageFileHandler(Activity activity, MediaStoreStreamHandler mediaStoreStreamHandler) { this.activity = activity; - imageDecodeTaskManager = new ImageDecodeTaskManager(activity); this.mediaStoreStreamHandler = mediaStoreStreamHandler; } @@ -59,9 +57,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { case "getThumbnail": new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start(); break; - case "cancelGetThumbnail": - new Thread(() -> cancelGetThumbnail(call, new MethodResultWrapper(result))).start(); - break; case "delete": new Thread(() -> delete(call, new MethodResultWrapper(result))).start(); break; @@ -145,13 +140,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { return; } ImageEntry entry = new ImageEntry(entryMap); - imageDecodeTaskManager.fetch(result, entry, width, height); - } - - private void cancelGetThumbnail(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String uri = call.argument("uri"); - imageDecodeTaskManager.cancel(uri); - result.success(null); + new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, result)); } private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { @@ -275,7 +264,7 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; - int len = 0; + int len; while ((len = inputStream.read(buffer)) != -1) { byteBuffer.write(buffer, 0, len); } diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 7074512c1..ae1ad8b62 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -223,8 +223,8 @@ class ImageEntry { try { final addresses = await servicePolicy.call( () => Geocoder.local.findAddressesFromCoordinates(coordinates), - ServiceCallPriority.background, - 'findAddressesFromCoordinates-$path', + priority: ServiceCallPriority.background, + debugLabel: 'findAddressesFromCoordinates-$path', ); if (addresses != null && addresses.isNotEmpty) { final address = addresses.first; diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 65cf15e2f..d26610f5d 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -43,7 +43,7 @@ class ImageFileService { return Uint8List(0); } - static Future getThumbnail(ImageEntry entry, int width, int height) { + static Future getThumbnail(ImageEntry entry, int width, int height, {Object cancellationKey}) { return servicePolicy.call( () async { if (width > 0 && height > 0) { @@ -61,21 +61,12 @@ class ImageFileService { } return Uint8List(0); }, - ServiceCallPriority.asapLifo, - 'getThumbnail-${entry.path}', + priority: ServiceCallPriority.asap, + debugLabel: 'getThumbnail-${entry.path}', + cancellationKey: cancellationKey, ); } - static Future cancelGetThumbnail(String uri) async { - try { - await platform.invokeMethod('cancelGetThumbnail', { - 'uri': uri, - }); - } on PlatformException catch (e) { - debugPrint('cancelGetThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}'); - } - } - static Future delete(ImageEntry entry) async { try { await platform.invokeMethod('delete', { diff --git a/lib/services/metadata_service.dart b/lib/services/metadata_service.dart index 9a4f6b223..f63b2b94e 100644 --- a/lib/services/metadata_service.dart +++ b/lib/services/metadata_service.dart @@ -50,8 +50,8 @@ class MetadataService { } return null; }, - ServiceCallPriority.background, - 'getCatalogMetadata-${entry.path}', + priority: ServiceCallPriority.background, + debugLabel: 'getCatalogMetadata-${entry.path}', ); } diff --git a/lib/services/service_policy.dart b/lib/services/service_policy.dart index dfa7e215e..fb851b44d 100644 --- a/lib/services/service_policy.dart +++ b/lib/services/service_policy.dart @@ -1,47 +1,55 @@ import 'dart:async'; import 'dart:collection'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; final ServicePolicy servicePolicy = ServicePolicy._private(); class ServicePolicy { - final Queue _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue(); - VoidCallback _running; + final Queue<_Task> _asapQueue, _normalQueue, _backgroundQueue; + List> _queues; + _Task _running; - ServicePolicy._private(); + ServicePolicy._private() + : _asapQueue = Queue(), + _normalQueue = Queue(), + _backgroundQueue = Queue() { + _queues = [_asapQueue, _normalQueue, _backgroundQueue]; + } - Future call(Future Function() platformCall, [ServiceCallPriority priority = ServiceCallPriority.normal, String debugLabel]) { - Queue q; + Future call( + Future Function() platformCall, { + ServiceCallPriority priority = ServiceCallPriority.normal, + String debugLabel, + Object cancellationKey, + }) { + Queue<_Task> queue; switch (priority) { - case ServiceCallPriority.asapFifo: - q = _asapQueue; - break; - case ServiceCallPriority.asapLifo: - q = _asapQueue; + case ServiceCallPriority.asap: + queue = _asapQueue; break; case ServiceCallPriority.background: - q = _backgroundQueue; + queue = _backgroundQueue; break; case ServiceCallPriority.normal: default: - q = _normalQueue; + queue = _normalQueue; break; } final completer = Completer(); - final wrapped = () async { + final wrapped = _Task( + () async { // if (debugLabel != null) debugPrint('$runtimeType $debugLabel start'); - final result = await platformCall(); - completer.complete(result); + final result = await platformCall(); + completer.complete(result); // if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed'); - _running = null; - _pickNext(); - }; - if (priority == ServiceCallPriority.asapLifo) { - q.addFirst(wrapped); - } else { - q.addLast(wrapped); - } + _running = null; + _pickNext(); + }, + completer, + cancellationKey, + ); + queue.addLast(wrapped); _pickNext(); return completer.future; @@ -49,10 +57,32 @@ class ServicePolicy { void _pickNext() { if (_running != null) return; - final queue = [_asapQueue, _normalQueue, _backgroundQueue].firstWhere((q) => q.isNotEmpty, orElse: () => null); + final queue = _queues.firstWhere((q) => q.isNotEmpty, orElse: () => null); _running = queue?.removeFirst(); - _running?.call(); + _running?.callback?.call(); + } + + bool cancel(Object cancellationKey) { + var cancelled = false; + final tasks = _queues.expand((q) => q.where((task) => task.cancellationKey == cancellationKey)).toList(); + tasks.forEach((task) => _queues.forEach((q) { + if (q.remove(task)) { + cancelled = true; + task.completer.completeError(CancelledException()); + } + })); + return cancelled; } } -enum ServiceCallPriority { asapFifo, asapLifo, normal, background } +class _Task { + final VoidCallback callback; + final Completer completer; + final Object cancellationKey; + + const _Task(this.callback, this.completer, this.cancellationKey); +} + +class CancelledException {} + +enum ServiceCallPriority { asap, normal, background } diff --git a/lib/widgets/album/grid/header_generic.dart b/lib/widgets/album/grid/header_generic.dart index a2bfddf8e..64f25ae0e 100644 --- a/lib/widgets/album/grid/header_generic.dart +++ b/lib/widgets/album/grid/header_generic.dart @@ -7,7 +7,6 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/album/grid/header_album.dart'; import 'package:aves/widgets/album/grid/header_date.dart'; import 'package:aves/widgets/common/fx/outlined_text.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; diff --git a/lib/widgets/album/grid/list_section_layout.dart b/lib/widgets/album/grid/list_section_layout.dart index 5037e8e9d..de6a2ce04 100644 --- a/lib/widgets/album/grid/list_section_layout.dart +++ b/lib/widgets/album/grid/list_section_layout.dart @@ -91,10 +91,12 @@ class SectionedListLayoutProvider extends StatelessWidget { final maxEntryIndex = min(sectionEntryCount, minEntryIndex + columnCount); final children = []; for (var i = minEntryIndex; i < maxEntryIndex; i++) { + final entry = section[i]; children.add(GridThumbnail( + key: ValueKey(entry.contentId), collection: collection, index: i, - entry: section[i], + entry: entry, tileExtent: tileExtent, )); } diff --git a/lib/widgets/album/grid/list_sliver.dart b/lib/widgets/album/grid/list_sliver.dart index 0a5dfe94d..3aeaf654b 100644 --- a/lib/widgets/album/grid/list_sliver.dart +++ b/lib/widgets/album/grid/list_sliver.dart @@ -54,7 +54,7 @@ class GridThumbnail extends StatelessWidget { onTap: () => _goToFullscreen(context), child: MetaData( metaData: ThumbnailMetadata(index, entry), - child: Thumbnail( + child: DecoratedThumbnail( entry: entry, extent: tileExtent, heroTag: collection.heroTag(entry), diff --git a/lib/widgets/album/grid/scaling.dart b/lib/widgets/album/grid/scaling.dart index b0248d8dd..453f16103 100644 --- a/lib/widgets/album/grid/scaling.dart +++ b/lib/widgets/album/grid/scaling.dart @@ -204,7 +204,7 @@ class _ScaleOverlayState extends State { Positioned( left: clampedCenter.dx - extent / 2, top: clampedCenter.dy - extent / 2, - child: Thumbnail( + child: DecoratedThumbnail( entry: widget.imageEntry, extent: extent, ), @@ -232,12 +232,12 @@ class GridPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() - ..strokeWidth = Thumbnail.borderWidth + ..strokeWidth = DecoratedThumbnail.borderWidth ..shader = ui.Gradient.radial( center, size.width / 2, [ - Thumbnail.borderColor, + DecoratedThumbnail.borderColor, Colors.transparent, ], [ diff --git a/lib/widgets/album/thumbnail.dart b/lib/widgets/album/thumbnail.dart index e33acdc60..0e9acf5fd 100644 --- a/lib/widgets/album/thumbnail.dart +++ b/lib/widgets/album/thumbnail.dart @@ -10,7 +10,7 @@ import 'package:aves/widgets/common/transition_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class Thumbnail extends StatelessWidget { +class DecoratedThumbnail extends StatelessWidget { final ImageEntry entry; final double extent; final Object heroTag; @@ -18,7 +18,7 @@ class Thumbnail extends StatelessWidget { static final Color borderColor = Colors.grey.shade700; static const double borderWidth = .5; - const Thumbnail({ + const DecoratedThumbnail({ Key key, @required this.entry, @required this.extent, @@ -39,7 +39,13 @@ class Thumbnail extends StatelessWidget { child: Stack( alignment: AlignmentDirectional.bottomStart, children: [ - entry.isSvg ? _buildVectorImage() : _buildRasterImage(), + entry.isSvg + ? _buildVectorImage() + : ThumbnailRasterImage( + entry: entry, + extent: extent, + heroTag: heroTag, + ), _ThumbnailOverlay( entry: entry, extent: extent, @@ -49,38 +55,6 @@ class Thumbnail extends StatelessWidget { ); } - Widget _buildRasterImage() { - final thumbnailProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent); - final image = Image( - image: thumbnailProvider, - width: extent, - height: extent, - fit: BoxFit.cover, - ); - return heroTag == null - ? image - : Hero( - tag: heroTag, - flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { - ImageProvider heroImageProvider = thumbnailProvider; - if (!entry.isVideo && !entry.isSvg) { - final imageProvider = UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - ); - if (imageCache.statusForKey(imageProvider).keepAlive) { - heroImageProvider = imageProvider; - } - } - return TransitionImage( - image: heroImageProvider, - animation: animation, - ); - }, - child: image, - ); - } - Widget _buildVectorImage() { final child = Container( // center `SvgPicture` inside `Container` with the thumbnail dimensions @@ -106,6 +80,89 @@ class Thumbnail extends StatelessWidget { } } +class ThumbnailRasterImage extends StatefulWidget { + final ImageEntry entry; + final double extent; + final Object heroTag; + + const ThumbnailRasterImage({ + Key key, + @required this.entry, + @required this.extent, + this.heroTag, + }) : super(key: key); + + @override + _ThumbnailRasterImageState createState() => _ThumbnailRasterImageState(); +} + +class _ThumbnailRasterImageState extends State { + ThumbnailProvider _imageProvider; + + ImageEntry get entry => widget.entry; + + double get extent => widget.extent; + + Object get heroTag => widget.heroTag; + + @override + void initState() { + super.initState(); + _initProvider(); + } + + @override + void didUpdateWidget(ThumbnailRasterImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.entry != entry) { + _cancelProvider(); + _initProvider(); + } + } + + @override + void dispose() { + _cancelProvider(); + super.dispose(); + } + + void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent); + + void _cancelProvider() => _imageProvider?.cancel(); + + @override + Widget build(BuildContext context) { + final image = Image( + image: _imageProvider, + width: extent, + height: extent, + fit: BoxFit.cover, + ); + return heroTag == null + ? image + : Hero( + tag: heroTag, + flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { + ImageProvider heroImageProvider = _imageProvider; + if (!entry.isVideo && !entry.isSvg) { + final imageProvider = UriImage( + uri: entry.uri, + mimeType: entry.mimeType, + ); + if (imageCache.statusForKey(imageProvider).keepAlive) { + heroImageProvider = imageProvider; + } + } + return TransitionImage( + image: heroImageProvider, + animation: animation, + ); + }, + child: image, + ); + } +} + class _ThumbnailOverlay extends StatelessWidget { final ImageEntry entry; final double extent; diff --git a/lib/widgets/common/image_providers/app_icon_image_provider.dart b/lib/widgets/common/image_providers/app_icon_image_provider.dart index db2ab146e..6e0e77437 100644 --- a/lib/widgets/common/image_providers/app_icon_image_provider.dart +++ b/lib/widgets/common/image_providers/app_icon_image_provider.dart @@ -1,3 +1,4 @@ +import 'dart:typed_data'; import 'dart:ui' as ui show Codec; import 'package:aves/services/android_app_service.dart'; @@ -38,11 +39,7 @@ class AppIconImage extends ImageProvider { Future _loadAsync(AppIconImageKey key, DecoderCallback decode) async { final bytes = await AndroidAppService.getAppIcon(key.packageName, key.sizePixels); - if (bytes.lengthInBytes == 0) { - return null; - } - - return await decode(bytes); + return await decode(bytes ?? Uint8List(0)); } } diff --git a/lib/widgets/common/image_providers/thumbnail_provider.dart b/lib/widgets/common/image_providers/thumbnail_provider.dart index 0f748b522..bd7dd3923 100644 --- a/lib/widgets/common/image_providers/thumbnail_provider.dart +++ b/lib/widgets/common/image_providers/thumbnail_provider.dart @@ -1,12 +1,15 @@ +import 'dart:typed_data'; import 'dart:ui' as ui show Codec; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/services/service_policy.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; class ThumbnailProvider extends ImageProvider { - const ThumbnailProvider({ + ThumbnailProvider({ @required this.entry, @required this.extent, this.scale = 1.0, @@ -18,6 +21,8 @@ class ThumbnailProvider extends ImageProvider { final double extent; final double scale; + final Object _cancellationKey = Uuid(); + @override Future obtainKey(ImageConfiguration configuration) { // configuration can be empty (e.g. when obtaining key for eviction) @@ -33,7 +38,7 @@ class ThumbnailProvider extends ImageProvider { @override ImageStreamCompleter load(ThumbnailProviderKey key, DecoderCallback decode) { - return MultiFrameImageStreamCompleter( + return CancellableMultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { @@ -44,12 +49,14 @@ class ThumbnailProvider extends ImageProvider { Future _loadAsync(ThumbnailProviderKey key, DecoderCallback decode) async { final dimPixels = (extent * key.devicePixelRatio).round(); - final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels); - if (bytes.lengthInBytes == 0) { - return null; - } + final bytes = await ImageFileService.getThumbnail(key.entry, dimPixels, dimPixels, cancellationKey: _cancellationKey); + return await decode(bytes ?? Uint8List(0)); + } - return await decode(bytes); + Future cancel() async { + if (servicePolicy.cancel(_cancellationKey)) { + await evict(); + } } } @@ -74,4 +81,30 @@ class ThumbnailProviderKey { @override int get hashCode => hashValues(entry.uri, extent, scale); + + @override + String toString() { + return 'ThumbnailProviderKey{uri=${entry.uri}, extent=$extent, scale=$scale}'; + } +} + +class CancellableMultiFrameImageStreamCompleter extends MultiFrameImageStreamCompleter { + CancellableMultiFrameImageStreamCompleter({ + @required Future codec, + @required double scale, + Stream chunkEvents, + InformationCollector informationCollector, + }) : super( + codec: codec, + scale: scale, + chunkEvents: chunkEvents, + informationCollector: informationCollector, + ); + + @override + void reportError({DiagnosticsNode context, dynamic exception, StackTrace stack, informationCollector, bool silent = false}) { + // prevent default error reporting in case of planned cancellation + if (exception is CancelledException) return; + super.reportError(context: context, exception: exception, stack: stack, informationCollector: informationCollector, silent: silent); + } } diff --git a/lib/widgets/common/image_providers/uri_image_provider.dart b/lib/widgets/common/image_providers/uri_image_provider.dart index 46c4dc0b7..7dc101f34 100644 --- a/lib/widgets/common/image_providers/uri_image_provider.dart +++ b/lib/widgets/common/image_providers/uri_image_provider.dart @@ -1,3 +1,4 @@ +import 'dart:typed_data'; import 'dart:ui' as ui show Codec; import 'package:aves/services/image_file_service.dart'; @@ -36,11 +37,7 @@ class UriImage extends ImageProvider { assert(key == this); final bytes = await ImageFileService.getImage(uri, mimeType); - if (bytes.lengthInBytes == 0) { - return null; - } - - return await decode(bytes); + return await decode(bytes ?? Uint8List(0)); } @override diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 6df0df5a0..640465bec 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -399,8 +399,8 @@ class _FullscreenVerticalPageViewState extends State @override void dispose() { - super.dispose(); _unregisterWidget(widget); + super.dispose(); } void _registerWidget(FullscreenVerticalPageView widget) { diff --git a/pubspec.lock b/pubspec.lock index 8606a78d5..c022e688b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -475,6 +475,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.0+5" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ace22c153..bb3440d4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,12 +20,12 @@ dependencies: charts_flutter: collection: draggable_scrollbar: -# path: ../flutter-draggable-scrollbar + # path: ../flutter-draggable-scrollbar git: url: git://github.com/deckerst/flutter-draggable-scrollbar.git event_bus: expansion_tile_card: -# path: ../expansion_tile_card + # path: ../expansion_tile_card git: url: git://github.com/deckerst/expansion_tile_card.git flushbar: @@ -54,6 +54,7 @@ dependencies: sqflite: transparent_image: tuple: + uuid: video_player: path: ../plugins/packages/video_player/video_player