nc-photos/app/lib/widget/image_viewer.dart

345 lines
9.5 KiB
Dart
Raw Normal View History

2021-12-10 05:53:22 +01:00
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:flutter/gestures.dart';
2021-05-02 16:18:26 +02:00
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
2021-09-16 12:10:50 +02:00
import 'package:nc_photos/cache_manager_util.dart';
2022-05-06 07:23:04 +02:00
import 'package:nc_photos/entity/file.dart' as app;
import 'package:nc_photos/entity/local_file.dart';
2021-05-02 16:18:26 +02:00
import 'package:nc_photos/k.dart' as k;
2022-05-06 07:23:04 +02:00
import 'package:nc_photos/mobile/android/content_uri_image_provider.dart';
2021-05-02 16:18:26 +02:00
import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
2022-05-06 07:23:04 +02:00
class LocalImageViewer extends StatefulWidget {
const LocalImageViewer({
Key? key,
required this.file,
required this.canZoom,
this.onLoaded,
this.onHeightChanged,
this.onZoomStarted,
this.onZoomEnded,
}) : super(key: key);
@override
createState() => _LocalImageViewerState();
final LocalFile file;
final bool canZoom;
final VoidCallback? onLoaded;
final ValueChanged<double>? onHeightChanged;
final VoidCallback? onZoomStarted;
final VoidCallback? onZoomEnded;
}
class _LocalImageViewerState extends State<LocalImageViewer> {
@override
build(BuildContext context) {
final ImageProvider provider;
if (widget.file is LocalUriFile) {
provider = ContentUriImage((widget.file as LocalUriFile).uri);
} else {
throw ArgumentError("Invalid file");
}
return _ImageViewer(
canZoom: widget.canZoom,
onHeightChanged: widget.onHeightChanged,
onZoomStarted: widget.onZoomStarted,
onZoomEnded: widget.onZoomEnded,
child: Image(
image: provider,
fit: BoxFit.contain,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2022-05-06 07:23:04 +02:00
_onItemLoaded();
});
return child;
},
),
);
}
void _onItemLoaded() {
if (!_isLoaded) {
_log.info("[_onItemLoaded] ${widget.file.logTag}");
_isLoaded = true;
widget.onLoaded?.call();
}
}
var _isLoaded = false;
static final _log = Logger("widget.image_viewer._LocalImageViewerState");
}
class RemoteImageViewer extends StatefulWidget {
const RemoteImageViewer({
2021-09-15 08:58:06 +02:00
Key? key,
2021-07-23 22:05:57 +02:00
required this.account,
required this.file,
required this.canZoom,
2021-05-02 16:18:26 +02:00
this.onLoaded,
this.onHeightChanged,
this.onZoomStarted,
this.onZoomEnded,
2021-09-15 08:58:06 +02:00
}) : super(key: key);
2021-05-02 16:18:26 +02:00
@override
createState() => _RemoteImageViewerState();
2021-05-02 16:18:26 +02:00
2022-05-06 07:23:04 +02:00
static void preloadImage(Account account, app.File file) {
2021-09-16 12:10:50 +02:00
LargeImageCacheManager.inst.getFileStream(
2021-05-02 16:18:26 +02:00
_getImageUrl(account, file),
headers: {
"Authorization": Api.getAuthorizationHeaderValue(account),
},
);
}
final Account account;
2022-05-06 07:23:04 +02:00
final app.File file;
2021-05-02 16:18:26 +02:00
final bool canZoom;
2021-07-23 22:05:57 +02:00
final VoidCallback? onLoaded;
final ValueChanged<double>? onHeightChanged;
final VoidCallback? onZoomStarted;
final VoidCallback? onZoomEnded;
2021-05-02 16:18:26 +02:00
}
class _RemoteImageViewerState extends State<RemoteImageViewer> {
@override
build(BuildContext context) => _ImageViewer(
canZoom: widget.canZoom,
onHeightChanged: widget.onHeightChanged,
onZoomStarted: widget.onZoomStarted,
onZoomEnded: widget.onZoomEnded,
child: mod.CachedNetworkImage(
cacheManager: LargeImageCacheManager.inst,
imageUrl: _getImageUrl(widget.account, widget.file),
httpHeaders: {
"Authorization": Api.getAuthorizationHeaderValue(widget.account),
},
fit: BoxFit.contain,
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
imageBuilder: (context, child, imageProvider) {
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
_onItemLoaded();
});
SizeChangedLayoutNotification().dispatch(context);
return child;
},
),
);
void _onItemLoaded() {
if (!_isLoaded) {
_log.info("[_onItemLoaded]");
_isLoaded = true;
widget.onLoaded?.call();
}
}
var _isLoaded = false;
static final _log = Logger("widget.image_viewer._RemoteImageViewerState");
}
class _ImageViewer extends StatefulWidget {
const _ImageViewer({
Key? key,
required this.child,
required this.canZoom,
this.onHeightChanged,
this.onZoomStarted,
this.onZoomEnded,
}) : super(key: key);
@override
createState() => _ImageViewerState();
final Widget child;
final bool canZoom;
final ValueChanged<double>? onHeightChanged;
final VoidCallback? onZoomStarted;
final VoidCallback? onZoomEnded;
}
class _ImageViewerState extends State<_ImageViewer>
2021-05-02 16:18:26 +02:00
with TickerProviderStateMixin {
@override
build(BuildContext context) {
final content = InteractiveViewer(
minScale: 1.0,
2021-11-26 11:10:32 +01:00
maxScale: 3.5,
2021-05-02 16:18:26 +02:00
transformationController: _transformationController,
2021-11-26 11:10:18 +01:00
panEnabled: widget.canZoom && _isZoomed,
2021-05-02 16:18:26 +02:00
scaleEnabled: widget.canZoom,
// allow the image to be zoomed to fill the whole screen
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
alignment: Alignment.center,
child: NotificationListener<SizeChangedLayoutNotification>(
onNotification: (_) {
2022-06-20 13:49:58 +02:00
WidgetsBinding.instance.addPostFrameCallback((_) {
2021-05-02 16:18:26 +02:00
if (_key.currentContext != null) {
2021-07-23 22:05:57 +02:00
widget.onHeightChanged?.call(_key.currentContext!.size!.height);
2021-05-02 16:18:26 +02:00
}
});
return false;
},
child: SizeChangedLayoutNotifier(
key: _key,
child: widget.child,
2021-05-02 16:18:26 +02:00
),
),
),
);
if (widget.canZoom) {
return Listener(
onPointerDown: (_) {
++_finger;
if (_finger >= 2) {
_setIsZooming(true);
}
},
onPointerUp: (event) {
--_finger;
if (_finger < 2) {
_setIsZooming(false);
}
_prevFingerPosition = event.position;
},
onPointerCancel: (event) {
--_finger;
if (_finger < 2) {
_setIsZooming(false);
}
},
onPointerSignal: (event) {
if (event is PointerScrollEvent &&
event.kind == PointerDeviceKind.mouse) {
if (event.scrollDelta.dy < 0) {
// scroll up
_setIsZooming(true);
} else if (event.scrollDelta.dy > 0) {
// scroll down
_setIsZooming(false);
}
}
},
2021-05-02 16:18:26 +02:00
child: GestureDetector(
onDoubleTap: () {
if (_isZoomed) {
// restore transformation
_autoZoomOut();
} else {
_autoZoomIn();
}
},
child: content,
),
);
} else {
return content;
}
}
@override
dispose() {
super.dispose();
_transformationController.dispose();
}
void _setIsZooming(bool flag) {
_isZooming = flag;
final next = _isZoomed;
if (next != _wasZoomed) {
_wasZoomed = next;
_log.info("[_setIsZooming] Is zoomed: $next");
if (next) {
widget.onZoomStarted?.call();
} else {
widget.onZoomEnded?.call();
}
}
}
bool get _isZoomed =>
_isZooming || _transformationController.value.getMaxScaleOnAxis() != 1.0;
/// Called when double tapping the image to zoom in to the default level
void _autoZoomIn() {
final animController =
AnimationController(duration: k.animationDurationShort, vsync: this);
final originX = -_prevFingerPosition.dx / 2;
final originY = -_prevFingerPosition.dy / 2;
final anim = Matrix4Tween(
begin: Matrix4.identity(),
end: Matrix4.identity()
..scale(2.0)
..translate(originX, originY))
.animate(animController);
animController
..addListener(() {
_transformationController.value = anim.value;
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_setIsZooming(false);
}
})
..forward();
_setIsZooming(true);
}
/// Called when double tapping the zoomed image to zoom out
void _autoZoomOut() {
final animController =
AnimationController(duration: k.animationDurationShort, vsync: this);
final anim = Matrix4Tween(
begin: _transformationController.value, end: Matrix4.identity())
.animate(animController);
animController
..addListener(() {
_transformationController.value = anim.value;
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_setIsZooming(false);
}
})
..forward();
_setIsZooming(true);
}
final _key = GlobalKey();
final _transformationController = TransformationController();
var _isZooming = false;
var _wasZoomed = false;
int _finger = 0;
2021-09-15 08:58:06 +02:00
var _prevFingerPosition = const Offset(0, 0);
2021-05-02 16:18:26 +02:00
static final _log = Logger("widget.image_viewer._ImageViewerState");
}
2022-05-06 07:23:04 +02:00
String _getImageUrl(Account account, app.File file) {
2021-06-22 07:24:37 +02:00
if (file.contentType == "image/gif") {
return api_util.getFileUrl(account, file);
} else {
return api_util.getFilePreviewUrl(
2021-05-02 16:18:26 +02:00
account,
file,
2021-09-16 12:25:08 +02:00
width: k.photoLargeSize,
height: k.photoLargeSize,
2021-05-02 16:18:26 +02:00
a: true,
);
2021-06-22 07:24:37 +02:00
}
}