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