thumbnail & transition image loading review
This commit is contained in:
parent
1a50fcc65e
commit
c9fa903309
29 changed files with 385 additions and 408 deletions
|
@ -2,7 +2,7 @@ import 'dart:ui' as ui show Codec;
|
|||
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class AppIconImage extends ImageProvider<AppIconImageKey> {
|
||||
const AppIconImage({
|
||||
|
|
|
@ -2,10 +2,9 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||
final RegionProviderKey key;
|
||||
|
@ -23,7 +22,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
codec: _loadAsync(key, decode),
|
||||
scale: key.scale,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, regionRect=${key.regionRect}');
|
||||
yield ErrorDescription('uri=${key.uri}, page=${key.page}, mimeType=${key.mimeType}, region=${key.region}');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -39,7 +38,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
key.rotationDegrees,
|
||||
key.isFlipped,
|
||||
key.sampleSize,
|
||||
key.regionRect,
|
||||
key.region,
|
||||
key.imageSize,
|
||||
page: page,
|
||||
taskKey: key,
|
||||
|
@ -64,21 +63,23 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
|||
}
|
||||
|
||||
class RegionProviderKey {
|
||||
// do not store the entry as it is, because the key should be constant
|
||||
// but the entry attributes may change over time
|
||||
final String uri, mimeType;
|
||||
final int rotationDegrees, sampleSize, page;
|
||||
final int page, rotationDegrees, sampleSize;
|
||||
final bool isFlipped;
|
||||
final Rectangle<int> regionRect;
|
||||
final Rectangle<int> region;
|
||||
final Size imageSize;
|
||||
final double scale;
|
||||
|
||||
const RegionProviderKey({
|
||||
@required this.uri,
|
||||
@required this.mimeType,
|
||||
@required this.page,
|
||||
@required this.rotationDegrees,
|
||||
@required this.isFlipped,
|
||||
this.page,
|
||||
@required this.sampleSize,
|
||||
@required this.regionRect,
|
||||
@required this.region,
|
||||
@required this.imageSize,
|
||||
this.scale = 1.0,
|
||||
}) : assert(uri != null),
|
||||
|
@ -86,48 +87,29 @@ class RegionProviderKey {
|
|||
assert(rotationDegrees != null),
|
||||
assert(isFlipped != null),
|
||||
assert(sampleSize != null),
|
||||
assert(regionRect != null),
|
||||
assert(region != null),
|
||||
assert(imageSize != null),
|
||||
assert(scale != null);
|
||||
|
||||
// do not store the entry as it is, because the key should be constant
|
||||
// but the entry attributes may change over time
|
||||
factory RegionProviderKey.fromEntry(
|
||||
ImageEntry entry, {
|
||||
@required int sampleSize,
|
||||
@required Rectangle<int> rect,
|
||||
}) {
|
||||
return RegionProviderKey(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
page: entry.page,
|
||||
sampleSize: sampleSize,
|
||||
regionRect: rect,
|
||||
imageSize: Size(entry.width.toDouble(), entry.height.toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale;
|
||||
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.region == region && other.imageSize == imageSize && other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
uri,
|
||||
mimeType,
|
||||
page,
|
||||
rotationDegrees,
|
||||
isFlipped,
|
||||
page,
|
||||
sampleSize,
|
||||
regionRect,
|
||||
region,
|
||||
imageSize,
|
||||
scale,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale}';
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, region=$region, imageSize=$imageSize, scale=$scale}';
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import 'dart:ui' as ui show Codec;
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
||||
final ThumbnailProviderKey key;
|
||||
|
@ -35,14 +34,13 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
final page = key.page;
|
||||
try {
|
||||
final bytes = await ImageFileService.getThumbnail(
|
||||
uri,
|
||||
mimeType,
|
||||
key.dateModifiedSecs,
|
||||
key.rotationDegrees,
|
||||
key.isFlipped,
|
||||
key.extent,
|
||||
key.extent,
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
rotationDegrees: key.rotationDegrees,
|
||||
isFlipped: key.isFlipped,
|
||||
dateModifiedSecs: key.dateModifiedSecs,
|
||||
extent: key.extent,
|
||||
taskKey: key,
|
||||
);
|
||||
if (bytes == null) {
|
||||
|
@ -65,61 +63,49 @@ class ThumbnailProvider extends ImageProvider<ThumbnailProviderKey> {
|
|||
}
|
||||
|
||||
class ThumbnailProviderKey {
|
||||
// do not store the entry as it is, because the key should be constant
|
||||
// but the entry attributes may change over time
|
||||
final String uri, mimeType;
|
||||
final int dateModifiedSecs, rotationDegrees, page;
|
||||
final int page, rotationDegrees;
|
||||
final bool isFlipped;
|
||||
final int dateModifiedSecs;
|
||||
final double extent, scale;
|
||||
|
||||
const ThumbnailProviderKey({
|
||||
@required this.uri,
|
||||
@required this.mimeType,
|
||||
@required this.dateModifiedSecs,
|
||||
@required this.page,
|
||||
@required this.rotationDegrees,
|
||||
@required this.isFlipped,
|
||||
this.page,
|
||||
@required this.dateModifiedSecs,
|
||||
this.extent = 0,
|
||||
this.scale = 1,
|
||||
}) : assert(uri != null),
|
||||
assert(mimeType != null),
|
||||
assert(dateModifiedSecs != null),
|
||||
assert(rotationDegrees != null),
|
||||
assert(isFlipped != null),
|
||||
assert(dateModifiedSecs != null),
|
||||
assert(extent != null),
|
||||
assert(scale != null);
|
||||
|
||||
// do not store the entry as it is, because the key should be constant
|
||||
// but the entry attributes may change over time
|
||||
factory ThumbnailProviderKey.fromEntry(ImageEntry entry, {double extent = 0}) {
|
||||
return ThumbnailProviderKey(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
// `dateModifiedSecs` can be missing in viewer mode
|
||||
dateModifiedSecs: entry.dateModifiedSecs ?? -1,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
page: entry.page,
|
||||
extent: extent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is ThumbnailProviderKey && other.uri == uri && other.extent == extent && other.mimeType == mimeType && other.dateModifiedSecs == dateModifiedSecs && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.page == page && other.scale == scale;
|
||||
return other is ThumbnailProviderKey && other.uri == uri && other.mimeType == mimeType && other.page == page && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.dateModifiedSecs == dateModifiedSecs && other.extent == extent && other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
uri,
|
||||
mimeType,
|
||||
dateModifiedSecs,
|
||||
page,
|
||||
rotationDegrees,
|
||||
isFlipped,
|
||||
page,
|
||||
dateModifiedSecs,
|
||||
extent,
|
||||
scale,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, dateModifiedSecs=$dateModifiedSecs, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, page=$page, extent=$extent, scale=$scale}';
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, mimeType=$mimeType, page=$page, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, dateModifiedSecs=$dateModifiedSecs, extent=$extent, scale=$scale}';
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:ui' as ui show Codec;
|
|||
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
class UriImage extends ImageProvider<UriImage> {
|
||||
|
@ -15,7 +15,7 @@ class UriImage extends ImageProvider<UriImage> {
|
|||
const UriImage({
|
||||
@required this.uri,
|
||||
@required this.mimeType,
|
||||
this.page,
|
||||
@required this.page,
|
||||
@required this.rotationDegrees,
|
||||
@required this.isFlipped,
|
||||
this.expectedContentLength,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
||||
|
@ -30,7 +30,7 @@ class UriPicture extends PictureProvider<UriPicture> {
|
|||
Future<PictureInfo> _loadAsync(UriPicture key, {PictureErrorListener onError}) async {
|
||||
assert(key == this);
|
||||
|
||||
final data = await ImageFileService.getImage(uri, mimeType, 0, false);
|
||||
final data = await ImageFileService.getSvg(uri, mimeType);
|
||||
if (data == null || data.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -47,8 +47,8 @@ class _AvesAppState extends State<AvesApp> {
|
|||
// observers are not registered when using the same list object with different items
|
||||
// the list itself needs to be reassigned
|
||||
List<NavigatorObserver> _navigatorObservers = [];
|
||||
final _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
|
||||
final _navigatorKey = GlobalKey<NavigatorState>();
|
||||
final EventChannel _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
|
||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||
|
||||
static const accentColor = Colors.indigoAccent;
|
||||
|
||||
|
|
|
@ -13,11 +13,13 @@ class EntryCache {
|
|||
bool oldIsFlipped,
|
||||
) async {
|
||||
// TODO TLAD provide page parameter for multipage items, if someday image editing features are added for them
|
||||
int page;
|
||||
|
||||
// evict fullscreen image
|
||||
await UriImage(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
rotationDegrees: oldRotationDegrees,
|
||||
isFlipped: oldIsFlipped,
|
||||
).evict();
|
||||
|
@ -26,6 +28,7 @@ class EntryCache {
|
|||
await ThumbnailProvider(ThumbnailProviderKey(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
dateModifiedSecs: dateModifiedSecs,
|
||||
rotationDegrees: oldRotationDegrees,
|
||||
isFlipped: oldIsFlipped,
|
||||
|
@ -38,6 +41,7 @@ class EntryCache {
|
|||
(extent) => ThumbnailProvider(ThumbnailProviderKey(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
dateModifiedSecs: dateModifiedSecs,
|
||||
rotationDegrees: oldRotationDegrees,
|
||||
isFlipped: oldIsFlipped,
|
||||
|
|
63
lib/model/entry_images.dart
Normal file
63
lib/model/entry_images.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/image_providers/region_provider.dart';
|
||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension ExtraAvesEntry on ImageEntry {
|
||||
ThumbnailProvider getThumbnail({double extent = 0}) => ThumbnailProvider(_getThumbnailProviderKey(extent));
|
||||
|
||||
ThumbnailProviderKey _getThumbnailProviderKey(double extent) {
|
||||
// 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)
|
||||
final requestExtent = extent == 0 ? .0 : pow(2, (log(extent) / log(2)).ceil()).toDouble();
|
||||
|
||||
return ThumbnailProviderKey(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
dateModifiedSecs: dateModifiedSecs ?? -1,
|
||||
extent: requestExtent,
|
||||
);
|
||||
}
|
||||
|
||||
RegionProvider getRegion({@required int sampleSize, Rectangle<int> region}) => RegionProvider(getRegionProviderKey(sampleSize, region));
|
||||
|
||||
RegionProviderKey getRegionProviderKey(int sampleSize, Rectangle<int> region) {
|
||||
return RegionProviderKey(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
sampleSize: sampleSize,
|
||||
region: region ?? Rectangle<int>(0, 0, width, height),
|
||||
imageSize: Size(width.toDouble(), height.toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
UriImage get uriImage => UriImage(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
expectedContentLength: sizeBytes,
|
||||
);
|
||||
|
||||
bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive;
|
||||
|
||||
ImageProvider getBestThumbnail(double extent) {
|
||||
final sizedThumbnailKey = _getThumbnailProviderKey(extent);
|
||||
if (_isReady(sizedThumbnailKey)) return ThumbnailProvider(sizedThumbnailKey);
|
||||
|
||||
return getThumbnail();
|
||||
}
|
||||
}
|
|
@ -31,6 +31,8 @@ class ImageEntry {
|
|||
int sourceRotationDegrees;
|
||||
final int sizeBytes;
|
||||
String sourceTitle;
|
||||
|
||||
// `dateModifiedSecs` can be missing in viewer mode
|
||||
int _dateModifiedSecs;
|
||||
final int sourceDateTakenMillis;
|
||||
final int durationMillis;
|
||||
|
@ -227,7 +229,11 @@ class ImageEntry {
|
|||
!isAnimated &&
|
||||
page == null;
|
||||
|
||||
bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||
bool get supportTiling => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||
|
||||
// as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved
|
||||
// so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution
|
||||
bool get useTiles => supportTiling && (width > 4096 || height > 4096 || is360);
|
||||
|
||||
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
||||
|
||||
|
|
|
@ -26,18 +26,18 @@ class AppShortcutService {
|
|||
return false;
|
||||
}
|
||||
|
||||
static Future<void> pin(String label, ImageEntry iconEntry, Set<CollectionFilter> filters) async {
|
||||
static Future<void> pin(String label, ImageEntry entry, Set<CollectionFilter> filters) async {
|
||||
Uint8List iconBytes;
|
||||
if (iconEntry != null) {
|
||||
final size = iconEntry.isVideo ? 0.0 : 256.0;
|
||||
if (entry != null) {
|
||||
final size = entry.isVideo ? 0.0 : 256.0;
|
||||
iconBytes = await ImageFileService.getThumbnail(
|
||||
iconEntry.uri,
|
||||
iconEntry.mimeType,
|
||||
iconEntry.dateModifiedSecs,
|
||||
iconEntry.rotationDegrees,
|
||||
iconEntry.isFlipped,
|
||||
size,
|
||||
size,
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
page: entry.page,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
dateModifiedSecs: entry.dateModifiedSecs,
|
||||
extent: size,
|
||||
);
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -69,6 +69,21 @@ class ImageFileService {
|
|||
return null;
|
||||
}
|
||||
|
||||
static Future<Uint8List> getSvg(
|
||||
String uri,
|
||||
String mimeType, {
|
||||
int expectedContentLength,
|
||||
BytesReceivedCallback onBytesReceived,
|
||||
}) =>
|
||||
getImage(
|
||||
uri,
|
||||
mimeType,
|
||||
0,
|
||||
false,
|
||||
expectedContentLength: expectedContentLength,
|
||||
onBytesReceived: onBytesReceived,
|
||||
);
|
||||
|
||||
static Future<Uint8List> getImage(
|
||||
String uri,
|
||||
String mimeType,
|
||||
|
@ -155,15 +170,14 @@ class ImageFileService {
|
|||
);
|
||||
}
|
||||
|
||||
static Future<Uint8List> getThumbnail(
|
||||
String uri,
|
||||
String mimeType,
|
||||
int dateModifiedSecs,
|
||||
int rotationDegrees,
|
||||
bool isFlipped,
|
||||
double width,
|
||||
double height, {
|
||||
int page,
|
||||
static Future<Uint8List> getThumbnail({
|
||||
@required String uri,
|
||||
@required String mimeType,
|
||||
@required int rotationDegrees,
|
||||
@required int page,
|
||||
@required bool isFlipped,
|
||||
@required int dateModifiedSecs,
|
||||
@required double extent,
|
||||
Object taskKey,
|
||||
int priority,
|
||||
}) {
|
||||
|
@ -179,8 +193,8 @@ class ImageFileService {
|
|||
'dateModifiedSecs': dateModifiedSecs,
|
||||
'rotationDegrees': rotationDegrees,
|
||||
'isFlipped': isFlipped,
|
||||
'widthDip': width,
|
||||
'heightDip': height,
|
||||
'widthDip': extent,
|
||||
'heightDip': extent,
|
||||
'page': page,
|
||||
'defaultSizeDip': thumbnailDefaultSize,
|
||||
});
|
||||
|
@ -191,7 +205,7 @@ class ImageFileService {
|
|||
return null;
|
||||
},
|
||||
// debugLabel: 'getThumbnail width=$width, height=$height entry=${entry.filenameWithoutExtension}',
|
||||
priority: priority ?? (width == 0 || height == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail),
|
||||
priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail),
|
||||
key: taskKey,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class SvgMetadataService {
|
|||
|
||||
static Future<Size> getSize(ImageEntry entry) async {
|
||||
try {
|
||||
final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false);
|
||||
final data = await ImageFileService.getSvg(entry.uri, entry.mimeType);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
@ -59,7 +59,7 @@ class SvgMetadataService {
|
|||
}
|
||||
|
||||
try {
|
||||
final data = await ImageFileService.getImage(entry.uri, entry.mimeType, 0, false);
|
||||
final data = await ImageFileService.getSvg(entry.uri, entry.mimeType);
|
||||
|
||||
final document = XmlDocument.parse(utf8.decode(data));
|
||||
final root = document.rootElement;
|
||||
|
|
|
@ -25,7 +25,7 @@ class FilterBar extends StatefulWidget implements PreferredSizeWidget {
|
|||
}
|
||||
|
||||
class _FilterBarState extends State<FilterBar> {
|
||||
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey();
|
||||
final GlobalKey<AnimatedListState> _animatedListKey = GlobalKey(debugLabel: 'filter-bar-animated-list');
|
||||
CollectionFilter _userRemovedFilter;
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/error.dart';
|
||||
|
@ -37,11 +35,6 @@ class _RasterImageThumbnailState extends State<RasterImageThumbnail> {
|
|||
|
||||
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();
|
||||
|
@ -76,13 +69,9 @@ class _RasterImageThumbnailState extends State<RasterImageThumbnail> {
|
|||
void _initProvider() {
|
||||
if (!entry.canDecode) return;
|
||||
|
||||
_fastThumbnailProvider = ThumbnailProvider(
|
||||
ThumbnailProviderKey.fromEntry(entry),
|
||||
);
|
||||
_fastThumbnailProvider = entry.getThumbnail();
|
||||
if (!entry.isVideo) {
|
||||
_sizedThumbnailProvider = ThumbnailProvider(
|
||||
ThumbnailProviderKey.fromEntry(entry, extent: requestExtent),
|
||||
);
|
||||
_sizedThumbnailProvider = entry.getThumbnail(extent: extent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,22 +135,8 @@ class _RasterImageThumbnailState extends State<RasterImageThumbnail> {
|
|||
: Hero(
|
||||
tag: heroTag,
|
||||
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
|
||||
ImageProvider heroImageProvider = _fastThumbnailProvider;
|
||||
if (!entry.isVideo) {
|
||||
final imageProvider = UriImage(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
page: entry.page,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
expectedContentLength: entry.sizeBytes,
|
||||
);
|
||||
if (imageCache.statusForKey(imageProvider).keepAlive) {
|
||||
heroImageProvider = imageProvider;
|
||||
}
|
||||
}
|
||||
return TransitionImage(
|
||||
image: heroImageProvider,
|
||||
image: entry.getBestThumbnail(extent),
|
||||
animation: animation,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ class ThumbnailCollection extends StatelessWidget {
|
|||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
||||
final GlobalKey _scrollableKey = GlobalKey();
|
||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
||||
|
||||
static const columnCountDefault = 4;
|
||||
static const extentMin = 46.0;
|
||||
|
|
|
@ -15,7 +15,6 @@ class MagnifierCore extends StatefulWidget {
|
|||
Key key,
|
||||
@required this.child,
|
||||
@required this.onTap,
|
||||
@required this.gestureDetectorBehavior,
|
||||
@required this.controller,
|
||||
@required this.scaleStateCycle,
|
||||
@required this.applyScale,
|
||||
|
@ -29,7 +28,6 @@ class MagnifierCore extends StatefulWidget {
|
|||
|
||||
final MagnifierTapCallback onTap;
|
||||
|
||||
final HitTestBehavior gestureDetectorBehavior;
|
||||
final bool applyScale;
|
||||
final double panInertia;
|
||||
|
||||
|
|
|
@ -18,106 +18,64 @@ import 'package:flutter/material.dart';
|
|||
- added single & double tap position feedback
|
||||
- fixed focus when scaling by double-tap/pinch
|
||||
*/
|
||||
class Magnifier extends StatefulWidget {
|
||||
class Magnifier extends StatelessWidget {
|
||||
const Magnifier({
|
||||
Key key,
|
||||
@required this.child,
|
||||
this.childSize,
|
||||
this.controller,
|
||||
this.maxScale,
|
||||
this.minScale,
|
||||
this.initialScale,
|
||||
this.scaleStateCycle,
|
||||
@required this.controller,
|
||||
@required this.childSize,
|
||||
this.minScale = const ScaleLevel(factor: .0),
|
||||
this.maxScale = const ScaleLevel(factor: double.infinity),
|
||||
this.initialScale = const ScaleLevel(ref: ScaleReference.contained),
|
||||
this.scaleStateCycle = defaultScaleStateCycle,
|
||||
this.applyScale = true,
|
||||
this.onTap,
|
||||
this.gestureDetectorBehavior,
|
||||
this.applyScale,
|
||||
}) : super(key: key);
|
||||
@required this.child,
|
||||
}) : assert(controller != null),
|
||||
assert(childSize != null),
|
||||
assert(minScale != null),
|
||||
assert(maxScale != null),
|
||||
assert(initialScale != null),
|
||||
assert(scaleStateCycle != null),
|
||||
assert(applyScale != null),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final MagnifierController controller;
|
||||
|
||||
// The size of the custom [child]. This value is used to compute the relation between the child and the container's size to calculate the scale value.
|
||||
final Size childSize;
|
||||
|
||||
// Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size.
|
||||
final ScaleLevel maxScale;
|
||||
|
||||
// Defines the minimum size in which the image will be allowed to assume, it is proportional to the original image size.
|
||||
final ScaleLevel minScale;
|
||||
|
||||
// Defines the maximum size in which the image will be allowed to assume, it is proportional to the original image size.
|
||||
final ScaleLevel maxScale;
|
||||
|
||||
// Defines the size the image will assume when the component is initialized, it is proportional to the original image size.
|
||||
final ScaleLevel initialScale;
|
||||
|
||||
final MagnifierController controller;
|
||||
final ScaleStateCycle scaleStateCycle;
|
||||
final MagnifierTapCallback onTap;
|
||||
final HitTestBehavior gestureDetectorBehavior;
|
||||
final bool applyScale;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MagnifierState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MagnifierState extends State<Magnifier> {
|
||||
bool _controlledController;
|
||||
MagnifierController _controller;
|
||||
|
||||
Size get childSize => widget.childSize;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller == null) {
|
||||
_controlledController = true;
|
||||
_controller = MagnifierController();
|
||||
} else {
|
||||
_controlledController = false;
|
||||
_controller = widget.controller;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant Magnifier oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller == null) {
|
||||
if (!_controlledController) {
|
||||
_controlledController = true;
|
||||
_controller = MagnifierController();
|
||||
}
|
||||
} else {
|
||||
_controlledController = false;
|
||||
_controller = widget.controller;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controlledController) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
final MagnifierTapCallback onTap;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
_controller.setScaleBoundaries(ScaleBoundaries(
|
||||
widget.minScale ?? 0.0,
|
||||
widget.maxScale ?? ScaleLevel(factor: double.infinity),
|
||||
widget.initialScale ?? ScaleLevel(ref: ScaleReference.contained),
|
||||
constraints.biggest,
|
||||
widget.childSize?.isEmpty == true ? constraints.biggest : widget.childSize,
|
||||
controller.setScaleBoundaries(ScaleBoundaries(
|
||||
minScale: minScale,
|
||||
maxScale: maxScale,
|
||||
initialScale: initialScale,
|
||||
viewportSize: constraints.biggest,
|
||||
childSize: childSize?.isEmpty == false ? childSize : constraints.biggest,
|
||||
));
|
||||
|
||||
return MagnifierCore(
|
||||
child: widget.child,
|
||||
controller: _controller,
|
||||
scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
|
||||
onTap: widget.onTap,
|
||||
gestureDetectorBehavior: widget.gestureDetectorBehavior,
|
||||
applyScale: widget.applyScale ?? true,
|
||||
child: child,
|
||||
controller: controller,
|
||||
scaleStateCycle: scaleStateCycle,
|
||||
onTap: onTap,
|
||||
applyScale: applyScale,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -7,20 +7,22 @@ import 'package:flutter/foundation.dart';
|
|||
/// Internal class to wrap custom scale boundaries (min, max and initial)
|
||||
/// Also, stores values regarding the two sizes: the container and the child.
|
||||
class ScaleBoundaries {
|
||||
const ScaleBoundaries(
|
||||
this._minScale,
|
||||
this._maxScale,
|
||||
this._initialScale,
|
||||
this.viewportSize,
|
||||
this.childSize,
|
||||
);
|
||||
|
||||
final ScaleLevel _minScale;
|
||||
final ScaleLevel _maxScale;
|
||||
final ScaleLevel _initialScale;
|
||||
final Size viewportSize;
|
||||
final Size childSize;
|
||||
|
||||
const ScaleBoundaries({
|
||||
@required ScaleLevel minScale,
|
||||
@required ScaleLevel maxScale,
|
||||
@required ScaleLevel initialScale,
|
||||
@required this.viewportSize,
|
||||
@required this.childSize,
|
||||
}) : _minScale = minScale,
|
||||
_maxScale = maxScale,
|
||||
_initialScale = initialScale;
|
||||
|
||||
double _scaleForLevel(ScaleLevel level) {
|
||||
final factor = level.factor;
|
||||
switch (level.ref) {
|
||||
|
|
|
@ -40,7 +40,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
|||
|
||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||
final GlobalKey _scrollableKey = GlobalKey();
|
||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
||||
|
||||
static const columnCountDefault = 2;
|
||||
static const extentMin = 60.0;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/image_providers/uri_picture_provider.dart';
|
||||
import 'package:aves/main.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/viewer/debug/db.dart';
|
||||
|
@ -135,18 +135,14 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
Text('Raster (fast)'),
|
||||
Center(
|
||||
child: Image(
|
||||
image: ThumbnailProvider(
|
||||
ThumbnailProviderKey.fromEntry(entry),
|
||||
),
|
||||
image: entry.getThumbnail(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Raster ($extent)'),
|
||||
Center(
|
||||
child: Image(
|
||||
image: ThumbnailProvider(
|
||||
ThumbnailProviderKey.fromEntry(entry, extent: extent),
|
||||
),
|
||||
image: entry.getThumbnail(extent: extent),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -158,7 +158,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: SourceViewerPage.routeName),
|
||||
builder: (context) => SourceViewerPage(
|
||||
loader: () => ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode),
|
||||
loader: () => ImageFileService.getSvg(entry.uri, entry.mimeType).then(utf8.decode),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/widgets/viewer/multipage.dart';
|
|||
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class MultiEntryScroller extends StatefulWidget {
|
||||
|
@ -79,16 +80,22 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
|||
);
|
||||
}
|
||||
|
||||
EntryPageView _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) {
|
||||
return EntryPageView(
|
||||
key: Key('imageview'),
|
||||
mainEntry: entry,
|
||||
multiPageInfo: multiPageInfo,
|
||||
page: page,
|
||||
heroTag: widget.collection.heroTag(entry),
|
||||
onTap: (_) => widget.onTap?.call(),
|
||||
videoControllers: widget.videoControllers,
|
||||
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
|
||||
Widget _buildViewer(ImageEntry entry, {MultiPageInfo multiPageInfo, int page}) {
|
||||
return Selector<MediaQueryData, Size>(
|
||||
selector: (c, mq) => mq.size,
|
||||
builder: (c, mqSize, child) {
|
||||
return EntryPageView(
|
||||
key: Key('imageview'),
|
||||
mainEntry: entry,
|
||||
multiPageInfo: multiPageInfo,
|
||||
page: page,
|
||||
viewportSize: mqSize,
|
||||
heroTag: widget.collection.heroTag(entry),
|
||||
onTap: (_) => widget.onTap?.call(),
|
||||
videoControllers: widget.videoControllers,
|
||||
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -150,13 +157,19 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
|||
);
|
||||
}
|
||||
|
||||
EntryPageView _buildViewer({MultiPageInfo multiPageInfo, int page}) {
|
||||
return EntryPageView(
|
||||
mainEntry: entry,
|
||||
multiPageInfo: multiPageInfo,
|
||||
page: page,
|
||||
onTap: (_) => widget.onTap?.call(),
|
||||
videoControllers: widget.videoControllers,
|
||||
Widget _buildViewer({MultiPageInfo multiPageInfo, int page}) {
|
||||
return Selector<MediaQueryData, Size>(
|
||||
selector: (c, mq) => mq.size,
|
||||
builder: (c, mqSize, child) {
|
||||
return EntryPageView(
|
||||
mainEntry: entry,
|
||||
multiPageInfo: multiPageInfo,
|
||||
page: page,
|
||||
viewportSize: mqSize,
|
||||
onTap: (_) => widget.onTap?.call(),
|
||||
videoControllers: widget.videoControllers,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
|||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: widget.markers.map((i) {
|
||||
final key = GlobalKey();
|
||||
final key = GlobalKey(debugLabel: 'map-marker-$i');
|
||||
_globalKeys.add(key);
|
||||
return RepaintBoundary(
|
||||
key: key,
|
||||
|
|
|
@ -28,7 +28,7 @@ class VideoControlOverlay extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _progressBarKey = GlobalKey();
|
||||
final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar');
|
||||
bool _playingOnDragStart = false;
|
||||
AnimationController _playPauseAnimation;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/panorama.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -72,14 +72,7 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
);
|
||||
},
|
||||
child: Image(
|
||||
image: UriImage(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
page: entry.page,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
expectedContentLength: entry.sizeBytes,
|
||||
),
|
||||
image: entry.uriImage,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/image_file_service.dart';
|
||||
import 'package:aves/services/metadata_service.dart';
|
||||
|
@ -47,37 +47,25 @@ class EntryPrinter {
|
|||
final multiPageInfo = await MetadataService.getMultiPageInfo(entry);
|
||||
if (multiPageInfo.pageCount > 1) {
|
||||
for (final kv in multiPageInfo.pages.entries) {
|
||||
_addPdfPage(await _buildPageImage(page: kv.key));
|
||||
final pageEntry = entry.getPageEntry(multiPageInfo: multiPageInfo, page: kv.key);
|
||||
_addPdfPage(await _buildPageImage(pageEntry));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pages.isEmpty) {
|
||||
_addPdfPage(await _buildPageImage());
|
||||
_addPdfPage(await _buildPageImage(entry));
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
Future<pdf.Widget> _buildPageImage({int page}) async {
|
||||
final uri = entry.uri;
|
||||
final mimeType = entry.mimeType;
|
||||
final rotationDegrees = entry.rotationDegrees;
|
||||
final isFlipped = entry.isFlipped;
|
||||
|
||||
Future<pdf.Widget> _buildPageImage(ImageEntry entry) async {
|
||||
if (entry.isSvg) {
|
||||
final bytes = await ImageFileService.getImage(uri, mimeType, rotationDegrees, isFlipped);
|
||||
final bytes = await ImageFileService.getSvg(entry.uri, entry.mimeType);
|
||||
if (bytes != null && bytes.isNotEmpty) {
|
||||
return pdf.SvgImage(svg: utf8.decode(bytes));
|
||||
}
|
||||
} else {
|
||||
return pdf.Image(await flutterImageProvider(
|
||||
UriImage(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
page: page,
|
||||
rotationDegrees: rotationDegrees,
|
||||
isFlipped: isFlipped,
|
||||
),
|
||||
));
|
||||
return pdf.Image(await flutterImageProvider(entry.uriImage));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -26,21 +26,20 @@ class EntryPageView extends StatefulWidget {
|
|||
final ImageEntry entry;
|
||||
final MultiPageInfo multiPageInfo;
|
||||
final int page;
|
||||
final Size viewportSize;
|
||||
final Object heroTag;
|
||||
final MagnifierTapCallback onTap;
|
||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||
final VoidCallback onDisposed;
|
||||
|
||||
static const decorationCheckSize = 20.0;
|
||||
static const initialScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
static const minScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
static const maxScale = ScaleLevel(factor: 2.0);
|
||||
|
||||
EntryPageView({
|
||||
Key key,
|
||||
ImageEntry mainEntry,
|
||||
this.multiPageInfo,
|
||||
this.page,
|
||||
this.viewportSize,
|
||||
this.heroTag,
|
||||
@required this.onTap,
|
||||
@required this.videoControllers,
|
||||
|
@ -59,8 +58,14 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
Size get viewportSize => widget.viewportSize;
|
||||
|
||||
MagnifierTapCallback get onTap => widget.onTap;
|
||||
|
||||
static const initialScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
static const minScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
static const maxScale = ScaleLevel(factor: 2.0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -87,13 +92,28 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
}
|
||||
|
||||
void _registerWidget() {
|
||||
_viewStateNotifier.value = ViewState.zero;
|
||||
// try to initialize the view state to match magnifier initial state
|
||||
_viewStateNotifier.value = viewportSize != null
|
||||
? ViewState(
|
||||
Offset.zero,
|
||||
ScaleBoundaries(
|
||||
minScale: minScale,
|
||||
maxScale: maxScale,
|
||||
initialScale: initialScale,
|
||||
viewportSize: viewportSize,
|
||||
childSize: entry.displaySize,
|
||||
).initialScale,
|
||||
viewportSize,
|
||||
)
|
||||
: ViewState.zero;
|
||||
|
||||
_magnifierController = MagnifierController();
|
||||
_subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
|
||||
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
|
||||
}
|
||||
|
||||
void _unregisterWidget() {
|
||||
_magnifierController.dispose();
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
|
@ -113,8 +133,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
}
|
||||
child ??= ErrorView(onTap: () => onTap?.call(null));
|
||||
|
||||
// no hero for videos, as a typical video first frame is different from its thumbnail
|
||||
return widget.heroTag != null && !entry.isVideo
|
||||
return widget.heroTag != null
|
||||
? Hero(
|
||||
tag: widget.heroTag,
|
||||
transitionOnUserGestures: true,
|
||||
|
@ -124,21 +143,13 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
}
|
||||
|
||||
Widget _buildRasterView() {
|
||||
return Magnifier(
|
||||
// key includes size and orientation to refresh when the image is rotated
|
||||
key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
|
||||
child: TiledImageView(
|
||||
return _buildMagnifier(
|
||||
applyScale: false,
|
||||
child: RasterImageView(
|
||||
entry: entry,
|
||||
viewStateNotifier: _viewStateNotifier,
|
||||
errorBuilder: (context, error, stackTrace) => ErrorView(onTap: () => onTap?.call(null)),
|
||||
),
|
||||
childSize: entry.displaySize,
|
||||
controller: _magnifierController,
|
||||
maxScale: EntryPageView.maxScale,
|
||||
minScale: EntryPageView.minScale,
|
||||
initialScale: EntryPageView.initialScale,
|
||||
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
|
||||
applyScale: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -146,7 +157,9 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
final background = settings.vectorBackground;
|
||||
final colorFilter = background.isColor ? ColorFilter.mode(background.color, BlendMode.dstOver) : null;
|
||||
|
||||
Widget child = Magnifier(
|
||||
var child = _buildMagnifier(
|
||||
maxScale: ScaleLevel(factor: double.infinity),
|
||||
scaleStateCycle: _vectorScaleStateCycle,
|
||||
child: SvgPicture(
|
||||
UriPicture(
|
||||
uri: entry.uri,
|
||||
|
@ -154,12 +167,6 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
colorFilter: colorFilter,
|
||||
),
|
||||
),
|
||||
childSize: entry.displaySize,
|
||||
controller: _magnifierController,
|
||||
minScale: EntryPageView.minScale,
|
||||
initialScale: EntryPageView.initialScale,
|
||||
scaleStateCycle: _vectorScaleStateCycle,
|
||||
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
|
||||
);
|
||||
|
||||
if (background == EntryBackground.checkered) {
|
||||
|
@ -174,19 +181,33 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
|
||||
Widget _buildVideoView() {
|
||||
final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||
if (videoController == null) return SizedBox();
|
||||
return _buildMagnifier(
|
||||
child: VideoView(
|
||||
entry: entry,
|
||||
controller: videoController,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMagnifier({
|
||||
ScaleLevel maxScale = maxScale,
|
||||
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
|
||||
bool applyScale = true,
|
||||
@required Widget child,
|
||||
}) {
|
||||
return Magnifier(
|
||||
child: videoController != null
|
||||
? AvesVideo(
|
||||
entry: entry,
|
||||
controller: videoController,
|
||||
)
|
||||
: SizedBox(),
|
||||
childSize: entry.displaySize,
|
||||
// key includes size and orientation to refresh when the image is rotated
|
||||
key: ValueKey('${entry.page}_${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
|
||||
controller: _magnifierController,
|
||||
maxScale: EntryPageView.maxScale,
|
||||
minScale: EntryPageView.minScale,
|
||||
initialScale: EntryPageView.initialScale,
|
||||
childSize: entry.displaySize,
|
||||
minScale: minScale,
|
||||
maxScale: maxScale,
|
||||
initialScale: initialScale,
|
||||
scaleStateCycle: scaleStateCycle,
|
||||
applyScale: applyScale,
|
||||
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/image_providers/region_provider.dart';
|
||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/model/settings/entry_background.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/utils/math_utils.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
|
||||
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
|
||||
import 'package:aves/widgets/viewer/visual/state.dart';
|
||||
|
@ -14,22 +14,22 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class TiledImageView extends StatefulWidget {
|
||||
class RasterImageView extends StatefulWidget {
|
||||
final ImageEntry entry;
|
||||
final ValueNotifier<ViewState> viewStateNotifier;
|
||||
final ImageErrorWidgetBuilder errorBuilder;
|
||||
|
||||
const TiledImageView({
|
||||
const RasterImageView({
|
||||
@required this.entry,
|
||||
@required this.viewStateNotifier,
|
||||
@required this.errorBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
_TiledImageViewState createState() => _TiledImageViewState();
|
||||
_RasterImageViewState createState() => _RasterImageViewState();
|
||||
}
|
||||
|
||||
class _TiledImageViewState extends State<TiledImageView> {
|
||||
class _RasterImageViewState extends State<RasterImageView> {
|
||||
Size _displaySize;
|
||||
bool _isTilingInitialized = false;
|
||||
int _maxSampleSize;
|
||||
|
@ -45,42 +45,16 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
|
||||
bool get useBackground => entry.canHaveAlpha && settings.rasterBackground != EntryBackground.transparent;
|
||||
|
||||
// as of panorama v0.3.1, the `Panorama` widget throws on initialization when the image is already resolved
|
||||
// so we use tiles for panoramas as a workaround to not collide with the `panorama` package resolution
|
||||
bool get useTiles => entry.canTile && (entry.displaySize.longestSide > 4096 || entry.is360);
|
||||
ViewState get viewState => viewStateNotifier.value;
|
||||
|
||||
ImageProvider get thumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry));
|
||||
ImageProvider get thumbnailProvider => entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName));
|
||||
|
||||
ImageProvider get fullImageProvider {
|
||||
if (useTiles) {
|
||||
if (entry.useTiles) {
|
||||
assert(_isTilingInitialized);
|
||||
final displayWidth = _displaySize.width.round();
|
||||
final displayHeight = _displaySize.height.round();
|
||||
final viewState = viewStateNotifier.value;
|
||||
final regionRect = _getTileRects(
|
||||
x: 0,
|
||||
y: 0,
|
||||
layerRegionWidth: displayWidth,
|
||||
layerRegionHeight: displayHeight,
|
||||
displayWidth: displayWidth,
|
||||
displayHeight: displayHeight,
|
||||
scale: viewState.scale,
|
||||
viewRect: _getViewRect(viewState, displayWidth, displayHeight),
|
||||
)?.item2;
|
||||
return RegionProvider(RegionProviderKey.fromEntry(
|
||||
entry,
|
||||
sampleSize: _maxSampleSize,
|
||||
rect: regionRect,
|
||||
));
|
||||
return entry.getRegion(sampleSize: _maxSampleSize);
|
||||
} else {
|
||||
return UriImage(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
page: entry.page,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
expectedContentLength: entry.sizeBytes,
|
||||
);
|
||||
return entry.uriImage;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,11 +66,11 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
super.initState();
|
||||
_displaySize = entry.displaySize;
|
||||
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
|
||||
if (!useTiles) _registerFullImage();
|
||||
if (!entry.useTiles) _registerFullImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TiledImageView oldWidget) {
|
||||
void didUpdateWidget(covariant RasterImageView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final oldViewState = oldWidget.viewStateNotifier.value;
|
||||
|
@ -133,11 +107,12 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
Widget build(BuildContext context) {
|
||||
if (viewStateNotifier == null) return SizedBox.shrink();
|
||||
|
||||
final useTiles = entry.useTiles;
|
||||
return ValueListenableBuilder<ViewState>(
|
||||
valueListenable: viewStateNotifier,
|
||||
builder: (context, viewState, child) {
|
||||
final viewportSize = viewState.viewportSize;
|
||||
final viewportSized = viewportSize != null;
|
||||
final viewportSized = viewportSize?.isEmpty == false;
|
||||
if (viewportSized && useTiles && !_isTilingInitialized) _initTiling(viewportSize);
|
||||
|
||||
return SizedBox.fromSize(
|
||||
|
@ -145,9 +120,9 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (useBackground && viewportSized) _buildBackground(viewState),
|
||||
_buildLoading(viewState),
|
||||
if (useTiles) ..._getTiles(viewState),
|
||||
if (useBackground && viewportSized) _buildBackground(),
|
||||
_buildLoading(),
|
||||
if (useTiles) ..._getTiles(),
|
||||
if (!useTiles)
|
||||
Image(
|
||||
image: fullImageProvider,
|
||||
|
@ -184,7 +159,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
_registerFullImage();
|
||||
}
|
||||
|
||||
Widget _buildLoading(ViewState viewState) {
|
||||
Widget _buildLoading() {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _fullImageLoaded,
|
||||
builder: (context, fullImageLoaded, child) {
|
||||
|
@ -204,7 +179,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildBackground(ViewState viewState) {
|
||||
Widget _buildBackground() {
|
||||
final viewportSize = viewState.viewportSize;
|
||||
assert(viewportSize != null);
|
||||
|
||||
|
@ -212,19 +187,30 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position;
|
||||
final decorationSize = applyBoxFit(BoxFit.none, viewSize, viewportSize).source;
|
||||
|
||||
Decoration decoration;
|
||||
Widget child;
|
||||
final background = settings.rasterBackground;
|
||||
if (background == EntryBackground.checkered) {
|
||||
final side = viewportSize.shortestSide;
|
||||
final checkSize = side / ((side / EntryPageView.decorationCheckSize).round());
|
||||
final offset = ((decorationSize - viewportSize) as Offset) / 2;
|
||||
decoration = CheckeredDecoration(
|
||||
checkSize: checkSize,
|
||||
offset: offset,
|
||||
child = ValueListenableBuilder(
|
||||
valueListenable: _fullImageLoaded,
|
||||
builder: (context, fullImageLoaded, child) {
|
||||
if (!fullImageLoaded) return SizedBox.shrink();
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: CheckeredDecoration(
|
||||
checkSize: checkSize,
|
||||
offset: offset,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
decoration = BoxDecoration(
|
||||
color: background.color,
|
||||
child = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: background.color,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Positioned(
|
||||
|
@ -232,36 +218,37 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
top: decorationOffset.dy >= 0 ? decorationOffset.dy : null,
|
||||
width: decorationSize.width,
|
||||
height: decorationSize.height,
|
||||
child: DecoratedBox(
|
||||
decoration: decoration,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _getTiles(ViewState viewState) {
|
||||
List<Widget> _getTiles() {
|
||||
if (!_isTilingInitialized) return [];
|
||||
|
||||
final displayWidth = _displaySize.width.round();
|
||||
final displayHeight = _displaySize.height.round();
|
||||
final viewRect = _getViewRect(viewState, displayWidth, displayHeight);
|
||||
final viewRect = _getViewRect(displayWidth, displayHeight);
|
||||
final scale = viewState.scale;
|
||||
|
||||
final tiles = <RegionTile>[];
|
||||
// for the largest sample size (matching the initial scale), the whole image is in view
|
||||
// so we subsample the whole image without tiling
|
||||
final fullImageRegionTile = RegionTile(
|
||||
entry: entry,
|
||||
tileRect: Rect.fromLTWH(0, 0, displayWidth * scale, displayHeight * scale),
|
||||
sampleSize: _maxSampleSize,
|
||||
);
|
||||
final tiles = [fullImageRegionTile];
|
||||
|
||||
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
||||
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
||||
// for the largest sample size (matching the initial scale), the whole image is in view
|
||||
// so we subsample the whole image without tiling
|
||||
final fullImageRegion = sampleSize == _maxSampleSize;
|
||||
int nextSampleSize(int sampleSize) => (sampleSize / 2).floor();
|
||||
for (var sampleSize = nextSampleSize(_maxSampleSize); sampleSize >= minSampleSize; sampleSize = nextSampleSize(sampleSize)) {
|
||||
final regionSide = (_tileSide * sampleSize).round();
|
||||
final layerRegionWidth = fullImageRegion ? displayWidth : regionSide;
|
||||
final layerRegionHeight = fullImageRegion ? displayHeight : regionSide;
|
||||
for (var x = 0; x < displayWidth; x += layerRegionWidth) {
|
||||
for (var y = 0; y < displayHeight; y += layerRegionHeight) {
|
||||
for (var x = 0; x < displayWidth; x += regionSide) {
|
||||
for (var y = 0; y < displayHeight; y += regionSide) {
|
||||
final rects = _getTileRects(
|
||||
x: x,
|
||||
y: y,
|
||||
layerRegionWidth: layerRegionWidth,
|
||||
layerRegionHeight: layerRegionHeight,
|
||||
regionSide: regionSide,
|
||||
displayWidth: displayWidth,
|
||||
displayHeight: displayHeight,
|
||||
scale: scale,
|
||||
|
@ -281,7 +268,7 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
return tiles;
|
||||
}
|
||||
|
||||
Rect _getViewRect(ViewState viewState, int displayWidth, int displayHeight) {
|
||||
Rect _getViewRect(int displayWidth, int displayHeight) {
|
||||
final scale = viewState.scale;
|
||||
final centerOffset = viewState.position;
|
||||
final viewportSize = viewState.viewportSize;
|
||||
|
@ -295,17 +282,16 @@ class _TiledImageViewState extends State<TiledImageView> {
|
|||
Tuple2<Rect, Rectangle<int>> _getTileRects({
|
||||
@required int x,
|
||||
@required int y,
|
||||
@required int layerRegionWidth,
|
||||
@required int layerRegionHeight,
|
||||
@required int regionSide,
|
||||
@required int displayWidth,
|
||||
@required int displayHeight,
|
||||
@required double scale,
|
||||
@required Rect viewRect,
|
||||
}) {
|
||||
final nextX = x + layerRegionWidth;
|
||||
final nextY = y + layerRegionHeight;
|
||||
final thisRegionWidth = layerRegionWidth - (nextX >= displayWidth ? nextX - displayWidth : 0);
|
||||
final thisRegionHeight = layerRegionHeight - (nextY >= displayHeight ? nextY - displayHeight : 0);
|
||||
final nextX = x + regionSide;
|
||||
final nextY = y + regionSide;
|
||||
final thisRegionWidth = regionSide - (nextX >= displayWidth ? nextX - displayWidth : 0);
|
||||
final thisRegionHeight = regionSide - (nextY >= displayHeight ? nextY - displayHeight : 0);
|
||||
final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale);
|
||||
|
||||
// only build visible tiles
|
||||
|
@ -348,12 +334,21 @@ class RegionTile extends StatefulWidget {
|
|||
const RegionTile({
|
||||
@required this.entry,
|
||||
@required this.tileRect,
|
||||
@required this.regionRect,
|
||||
this.regionRect,
|
||||
@required this.sampleSize,
|
||||
});
|
||||
|
||||
@override
|
||||
_RegionTileState createState() => _RegionTileState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('contentId', entry.contentId));
|
||||
properties.add(DiagnosticsProperty<Rect>('tileRect', tileRect));
|
||||
properties.add(DiagnosticsProperty<Rectangle<int>>('regionRect', regionRect));
|
||||
properties.add(IntProperty('sampleSize', sampleSize));
|
||||
}
|
||||
}
|
||||
|
||||
class _RegionTileState extends State<RegionTile> {
|
||||
|
@ -393,11 +388,10 @@ class _RegionTileState extends State<RegionTile> {
|
|||
void _initProvider() {
|
||||
if (!entry.canDecode) return;
|
||||
|
||||
_provider = RegionProvider(RegionProviderKey.fromEntry(
|
||||
entry,
|
||||
_provider = entry.getRegion(
|
||||
sampleSize: widget.sampleSize,
|
||||
rect: widget.regionRect,
|
||||
));
|
||||
region: widget.regionRect,
|
||||
);
|
||||
}
|
||||
|
||||
void _pauseProvider() => _provider?.pause();
|
||||
|
@ -440,12 +434,4 @@ class _RegionTileState extends State<RegionTile> {
|
|||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('contentId', widget.entry.contentId));
|
||||
properties.add(IntProperty('sampleSize', widget.sampleSize));
|
||||
properties.add(DiagnosticsProperty<Rectangle<int>>('regionRect', widget.regionRect));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/image_providers/uri_image_provider.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||
|
||||
class AvesVideo extends StatefulWidget {
|
||||
class VideoView extends StatefulWidget {
|
||||
final ImageEntry entry;
|
||||
final IjkMediaController controller;
|
||||
|
||||
const AvesVideo({
|
||||
const VideoView({
|
||||
Key key,
|
||||
@required this.entry,
|
||||
@required this.controller,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AvesVideoState();
|
||||
State<StatefulWidget> createState() => _VideoViewState();
|
||||
}
|
||||
|
||||
class _AvesVideoState extends State<AvesVideo> {
|
||||
class _VideoViewState extends State<VideoView> {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
@ -34,7 +34,7 @@ class _AvesVideoState extends State<AvesVideo> {
|
|||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AvesVideo oldWidget) {
|
||||
void didUpdateWidget(covariant VideoView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
|
@ -46,11 +46,11 @@ class _AvesVideoState extends State<AvesVideo> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(AvesVideo widget) {
|
||||
void _registerWidget(VideoView widget) {
|
||||
_subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish));
|
||||
}
|
||||
|
||||
void _unregisterWidget(AvesVideo widget) {
|
||||
void _unregisterWidget(VideoView widget) {
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
|
@ -98,16 +98,8 @@ class _AvesVideoState extends State<AvesVideo> {
|
|||
backgroundColor: Colors.transparent,
|
||||
)
|
||||
: Image(
|
||||
image: UriImage(
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
page: entry.page,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
isFlipped: entry.isFlipped,
|
||||
expectedContentLength: entry.sizeBytes,
|
||||
),
|
||||
width: entry.width.toDouble(),
|
||||
height: entry.height.toDouble(),
|
||||
image: entry.getBestThumbnail(entry.displaySize.longestSide),
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue