use Glide for a lower priority pass of higher quality thumbnails
This commit is contained in:
parent
f7ef4c0d01
commit
65fffdd21a
13 changed files with 189 additions and 122 deletions
|
@ -12,6 +12,7 @@ import android.provider.MediaStore;
|
|||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
@ -35,14 +36,15 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
|
||||
static class Params {
|
||||
ImageEntry entry;
|
||||
int width, height;
|
||||
Integer width, height, defaultSize;
|
||||
MethodChannel.Result result;
|
||||
|
||||
Params(ImageEntry entry, int width, int height, MethodChannel.Result result) {
|
||||
Params(ImageEntry entry, @Nullable Integer width, @Nullable Integer height, Integer defaultSize, MethodChannel.Result result) {
|
||||
this.entry = entry;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.result = result;
|
||||
this.defaultSize = defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,17 +71,24 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
Bitmap bitmap = null;
|
||||
if (!this.isCancelled()) {
|
||||
Exception exception = null;
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
bitmap = getThumbnailBytesByResolver(p);
|
||||
} else {
|
||||
bitmap = getThumbnailBytesByMediaStore(p);
|
||||
Integer w = p.width;
|
||||
Integer h = p.height;
|
||||
// fetch low quality thumbnails when size is not specified
|
||||
if (w == null || h == null || w == 0 || h == 0) {
|
||||
p.width = p.defaultSize;
|
||||
p.height = p.defaultSize;
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
bitmap = getThumbnailBytesByResolver(p);
|
||||
} else {
|
||||
bitmap = getThumbnailBytesByMediaStore(p);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
}
|
||||
|
||||
// fallback if the native methods failed
|
||||
// fallback if the native methods failed or for higher quality thumbnails
|
||||
try {
|
||||
if (bitmap == null) {
|
||||
bitmap = getThumbnailByGlide(p);
|
||||
|
@ -108,8 +117,9 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
private Bitmap getThumbnailBytesByResolver(Params params) throws IOException {
|
||||
ImageEntry entry = params.entry;
|
||||
int width = params.width;
|
||||
int height = params.height;
|
||||
Integer width = params.width;
|
||||
Integer height = params.height;
|
||||
// Log.d(LOG_TAG, "getThumbnailBytesByResolver width=" + width + ", path=" + entry.path);
|
||||
|
||||
ContentResolver resolver = activity.getContentResolver();
|
||||
return resolver.loadThumbnail(entry.uri, new Size(width, height), null);
|
||||
|
@ -141,6 +151,7 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
|||
ImageEntry entry = params.entry;
|
||||
int width = params.width;
|
||||
int height = params.height;
|
||||
// Log.d(LOG_TAG, "getThumbnailByGlide width=" + width + ", path=" + entry.path);
|
||||
|
||||
// add signature to ignore cache for images which got modified but kept the same URI
|
||||
Key signature = new ObjectKey("" + entry.dateModifiedSecs + entry.width + entry.orientationDegrees);
|
||||
|
|
|
@ -7,6 +7,8 @@ import android.os.Looper;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import deckers.thibault.aves.model.ImageEntry;
|
||||
|
@ -39,6 +41,10 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
case "getThumbnail":
|
||||
new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
case "clearSizedThumbnailDiskCache":
|
||||
new Thread(() -> Glide.get(activity).clearDiskCache()).start();
|
||||
result.success(null);
|
||||
break;
|
||||
case "rename":
|
||||
new Thread(() -> rename(call, new MethodResultWrapper(result))).start();
|
||||
break;
|
||||
|
@ -55,12 +61,13 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler {
|
|||
Map entryMap = call.argument("entry");
|
||||
Integer width = call.argument("width");
|
||||
Integer height = call.argument("height");
|
||||
if (entryMap == null || width == null || height == null) {
|
||||
Integer defaultSize = call.argument("defaultSize");
|
||||
if (entryMap == null || defaultSize == null) {
|
||||
result.error("getThumbnail-args", "failed because of missing arguments", null);
|
||||
return;
|
||||
}
|
||||
ImageEntry entry = new ImageEntry(entryMap);
|
||||
new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, result));
|
||||
new ImageDecodeTask(activity).execute(new ImageDecodeTask.Params(entry, width, height, defaultSize, result));
|
||||
}
|
||||
|
||||
private void getImageEntry(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
|
|
|
@ -266,7 +266,7 @@ class ImageEntry {
|
|||
try {
|
||||
final addresses = await servicePolicy.call(
|
||||
() => Geocoder.local.findAddressesFromCoordinates(coordinates),
|
||||
priority: ServiceCallPriority.background,
|
||||
priority: ServiceCallPriority.getLocation,
|
||||
);
|
||||
if (addresses != null && addresses.isNotEmpty) {
|
||||
final address = addresses.first;
|
||||
|
|
|
@ -14,8 +14,6 @@ class ImageFileService {
|
|||
static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream');
|
||||
static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream');
|
||||
|
||||
static const thumbnailPriority = ServiceCallPriority.asap;
|
||||
|
||||
static Future<void> getImageEntries() async {
|
||||
try {
|
||||
await platform.invokeMethod('getImageEntries');
|
||||
|
@ -64,28 +62,36 @@ class ImageFileService {
|
|||
static Future<Uint8List> getThumbnail(ImageEntry entry, int width, int height, {Object taskKey}) {
|
||||
return servicePolicy.call(
|
||||
() async {
|
||||
if (width > 0 && height > 0) {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
||||
'entry': entry.toMap(),
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
try {
|
||||
final result = await platform.invokeMethod('getThumbnail', <String, dynamic>{
|
||||
'entry': entry.toMap(),
|
||||
'width': width,
|
||||
'height': height,
|
||||
'defaultSize': 256,
|
||||
});
|
||||
return result as Uint8List;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
return Uint8List(0);
|
||||
},
|
||||
priority: thumbnailPriority,
|
||||
// debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}',
|
||||
priority: width == 0 || height == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail,
|
||||
key: taskKey,
|
||||
);
|
||||
}
|
||||
|
||||
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, thumbnailPriority);
|
||||
static Future<void> clearSizedThumbnailDiskCache() async {
|
||||
try {
|
||||
return platform.invokeMethod('clearSizedThumbnailDiskCache');
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('clearSizedThumbnailDiskCache failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey, thumbnailPriority);
|
||||
static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]);
|
||||
|
||||
static Future<T> resumeThumbnail<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||
|
||||
static Stream<ImageOpEvent> delete(Iterable<ImageEntry> entries) {
|
||||
try {
|
||||
|
|
|
@ -50,7 +50,7 @@ class MetadataService {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
priority: ServiceCallPriority.background,
|
||||
priority: ServiceCallPriority.getMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,36 +2,37 @@ import 'dart:async';
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
final ServicePolicy servicePolicy = ServicePolicy._private();
|
||||
|
||||
class ServicePolicy {
|
||||
final Map<Object, _Task> _paused = {};
|
||||
final Queue<_Task> _asapQueue = Queue(), _normalQueue = Queue(), _backgroundQueue = Queue();
|
||||
List<Queue<_Task>> _queues;
|
||||
final Map<Object, Tuple2<int, _Task>> _paused = {};
|
||||
final SplayTreeMap<int, Queue<_Task>> _queues = SplayTreeMap();
|
||||
_Task _running;
|
||||
|
||||
ServicePolicy._private() {
|
||||
_queues = [_asapQueue, _normalQueue, _backgroundQueue];
|
||||
}
|
||||
ServicePolicy._private();
|
||||
|
||||
Future<T> call<T>(
|
||||
Future<T> Function() platformCall, {
|
||||
ServiceCallPriority priority = ServiceCallPriority.normal,
|
||||
int priority = ServiceCallPriority.normal,
|
||||
String debugLabel,
|
||||
Object key,
|
||||
}) {
|
||||
var task = _paused.remove(key);
|
||||
if (task != null) {
|
||||
_Task task;
|
||||
final priorityTask = _paused.remove(key);
|
||||
if (priorityTask != null) {
|
||||
debugPrint('resume task with key=$key');
|
||||
priority = priorityTask.item1;
|
||||
task = priorityTask.item2;
|
||||
}
|
||||
var completer = task?.completer ?? Completer<T>();
|
||||
task ??= _Task(
|
||||
() async {
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
|
||||
if (debugLabel != null) debugPrint('$runtimeType $debugLabel start');
|
||||
final result = await platformCall();
|
||||
completer.complete(result);
|
||||
// if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
|
||||
if (debugLabel != null) debugPrint('$runtimeType $debugLabel completed');
|
||||
_running = null;
|
||||
_pickNext();
|
||||
},
|
||||
|
@ -43,62 +44,46 @@ class ServicePolicy {
|
|||
return completer.future;
|
||||
}
|
||||
|
||||
Future<T> resume<T>(Object key, ServiceCallPriority priority) {
|
||||
var task = _paused.remove(key);
|
||||
if (task == null) return null;
|
||||
Future<T> resume<T>(Object key) {
|
||||
final priorityTask = _paused.remove(key);
|
||||
if (priorityTask == null) return null;
|
||||
final priority = priorityTask.item1;
|
||||
final task = priorityTask.item2;
|
||||
_getQueue(priority).addLast(task);
|
||||
_pickNext();
|
||||
return task.completer.future;
|
||||
}
|
||||
|
||||
Queue<_Task> _getQueue(ServiceCallPriority priority) {
|
||||
Queue<_Task> queue;
|
||||
switch (priority) {
|
||||
case ServiceCallPriority.asap:
|
||||
queue = _asapQueue;
|
||||
break;
|
||||
case ServiceCallPriority.background:
|
||||
queue = _backgroundQueue;
|
||||
break;
|
||||
case ServiceCallPriority.normal:
|
||||
default:
|
||||
queue = _normalQueue;
|
||||
break;
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
Queue<_Task> _getQueue(int priority) => _queues.putIfAbsent(priority, () => Queue<_Task>());
|
||||
|
||||
void _pickNext() {
|
||||
if (_running != null) return;
|
||||
final queue = _queues.firstWhere((q) => q.isNotEmpty, orElse: () => null);
|
||||
final queue = _queues.entries.firstWhere((kv) => kv.value.isNotEmpty, orElse: () => null)?.value;
|
||||
_running = queue?.removeFirst();
|
||||
_running?.callback?.call();
|
||||
}
|
||||
|
||||
bool cancel(Object key, ServiceCallPriority priority) {
|
||||
var cancelled = false;
|
||||
final queue = _getQueue(priority);
|
||||
final tasks = queue.where((task) => task.key == key).toList();
|
||||
tasks.forEach((task) {
|
||||
if (queue.remove(task)) {
|
||||
cancelled = true;
|
||||
task.completer.completeError(CancelledException());
|
||||
}
|
||||
bool _takeOut(Object key, Iterable<int> priorities, void Function(int priority, _Task task) action) {
|
||||
var out = false;
|
||||
priorities.forEach((priority) {
|
||||
final queue = _getQueue(priority);
|
||||
final tasks = queue.where((task) => task.key == key).toList();
|
||||
tasks.forEach((task) {
|
||||
if (queue.remove(task)) {
|
||||
out = true;
|
||||
action(priority, task);
|
||||
}
|
||||
});
|
||||
});
|
||||
return cancelled;
|
||||
return out;
|
||||
}
|
||||
|
||||
bool pause(Object key, ServiceCallPriority priority) {
|
||||
var paused = false;
|
||||
final queue = _getQueue(priority);
|
||||
final tasks = queue.where((task) => task.key == key).toList();
|
||||
tasks.forEach((task) {
|
||||
if (queue.remove(task)) {
|
||||
paused = true;
|
||||
_paused.putIfAbsent(key, () => task);
|
||||
}
|
||||
});
|
||||
return paused;
|
||||
bool cancel(Object key, Iterable<int> priorities) {
|
||||
return _takeOut(key, priorities, (priority, task) => task.completer.completeError(CancelledException()));
|
||||
}
|
||||
|
||||
bool pause(Object key, Iterable<int> priorities) {
|
||||
return _takeOut(key, priorities, (priority, task) => _paused.putIfAbsent(key, () => Tuple2(priority, task)));
|
||||
}
|
||||
|
||||
bool isPaused(Object key) => _paused.containsKey(key);
|
||||
|
@ -114,4 +99,10 @@ class _Task {
|
|||
|
||||
class CancelledException {}
|
||||
|
||||
enum ServiceCallPriority { asap, normal, background }
|
||||
class ServiceCallPriority {
|
||||
static const int getFastThumbnail = 100;
|
||||
static const int getSizedThumbnail = 200;
|
||||
static const int normal = 500;
|
||||
static const int getMetadata = 1000;
|
||||
static const int getLocation = 1000;
|
||||
}
|
||||
|
|
|
@ -22,9 +22,6 @@ class Constants {
|
|||
// ref _PopupMenuRoute._kMenuDuration
|
||||
static const popupMenuTransitionDuration = Duration(milliseconds: 300);
|
||||
|
||||
// TODO TLAD smarter sizing, but shouldn't only depend on `extent` so that it doesn't reload during gridview scaling
|
||||
static const double thumbnailCacheExtent = 50;
|
||||
|
||||
static const svgBackground = Colors.white;
|
||||
static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/widgets/common/transition_image.dart';
|
||||
|
@ -24,7 +25,7 @@ class ThumbnailRasterImage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||
ThumbnailProvider _imageProvider;
|
||||
ThumbnailProvider _fastThumbnailProvider, _sizedThumbnailProvider;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
|
@ -32,6 +33,11 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
|||
|
||||
Object get heroTag => widget.heroTag;
|
||||
|
||||
// we standardize the thumbnail loading dimension by taking the nearest larger power of 2
|
||||
// so that there are less variants of the thumbnails to load and cache
|
||||
// it increases the chance of cache hit when loading similarly sized columns (e.g. on orientation change)
|
||||
double get requestExtent => pow(2, (log(extent) / log(2)).ceil()).toDouble();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -53,7 +59,12 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _initProvider() => _imageProvider = ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent);
|
||||
void _initProvider() {
|
||||
_fastThumbnailProvider = ThumbnailProvider(entry: entry);
|
||||
if (!entry.isVideo) {
|
||||
_sizedThumbnailProvider = ThumbnailProvider(entry: entry, extent: requestExtent);
|
||||
}
|
||||
}
|
||||
|
||||
void _pauseProvider() {
|
||||
final isScrolling = widget.isScrollingNotifier?.value ?? false;
|
||||
|
@ -61,24 +72,36 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
|||
// the retrieval task queue can pile up for thumbnails that got disposed
|
||||
// in this case we pause the image retrieval task to get it out of the queue
|
||||
if (isScrolling) {
|
||||
_imageProvider?.pause();
|
||||
_fastThumbnailProvider?.pause();
|
||||
_sizedThumbnailProvider?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = Image(
|
||||
image: _imageProvider,
|
||||
final fastImage = Image(
|
||||
image: _fastThumbnailProvider,
|
||||
width: extent,
|
||||
height: extent,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
final image = _sizedThumbnailProvider == null
|
||||
? fastImage
|
||||
: Image(
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
return frame == null ? fastImage : child;
|
||||
},
|
||||
image: _sizedThumbnailProvider,
|
||||
width: extent,
|
||||
height: extent,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
return heroTag == null
|
||||
? image
|
||||
: Hero(
|
||||
tag: heroTag,
|
||||
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
|
||||
ImageProvider heroImageProvider = _imageProvider;
|
||||
ImageProvider heroImageProvider = _fastThumbnailProvider;
|
||||
if (!entry.isVideo && !entry.isSvg) {
|
||||
final imageProvider = UriImage(
|
||||
uri: entry.uri,
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/common/icons.dart';
|
|||
import 'package:aves/widgets/common/scroll_thumb.dart';
|
||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
@ -32,6 +33,7 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
final mqSize = mq.item1;
|
||||
final mqHorizontalPadding = mq.item2;
|
||||
TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier);
|
||||
final cacheExtent = TileExtentManager.extentMaxForSize(mqSize);
|
||||
|
||||
// do not replace by Provider.of<CollectionLens>
|
||||
// so that view updates on collection filter changes
|
||||
|
@ -47,6 +49,7 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollController: PrimaryScrollController.of(context),
|
||||
cacheExtent: cacheExtent,
|
||||
);
|
||||
|
||||
final scaler = GridScaleGestureDetector(
|
||||
|
@ -91,6 +94,7 @@ class CollectionScrollView extends StatefulWidget {
|
|||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final ScrollController scrollController;
|
||||
final double cacheExtent;
|
||||
|
||||
const CollectionScrollView({
|
||||
@required this.scrollableKey,
|
||||
|
@ -99,6 +103,7 @@ class CollectionScrollView extends StatefulWidget {
|
|||
@required this.appBarHeightNotifier,
|
||||
@required this.isScrollingNotifier,
|
||||
@required this.scrollController,
|
||||
@required this.cacheExtent,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -149,6 +154,7 @@ class _CollectionScrollViewState extends State<CollectionScrollView> {
|
|||
// workaround to prevent scrolling the app bar away
|
||||
// when there is no content and we use `SliverFillRemaining`
|
||||
physics: collection.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
||||
cacheExtent: widget.cacheExtent,
|
||||
slivers: [
|
||||
appBar,
|
||||
collection.isEmpty
|
||||
|
|
|
@ -9,8 +9,8 @@ import 'package:flutter/material.dart';
|
|||
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||
ThumbnailProvider({
|
||||
@required this.entry,
|
||||
@required this.extent,
|
||||
this.scale = 1.0,
|
||||
this.extent = 0,
|
||||
this.scale = 1,
|
||||
}) : assert(entry != null),
|
||||
assert(extent != null),
|
||||
assert(scale != null) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/metadata_db.dart';
|
|||
import 'package:aves/model/settings.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/android_file_service.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||
|
@ -65,7 +66,7 @@ class DebugPageState extends State<DebugPage> {
|
|||
Tab(icon: Icon(OMIcons.whatshot)),
|
||||
Tab(icon: Icon(OMIcons.settings)),
|
||||
Tab(icon: Icon(OMIcons.sdStorage)),
|
||||
Tab(text: 'Env'),
|
||||
Tab(icon: Icon(OMIcons.android)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -108,8 +109,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Text('Image cache: ${imageCache.currentSize} items, ${formatFilesize(imageCache.currentSizeBytes)}'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFilesize(imageCache.currentSizeBytes)}/${formatFilesize(imageCache.maximumSizeBytes)}'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
imageCache.clear();
|
||||
|
@ -121,8 +124,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
),
|
||||
Row(
|
||||
children: [
|
||||
Text('SVG cache: ${PictureProvider.cacheCount} items'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('SVG cache: ${PictureProvider.cacheCount} items'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () {
|
||||
PictureProvider.clearCache();
|
||||
|
@ -132,6 +137,18 @@ class DebugPageState extends State<DebugPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text('Glide disk cache: ?'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => ImageFileService.clearSizedThumbnailDiskCache(),
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
FutureBuilder(
|
||||
future: _dbFileSizeLoader,
|
||||
|
@ -140,8 +157,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||
return Row(
|
||||
children: [
|
||||
Text('DB file size: ${formatFilesize(snapshot.data)}'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('DB file size: ${formatFilesize(snapshot.data)}'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => metadataDb.reset().then((_) => _startDbReport()),
|
||||
child: const Text('Reset'),
|
||||
|
@ -157,8 +176,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||
return Row(
|
||||
children: [
|
||||
Text('DB date rows: ${snapshot.data.length}'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('DB date rows: ${snapshot.data.length}'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => metadataDb.clearDates().then((_) => _startDbReport()),
|
||||
child: const Text('Clear'),
|
||||
|
@ -174,8 +195,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||
return Row(
|
||||
children: [
|
||||
Text('DB metadata rows: ${snapshot.data.length}'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('DB metadata rows: ${snapshot.data.length}'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => metadataDb.clearMetadataEntries().then((_) => _startDbReport()),
|
||||
child: const Text('Clear'),
|
||||
|
@ -191,8 +214,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||
return Row(
|
||||
children: [
|
||||
Text('DB address rows: ${snapshot.data.length}'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('DB address rows: ${snapshot.data.length}'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => metadataDb.clearAddresses().then((_) => _startDbReport()),
|
||||
child: const Text('Clear'),
|
||||
|
@ -208,8 +233,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
if (snapshot.connectionState != ConnectionState.done) return const SizedBox.shrink();
|
||||
return Row(
|
||||
children: [
|
||||
Text('DB favourite rows: ${snapshot.data.length} (${favourites.count} in memory)'),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: Text('DB favourite rows: ${snapshot.data.length} (${favourites.count} in memory)'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => favourites.clear().then((_) => _startDbReport()),
|
||||
child: const Text('Clear'),
|
||||
|
@ -228,8 +255,10 @@ class DebugPageState extends State<DebugPage> {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text('Settings'),
|
||||
const Spacer(),
|
||||
const Expanded(
|
||||
child: Text('Settings'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RaisedButton(
|
||||
onPressed: () => settings.reset().then((_) => setState(() {})),
|
||||
child: const Text('Reset'),
|
||||
|
|
|
@ -5,7 +5,6 @@ import 'package:aves/model/collection_lens.dart';
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/album/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart';
|
||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||
|
@ -477,7 +476,8 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
|||
|
||||
void _onImageChanged() async {
|
||||
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
|
||||
await ThumbnailProvider(entry: entry, extent: Constants.thumbnailCacheExtent).evict();
|
||||
// TODO TLAD also evict `ThumbnailProvider` with specified extents
|
||||
await ThumbnailProvider(entry: entry).evict();
|
||||
if (entry.path != null) await FileImage(File(entry.path)).evict();
|
||||
// rebuild to refresh the Image inside ImagePage
|
||||
setState(() {});
|
||||
|
|
|
@ -56,10 +56,7 @@ class ImageView extends StatelessWidget {
|
|||
// if the hero tag wraps the whole `PhotoView` and the `loadingBuilder` is not provided,
|
||||
// there's a black frame between the hero animation and the final image, even when it's cached.
|
||||
|
||||
final thumbnailProvider = ThumbnailProvider(
|
||||
entry: entry,
|
||||
extent: Constants.thumbnailCacheExtent,
|
||||
);
|
||||
final fastThumbnailProvider = ThumbnailProvider(entry: entry);
|
||||
// this loading builder shows a transition image until the final image is ready
|
||||
// if the image is already in the cache it will show the final image, otherwise the thumbnail
|
||||
// in any case, we should use `Center` + `AspectRatio` + `Fill` so that the transition image
|
||||
|
@ -86,7 +83,7 @@ class ImageView extends StatelessWidget {
|
|||
mimeType: entry.mimeType,
|
||||
colorFilter: Constants.svgColorFilter,
|
||||
),
|
||||
placeholderBuilder: (context) => loadingBuilder(context, thumbnailProvider),
|
||||
placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider),
|
||||
),
|
||||
backgroundDecoration: backgroundDecoration,
|
||||
scaleStateChangedCallback: onScaleChanged,
|
||||
|
@ -107,7 +104,7 @@ class ImageView extends StatelessWidget {
|
|||
// we still provide a `loadingBuilder` in that case to avoid a black frame after hero animation
|
||||
loadingBuilder: (context, event) => loadingBuilder(
|
||||
context,
|
||||
imageCache.statusForKey(uriImage).keepAlive ? uriImage : thumbnailProvider,
|
||||
imageCache.statusForKey(uriImage).keepAlive ? uriImage : fastThumbnailProvider,
|
||||
),
|
||||
loadFailedChild: const EmptyContent(
|
||||
icon: AIcons.error,
|
||||
|
|
Loading…
Reference in a new issue