156 lines
5.2 KiB
Dart
156 lines
5.2 KiB
Dart
import 'dart:math' show max, min;
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:matrix4_transform/matrix4_transform.dart';
|
|
import 'package:photo_view/photo_view.dart';
|
|
|
|
import '../util/share.dart';
|
|
import '../widgets/bottom_modal.dart';
|
|
|
|
/// View to interact with a media object. Zoom in/out, download, share, etc.
|
|
class MediaViewPage extends HookWidget {
|
|
final String url;
|
|
static const yThreshold = 150;
|
|
static const speedThreshold = 45;
|
|
|
|
const MediaViewPage(this.url);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final showButtons = useState(true);
|
|
final isZoomedOut = useState(true);
|
|
final scaleIsInitial = useState(true);
|
|
|
|
final isDragging = useState(false);
|
|
|
|
final offset = useState(Offset.zero);
|
|
final prevOffset = usePrevious(offset.value) ?? Offset.zero;
|
|
|
|
notImplemented() {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
|
content: Text("this feature hasn't been implemented yet 😰")));
|
|
}
|
|
|
|
// TODO: hide navbar and topbar on android without a content jump
|
|
|
|
sharePhoto() {
|
|
showBottomModal(
|
|
context: context,
|
|
builder: (context) => Column(
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.link),
|
|
title: const Text('Share link'),
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
share(url, context: context);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.image),
|
|
title: const Text('Share file'),
|
|
onTap: () {
|
|
Navigator.of(context).pop();
|
|
notImplemented();
|
|
// TODO: share file
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
extendBodyBehindAppBar: true,
|
|
extendBody: true,
|
|
backgroundColor:
|
|
Colors.black.withOpacity(max(0, 1.0 - (offset.value.dy.abs() / 200))),
|
|
appBar: showButtons.value
|
|
? AppBar(
|
|
brightness: Brightness.dark,
|
|
iconTheme: const IconThemeData(color: Colors.white),
|
|
backgroundColor: Colors.black38,
|
|
leading: const CloseButton(),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.share),
|
|
tooltip: 'share',
|
|
onPressed: sharePhoto,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.file_download),
|
|
tooltip: 'download',
|
|
onPressed: notImplemented,
|
|
),
|
|
],
|
|
)
|
|
: null,
|
|
body: Listener(
|
|
onPointerMove: scaleIsInitial.value
|
|
? (event) {
|
|
if (!isDragging.value &&
|
|
event.delta.dx.abs() > event.delta.dy.abs()) return;
|
|
isDragging.value = true;
|
|
offset.value += event.delta;
|
|
}
|
|
: (_) => isDragging.value = false,
|
|
onPointerCancel: (_) => offset.value = Offset.zero,
|
|
onPointerUp: isZoomedOut.value
|
|
? (_) {
|
|
if (!isDragging.value) {
|
|
showButtons.value = !showButtons.value;
|
|
return;
|
|
}
|
|
|
|
isDragging.value = false;
|
|
final speed = (offset.value - prevOffset).distance;
|
|
if (speed > speedThreshold ||
|
|
offset.value.dy.abs() > yThreshold) {
|
|
Navigator.of(context).pop();
|
|
} else {
|
|
offset.value = Offset.zero;
|
|
}
|
|
}
|
|
: (_) {
|
|
offset.value = Offset.zero;
|
|
isDragging.value = false;
|
|
},
|
|
child: AnimatedContainer(
|
|
transform: Matrix4Transform()
|
|
.scale(max(0.9, 1 - offset.value.dy.abs() / 1000))
|
|
.translateOffset(offset.value)
|
|
.rotate(min(-offset.value.dx / 2000, 0.1))
|
|
.matrix4,
|
|
duration: isDragging.value
|
|
? Duration.zero
|
|
: const Duration(milliseconds: 200),
|
|
child: PhotoView(
|
|
backgroundDecoration:
|
|
const BoxDecoration(color: Colors.transparent),
|
|
scaleStateChangedCallback: (value) {
|
|
isZoomedOut.value = value == PhotoViewScaleState.zoomedOut ||
|
|
value == PhotoViewScaleState.initial;
|
|
showButtons.value = isZoomedOut.value;
|
|
|
|
scaleIsInitial.value = value == PhotoViewScaleState.initial;
|
|
isDragging.value = false;
|
|
offset.value = Offset.zero;
|
|
},
|
|
onTapUp: isZoomedOut.value
|
|
? null
|
|
: (_, __, ___) => showButtons.value = !showButtons.value,
|
|
minScale: PhotoViewComputedScale.contained,
|
|
initialScale: PhotoViewComputedScale.contained,
|
|
imageProvider: CachedNetworkImageProvider(url),
|
|
heroAttributes: PhotoViewHeroAttributes(tag: url),
|
|
loadingBuilder: (context, event) =>
|
|
const Center(child: CircularProgressIndicator()),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|