opt: 图片预览
This commit is contained in:
@ -1,130 +0,0 @@
|
|||||||
// ignore_for_file: depend_on_referenced_packages
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:ui' as ui show Image;
|
|
||||||
|
|
||||||
import 'package:extended_image/extended_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
|
|
||||||
|
|
||||||
double get maxDragOffset => 100;
|
|
||||||
double hideHeight = maxDragOffset / 2.3;
|
|
||||||
double refreshHeight = maxDragOffset / 1.5;
|
|
||||||
|
|
||||||
class PullToRefreshHeader extends StatelessWidget {
|
|
||||||
const PullToRefreshHeader(
|
|
||||||
this.info,
|
|
||||||
this.lastRefreshTime, {
|
|
||||||
this.color,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PullToRefreshScrollNotificationInfo? info;
|
|
||||||
final DateTime? lastRefreshTime;
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final PullToRefreshScrollNotificationInfo? infos = info;
|
|
||||||
if (infos == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
String text = '';
|
|
||||||
if (infos.mode == PullToRefreshIndicatorMode.armed) {
|
|
||||||
text = 'Release to refresh';
|
|
||||||
} else if (infos.mode == PullToRefreshIndicatorMode.refresh ||
|
|
||||||
infos.mode == PullToRefreshIndicatorMode.snap) {
|
|
||||||
text = 'Loading...';
|
|
||||||
} else if (infos.mode == PullToRefreshIndicatorMode.done) {
|
|
||||||
text = 'Refresh completed.';
|
|
||||||
} else if (infos.mode == PullToRefreshIndicatorMode.drag) {
|
|
||||||
text = 'Pull to refresh';
|
|
||||||
} else if (infos.mode == PullToRefreshIndicatorMode.canceled) {
|
|
||||||
text = 'Cancel refresh';
|
|
||||||
}
|
|
||||||
|
|
||||||
final TextStyle ts = const TextStyle(
|
|
||||||
color: Colors.grey,
|
|
||||||
).copyWith(fontSize: 14);
|
|
||||||
|
|
||||||
final double dragOffset = info?.dragOffset ?? 0.0;
|
|
||||||
|
|
||||||
final DateTime time = lastRefreshTime ?? DateTime.now();
|
|
||||||
final double top = -hideHeight + dragOffset;
|
|
||||||
return Container(
|
|
||||||
height: dragOffset,
|
|
||||||
color: color ?? Colors.transparent,
|
|
||||||
// padding: EdgeInsets.only(top: dragOffset / 3),
|
|
||||||
// padding: EdgeInsets.only(bottom: 5.0),
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
Positioned(
|
|
||||||
left: 0.0,
|
|
||||||
right: 0.0,
|
|
||||||
top: top,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
margin: const EdgeInsets.only(right: 12.0),
|
|
||||||
child: RefreshImage(top, null),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Text(text, style: ts),
|
|
||||||
Text(
|
|
||||||
'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}',
|
|
||||||
style: ts.copyWith(fontSize: 14),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RefreshImage extends StatelessWidget {
|
|
||||||
const RefreshImage(this.top, Key? key) : super(key: key);
|
|
||||||
|
|
||||||
final double top;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const double imageSize = 30;
|
|
||||||
return ExtendedImage.asset(
|
|
||||||
'assets/flutterCandies_grey.png',
|
|
||||||
width: imageSize,
|
|
||||||
height: imageSize,
|
|
||||||
afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) {
|
|
||||||
final double imageHeight = image.height.toDouble();
|
|
||||||
final double imageWidth = image.width.toDouble();
|
|
||||||
final Size size = rect.size;
|
|
||||||
final double y =
|
|
||||||
(1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight;
|
|
||||||
|
|
||||||
canvas.drawImageRect(
|
|
||||||
image,
|
|
||||||
Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y),
|
|
||||||
Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height,
|
|
||||||
size.width, (imageHeight - y) / imageHeight * size.height),
|
|
||||||
Paint()
|
|
||||||
..colorFilter =
|
|
||||||
const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn)
|
|
||||||
..isAntiAlias = false
|
|
||||||
..filterQuality = FilterQuality.low,
|
|
||||||
);
|
|
||||||
|
|
||||||
//canvas.restore();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,11 @@
|
|||||||
// 内容
|
// 内容
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/widgets/badge.dart';
|
import 'package:pilipala/common/widgets/badge.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
import 'package:pilipala/models/dynamics/result.dart';
|
import 'package:pilipala/models/dynamics/result.dart';
|
||||||
import 'package:pilipala/pages/preview/index.dart';
|
import 'package:pilipala/plugin/pl_gallery/index.dart';
|
||||||
|
|
||||||
import 'rich_node_panel.dart';
|
import 'rich_node_panel.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
@ -59,16 +59,14 @@ class _ContentState extends State<Content> {
|
|||||||
(pictureItem.height != null && pictureItem.width != null
|
(pictureItem.height != null && pictureItem.width != null
|
||||||
? pictureItem.height! / pictureItem.width!
|
? pictureItem.height! / pictureItem.width!
|
||||||
: 1);
|
: 1);
|
||||||
return GestureDetector(
|
return Hero(
|
||||||
onTap: () {
|
tag: pictureItem.url!,
|
||||||
showDialog(
|
placeholderBuilder:
|
||||||
useSafeArea: false,
|
(BuildContext context, Size heroSize, Widget child) {
|
||||||
context: context,
|
return child;
|
||||||
builder: (context) {
|
|
||||||
return ImagePreview(initialPage: 0, imgList: picList);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onPreviewImg(picList, 1, context),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||||
@ -91,7 +89,9 @@ class _ContentState extends State<Content> {
|
|||||||
)
|
)
|
||||||
: const SizedBox(),
|
: const SizedBox(),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -102,20 +102,16 @@ class _ContentState extends State<Content> {
|
|||||||
List<Widget> list = [];
|
List<Widget> list = [];
|
||||||
for (var i = 0; i < len; i++) {
|
for (var i = 0; i < len; i++) {
|
||||||
picList.add(pics[i].url!);
|
picList.add(pics[i].url!);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
list.add(
|
list.add(
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, BoxConstraints box) {
|
builder: (context, BoxConstraints box) {
|
||||||
double maxWidth = box.maxWidth.truncateToDouble();
|
double maxWidth = box.maxWidth.truncateToDouble();
|
||||||
return GestureDetector(
|
return Hero(
|
||||||
onTap: () {
|
tag: picList[i],
|
||||||
showDialog(
|
child: GestureDetector(
|
||||||
useSafeArea: false,
|
onTap: () => onPreviewImg(picList, i, context),
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return ImagePreview(initialPage: i, imgList: picList);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
src: pics[i].url,
|
src: pics[i].url,
|
||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
@ -123,6 +119,7 @@ class _ContentState extends State<Content> {
|
|||||||
origAspectRatio:
|
origAspectRatio:
|
||||||
pics[i].width!.toInt() / pics[i].height!.toInt(),
|
pics[i].width!.toInt() / pics[i].height!.toInt(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -163,6 +160,43 @@ class _ContentState extends State<Content> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onPreviewImg(picList, initIndex, context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
HeroDialogRoute<void>(
|
||||||
|
builder: (BuildContext context) => InteractiveviewerGallery(
|
||||||
|
sources: picList,
|
||||||
|
initIndex: initIndex,
|
||||||
|
itemBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
bool isFocus,
|
||||||
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: picList[index],
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
imageUrl: picList[index],
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
TextStyle authorStyle =
|
TextStyle authorStyle =
|
||||||
|
@ -1,9 +1,47 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/common/constants.dart';
|
import 'package:pilipala/common/constants.dart';
|
||||||
import 'package:pilipala/common/widgets/badge.dart';
|
import 'package:pilipala/common/widgets/badge.dart';
|
||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
import 'package:pilipala/pages/preview/index.dart';
|
import 'package:pilipala/plugin/pl_gallery/index.dart';
|
||||||
|
|
||||||
|
void onPreviewImg(currentUrl, picList, initIndex, context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
HeroDialogRoute<void>(
|
||||||
|
builder: (BuildContext context) => InteractiveviewerGallery(
|
||||||
|
sources: picList,
|
||||||
|
initIndex: initIndex,
|
||||||
|
itemBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
bool isFocus,
|
||||||
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: picList[index],
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
imageUrl: picList[index],
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget picWidget(item, context) {
|
Widget picWidget(item, context) {
|
||||||
String type = item.modules.moduleDynamic.major.type;
|
String type = item.modules.moduleDynamic.major.type;
|
||||||
@ -21,25 +59,25 @@ Widget picWidget(item, context) {
|
|||||||
List<Widget> list = [];
|
List<Widget> list = [];
|
||||||
for (var i = 0; i < len; i++) {
|
for (var i = 0; i < len; i++) {
|
||||||
picList.add(pictures[i].src ?? pictures[i].url);
|
picList.add(pictures[i].src ?? pictures[i].url);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
list.add(
|
list.add(
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, BoxConstraints box) {
|
builder: (context, BoxConstraints box) {
|
||||||
return GestureDetector(
|
return Hero(
|
||||||
onTap: () {
|
tag: picList[i],
|
||||||
showDialog(
|
placeholderBuilder:
|
||||||
useSafeArea: false,
|
(BuildContext context, Size heroSize, Widget child) {
|
||||||
context: context,
|
return child;
|
||||||
builder: (context) {
|
|
||||||
return ImagePreview(initialPage: i, imgList: picList);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onPreviewImg(picList[i], picList, i, context),
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
src: pictures[i].src ?? pictures[i].url,
|
src: pictures[i].src ?? pictures[i].url,
|
||||||
width: box.maxWidth,
|
width: box.maxWidth,
|
||||||
height: box.maxWidth,
|
height: box.maxWidth,
|
||||||
),
|
),
|
||||||
// ),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -27,6 +27,7 @@ class MainController extends GetxController {
|
|||||||
RxBool userLogin = false.obs;
|
RxBool userLogin = false.obs;
|
||||||
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
|
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
|
||||||
late bool enableGradientBg;
|
late bool enableGradientBg;
|
||||||
|
bool imgPreviewStatus = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
|
||||||
|
|
||||||
class PreviewController extends GetxController {
|
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
|
||||||
RxInt initialPage = 0.obs;
|
|
||||||
RxInt currentPage = 1.obs;
|
|
||||||
RxList imgList = [].obs;
|
|
||||||
bool storage = true;
|
|
||||||
bool videos = true;
|
|
||||||
bool photos = true;
|
|
||||||
String currentImgUrl = '';
|
|
||||||
|
|
||||||
requestPermission() async {
|
|
||||||
Map<Permission, PermissionStatus> statuses = await [
|
|
||||||
Permission.storage,
|
|
||||||
// Permission.photos
|
|
||||||
].request();
|
|
||||||
|
|
||||||
statuses[Permission.storage].toString();
|
|
||||||
// final photosInfo = statuses[Permission.photos].toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图片分享
|
|
||||||
void onShareImg() async {
|
|
||||||
SmartDialog.showLoading();
|
|
||||||
var response = await Dio().get(imgList[initialPage.value],
|
|
||||||
options: Options(responseType: ResponseType.bytes));
|
|
||||||
final temp = await getTemporaryDirectory();
|
|
||||||
SmartDialog.dismiss();
|
|
||||||
String imgName =
|
|
||||||
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
|
|
||||||
var path = '${temp.path}/$imgName';
|
|
||||||
File(path).writeAsBytesSync(response.data);
|
|
||||||
Share.shareXFiles([XFile(path)], subject: imgList[initialPage.value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onChange(int index) {
|
|
||||||
initialPage.value = index;
|
|
||||||
currentPage.value = index + 1;
|
|
||||||
currentImgUrl = imgList[index];
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
library preview;
|
|
||||||
|
|
||||||
export './controller.dart';
|
|
||||||
export './view.dart';
|
|
@ -1,290 +0,0 @@
|
|||||||
// ignore_for_file: library_private_types_in_public_api
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:extended_image/extended_image.dart';
|
|
||||||
import 'package:pilipala/utils/download.dart';
|
|
||||||
import 'controller.dart';
|
|
||||||
import 'package:status_bar_control/status_bar_control.dart';
|
|
||||||
|
|
||||||
typedef DoubleClickAnimationListener = void Function();
|
|
||||||
|
|
||||||
class ImagePreview extends StatefulWidget {
|
|
||||||
final int? initialPage;
|
|
||||||
final List<String>? imgList;
|
|
||||||
const ImagePreview({
|
|
||||||
Key? key,
|
|
||||||
this.initialPage,
|
|
||||||
this.imgList,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_ImagePreviewState createState() => _ImagePreviewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ImagePreviewState extends State<ImagePreview>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
final PreviewController _previewController = Get.put(PreviewController());
|
|
||||||
// late AnimationController animationController;
|
|
||||||
late AnimationController _doubleClickAnimationController;
|
|
||||||
Animation<double>? _doubleClickAnimation;
|
|
||||||
late DoubleClickAnimationListener _doubleClickAnimationListener;
|
|
||||||
List<double> doubleTapScales = <double>[1.0, 2.0];
|
|
||||||
bool _dismissDisabled = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_previewController.initialPage.value = widget.initialPage!;
|
|
||||||
_previewController.currentPage.value = widget.initialPage! + 1;
|
|
||||||
_previewController.imgList.value = widget.imgList!;
|
|
||||||
_previewController.currentImgUrl = widget.imgList![widget.initialPage!];
|
|
||||||
// animationController = AnimationController(
|
|
||||||
// vsync: this, duration: const Duration(milliseconds: 400));
|
|
||||||
setStatusBar();
|
|
||||||
_doubleClickAnimationController = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 250), vsync: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpenMenu() {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
onTap: () {
|
|
||||||
_previewController.onShareImg();
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
dense: true,
|
|
||||||
title: const Text('分享', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
onTap: () {
|
|
||||||
Clipboard.setData(
|
|
||||||
ClipboardData(text: _previewController.currentImgUrl))
|
|
||||||
.then((value) {
|
|
||||||
Get.back();
|
|
||||||
SmartDialog.showToast('已复制到粘贴板');
|
|
||||||
}).catchError((err) {
|
|
||||||
SmartDialog.showNotify(
|
|
||||||
msg: err.toString(),
|
|
||||||
notifyType: NotifyType.error,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dense: true,
|
|
||||||
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
onTap: () {
|
|
||||||
Get.back();
|
|
||||||
DownloadUtils.downloadImg(_previewController.currentImgUrl);
|
|
||||||
},
|
|
||||||
dense: true,
|
|
||||||
title: const Text('保存到手机', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏状态栏,避免遮挡图片内容
|
|
||||||
setStatusBar() async {
|
|
||||||
if (Platform.isIOS || Platform.isAndroid) {
|
|
||||||
await StatusBarControl.setHidden(true,
|
|
||||||
animation: StatusBarAnimation.SLIDE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
// animationController.dispose();
|
|
||||||
try {
|
|
||||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.SLIDE);
|
|
||||||
} catch (_) {}
|
|
||||||
_doubleClickAnimationController.dispose();
|
|
||||||
clearGestureDetailsCache();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
primary: false,
|
|
||||||
extendBody: true,
|
|
||||||
appBar: AppBar(
|
|
||||||
primary: false,
|
|
||||||
toolbarHeight: 0,
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
|
||||||
),
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onLongPress: () => onOpenMenu(),
|
|
||||||
child: ExtendedImageGesturePageView.builder(
|
|
||||||
controller: ExtendedPageController(
|
|
||||||
initialPage: _previewController.initialPage.value,
|
|
||||||
pageSpacing: 0,
|
|
||||||
),
|
|
||||||
onPageChanged: (int index) => _previewController.onChange(index),
|
|
||||||
canScrollPage: (GestureDetails? gestureDetails) =>
|
|
||||||
gestureDetails!.totalScale! <= 1.0,
|
|
||||||
itemCount: widget.imgList!.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return ExtendedImage.network(
|
|
||||||
widget.imgList![index],
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
mode: ExtendedImageMode.gesture,
|
|
||||||
onDoubleTap: (ExtendedImageGestureState state) {
|
|
||||||
final Offset? pointerDownPosition =
|
|
||||||
state.pointerDownPosition;
|
|
||||||
final double? begin = state.gestureDetails!.totalScale;
|
|
||||||
double end;
|
|
||||||
|
|
||||||
//remove old
|
|
||||||
_doubleClickAnimation
|
|
||||||
?.removeListener(_doubleClickAnimationListener);
|
|
||||||
|
|
||||||
//stop pre
|
|
||||||
_doubleClickAnimationController.stop();
|
|
||||||
|
|
||||||
//reset to use
|
|
||||||
_doubleClickAnimationController.reset();
|
|
||||||
|
|
||||||
if (begin == doubleTapScales[0]) {
|
|
||||||
setState(() {
|
|
||||||
_dismissDisabled = true;
|
|
||||||
});
|
|
||||||
end = doubleTapScales[1];
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_dismissDisabled = false;
|
|
||||||
});
|
|
||||||
end = doubleTapScales[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
_doubleClickAnimationListener = () {
|
|
||||||
state.handleDoubleTap(
|
|
||||||
scale: _doubleClickAnimation!.value,
|
|
||||||
doubleTapPosition: pointerDownPosition);
|
|
||||||
};
|
|
||||||
_doubleClickAnimation = _doubleClickAnimationController
|
|
||||||
.drive(Tween<double>(begin: begin, end: end));
|
|
||||||
|
|
||||||
_doubleClickAnimation!
|
|
||||||
.addListener(_doubleClickAnimationListener);
|
|
||||||
|
|
||||||
_doubleClickAnimationController.forward();
|
|
||||||
},
|
|
||||||
// ignore: body_might_complete_normally_nullable
|
|
||||||
loadStateChanged: (ExtendedImageState state) {
|
|
||||||
if (state.extendedImageLoadState == LoadState.loading) {
|
|
||||||
final ImageChunkEvent? loadingProgress =
|
|
||||||
state.loadingProgress;
|
|
||||||
final double? progress =
|
|
||||||
loadingProgress?.expectedTotalBytes != null
|
|
||||||
? loadingProgress!.cumulativeBytesLoaded /
|
|
||||||
loadingProgress.expectedTotalBytes!
|
|
||||||
: null;
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
SizedBox(
|
|
||||||
width: 150.0,
|
|
||||||
child: LinearProgressIndicator(
|
|
||||||
value: progress,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// const SizedBox(height: 10.0),
|
|
||||||
// Text('${((progress ?? 0.0) * 100).toInt()}%',),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initGestureConfigHandler: (ExtendedImageState state) {
|
|
||||||
return GestureConfig(
|
|
||||||
inPageView: true,
|
|
||||||
initialScale: 1.0,
|
|
||||||
maxScale: 5.0,
|
|
||||||
animationMaxScale: 6.0,
|
|
||||||
initialAlignment: InitialAlignment.center,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 30),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: <Color>[
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.black87,
|
|
||||||
],
|
|
||||||
tileMode: TileMode.mirror,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
widget.imgList!.length > 1
|
|
||||||
? Obx(
|
|
||||||
() => Text.rich(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
TextSpan(
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white, fontSize: 16),
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: _previewController.currentPage
|
|
||||||
.toString()),
|
|
||||||
const TextSpan(text: ' / '),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
widget.imgList!.length.toString()),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox(),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appscheme/appscheme.dart';
|
import 'package:appscheme/appscheme.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -9,9 +10,10 @@ import 'package:pilipala/common/widgets/badge.dart';
|
|||||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
import 'package:pilipala/models/common/reply_type.dart';
|
import 'package:pilipala/models/common/reply_type.dart';
|
||||||
import 'package:pilipala/models/video/reply/item.dart';
|
import 'package:pilipala/models/video/reply/item.dart';
|
||||||
import 'package:pilipala/pages/preview/index.dart';
|
import 'package:pilipala/pages/main/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/index.dart';
|
import 'package:pilipala/pages/video/detail/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
|
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_gallery/index.dart';
|
||||||
import 'package:pilipala/utils/app_scheme.dart';
|
import 'package:pilipala/utils/app_scheme.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
import 'package:pilipala/utils/id_utils.dart';
|
import 'package:pilipala/utils/id_utils.dart';
|
||||||
@ -540,6 +542,53 @@ InlineSpan buildContent(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onPreviewImg(picList, initIndex) {
|
||||||
|
final MainController mainController = Get.find<MainController>();
|
||||||
|
mainController.imgPreviewStatus = true;
|
||||||
|
Navigator.of(context).push(
|
||||||
|
HeroDialogRoute<void>(
|
||||||
|
builder: (BuildContext context) => InteractiveviewerGallery(
|
||||||
|
sources: picList,
|
||||||
|
initIndex: initIndex,
|
||||||
|
itemBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
int index,
|
||||||
|
bool isFocus,
|
||||||
|
bool enablePageView,
|
||||||
|
) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
if (enablePageView) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
final MainController mainController =
|
||||||
|
Get.find<MainController>();
|
||||||
|
mainController.imgPreviewStatus = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: Hero(
|
||||||
|
tag: picList[index],
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
imageUrl: picList[index],
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPageChanged: (int pageIndex) {},
|
||||||
|
onDismissed: (int value) {
|
||||||
|
print('onDismissed');
|
||||||
|
final MainController mainController = Get.find<MainController>();
|
||||||
|
mainController.imgPreviewStatus = false;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 分割文本并处理每个部分
|
// 分割文本并处理每个部分
|
||||||
content.message.splitMapJoin(
|
content.message.splitMapJoin(
|
||||||
pattern,
|
pattern,
|
||||||
@ -831,16 +880,10 @@ InlineSpan buildContent(
|
|||||||
.truncateToDouble();
|
.truncateToDouble();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
return GestureDetector(
|
return Hero(
|
||||||
onTap: () {
|
tag: picList[0],
|
||||||
showDialog(
|
child: GestureDetector(
|
||||||
useSafeArea: false,
|
onTap: () => onPreviewImg(picList, 0),
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return ImagePreview(initialPage: 0, imgList: picList);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||||
@ -850,7 +893,7 @@ InlineSpan buildContent(
|
|||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
src: pictureItem['img_src'],
|
src: picList[0],
|
||||||
width: box.maxWidth / 2,
|
width: box.maxWidth / 2,
|
||||||
height: height,
|
height: height,
|
||||||
),
|
),
|
||||||
@ -865,6 +908,7 @@ InlineSpan buildContent(
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -874,25 +918,22 @@ InlineSpan buildContent(
|
|||||||
List<Widget> list = [];
|
List<Widget> list = [];
|
||||||
for (var i = 0; i < len; i++) {
|
for (var i = 0; i < len; i++) {
|
||||||
picList.add(content.pictures[i]['img_src']);
|
picList.add(content.pictures[i]['img_src']);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
list.add(
|
list.add(
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, BoxConstraints box) {
|
builder: (context, BoxConstraints box) {
|
||||||
return GestureDetector(
|
return Hero(
|
||||||
onTap: () {
|
tag: picList[i],
|
||||||
showDialog(
|
child: GestureDetector(
|
||||||
useSafeArea: false,
|
onTap: () => onPreviewImg(picList, i),
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return ImagePreview(initialPage: i, imgList: picList);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
src: content.pictures[i]['img_src'],
|
src: picList[i],
|
||||||
width: box.maxWidth,
|
width: box.maxWidth,
|
||||||
height: box.maxWidth,
|
height: box.maxWidth,
|
||||||
origAspectRatio: content.pictures[i]['img_width'] /
|
origAspectRatio: content.pictures[i]['img_width'] /
|
||||||
content.pictures[i]['img_height']),
|
content.pictures[i]['img_height']),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -15,6 +15,7 @@ import 'package:pilipala/http/user.dart';
|
|||||||
import 'package:pilipala/models/common/search_type.dart';
|
import 'package:pilipala/models/common/search_type.dart';
|
||||||
import 'package:pilipala/pages/bangumi/introduction/index.dart';
|
import 'package:pilipala/pages/bangumi/introduction/index.dart';
|
||||||
import 'package:pilipala/pages/danmaku/view.dart';
|
import 'package:pilipala/pages/danmaku/view.dart';
|
||||||
|
import 'package:pilipala/pages/main/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/controller.dart';
|
import 'package:pilipala/pages/video/detail/controller.dart';
|
||||||
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
import 'package:pilipala/pages/video/detail/introduction/index.dart';
|
||||||
@ -240,6 +241,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
@override
|
@override
|
||||||
// 离开当前页面时
|
// 离开当前页面时
|
||||||
void didPushNext() async {
|
void didPushNext() async {
|
||||||
|
final MainController mainController = Get.find<MainController>();
|
||||||
|
if (mainController.imgPreviewStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/// 开启
|
/// 开启
|
||||||
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
|
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
|
||||||
as bool) {
|
as bool) {
|
||||||
@ -259,6 +265,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
@override
|
@override
|
||||||
// 返回当前页面时
|
// 返回当前页面时
|
||||||
void didPopNext() async {
|
void didPopNext() async {
|
||||||
|
final MainController mainController = Get.find<MainController>();
|
||||||
|
if (mainController.imgPreviewStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (plPlayerController != null &&
|
if (plPlayerController != null &&
|
||||||
plPlayerController!.videoPlayerController != null) {
|
plPlayerController!.videoPlayerController != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
156
lib/plugin/pl_gallery/custom_dismissible.dart
Normal file
156
lib/plugin/pl_gallery/custom_dismissible.dart
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A widget used to dismiss its [child].
|
||||||
|
///
|
||||||
|
/// Similar to [Dismissible] with some adjustments.
|
||||||
|
class CustomDismissible extends StatefulWidget {
|
||||||
|
const CustomDismissible({
|
||||||
|
required this.child,
|
||||||
|
this.onDismissed,
|
||||||
|
this.dismissThreshold = 0.2,
|
||||||
|
this.enabled = true,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final double dismissThreshold;
|
||||||
|
final VoidCallback? onDismissed;
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CustomDismissible> createState() => _CustomDismissibleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomDismissibleState extends State<CustomDismissible>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _animateController;
|
||||||
|
late Animation<Offset> _moveAnimation;
|
||||||
|
late Animation<double> _scaleAnimation;
|
||||||
|
late Animation<Decoration> _opacityAnimation;
|
||||||
|
|
||||||
|
double _dragExtent = 0;
|
||||||
|
bool _dragUnderway = false;
|
||||||
|
|
||||||
|
bool get _isActive => _dragUnderway || _animateController.isAnimating;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_animateController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_updateMoveAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_animateController.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateMoveAnimation() {
|
||||||
|
final double end = _dragExtent.sign;
|
||||||
|
|
||||||
|
_moveAnimation = _animateController.drive(
|
||||||
|
Tween<Offset>(
|
||||||
|
begin: Offset.zero,
|
||||||
|
end: Offset(0, end),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_scaleAnimation = _animateController.drive(Tween<double>(
|
||||||
|
begin: 1,
|
||||||
|
end: 0.5,
|
||||||
|
));
|
||||||
|
|
||||||
|
_opacityAnimation = DecorationTween(
|
||||||
|
begin: const BoxDecoration(color: Color(0xFF000000)),
|
||||||
|
end: const BoxDecoration(color: Color(0x00000000)),
|
||||||
|
).animate(_animateController);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragStart(DragStartDetails details) {
|
||||||
|
_dragUnderway = true;
|
||||||
|
|
||||||
|
if (_animateController.isAnimating) {
|
||||||
|
_dragExtent =
|
||||||
|
_animateController.value * context.size!.height * _dragExtent.sign;
|
||||||
|
_animateController.stop();
|
||||||
|
} else {
|
||||||
|
_dragExtent = 0.0;
|
||||||
|
_animateController.value = 0.0;
|
||||||
|
}
|
||||||
|
setState(_updateMoveAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragUpdate(DragUpdateDetails details) {
|
||||||
|
if (!_isActive || _animateController.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double delta = details.primaryDelta!;
|
||||||
|
final double oldDragExtent = _dragExtent;
|
||||||
|
|
||||||
|
if (_dragExtent + delta < 0) {
|
||||||
|
_dragExtent += delta;
|
||||||
|
} else if (_dragExtent + delta > 0) {
|
||||||
|
_dragExtent += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDragExtent.sign != _dragExtent.sign) {
|
||||||
|
setState(_updateMoveAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_animateController.isAnimating) {
|
||||||
|
_animateController.value = _dragExtent.abs() / context.size!.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(DragEndDetails details) {
|
||||||
|
if (!_isActive || _animateController.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dragUnderway = false;
|
||||||
|
|
||||||
|
if (_animateController.isCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_animateController.isDismissed) {
|
||||||
|
// if the dragged value exceeded the dismissThreshold, call onDismissed
|
||||||
|
// else animate back to initial position.
|
||||||
|
if (_animateController.value > widget.dismissThreshold) {
|
||||||
|
widget.onDismissed?.call();
|
||||||
|
} else {
|
||||||
|
_animateController.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Widget content = DecoratedBoxTransition(
|
||||||
|
decoration: _opacityAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _moveAnimation,
|
||||||
|
child: ScaleTransition(
|
||||||
|
scale: _scaleAnimation,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onVerticalDragStart: widget.enabled ? _handleDragStart : null,
|
||||||
|
onVerticalDragUpdate: widget.enabled ? _handleDragUpdate : null,
|
||||||
|
onVerticalDragEnd: widget.enabled ? _handleDragEnd : null,
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
lib/plugin/pl_gallery/hero_dialog_route.dart
Normal file
63
lib/plugin/pl_gallery/hero_dialog_route.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A [PageRoute] with a semi transparent background.
|
||||||
|
///
|
||||||
|
/// Similar to calling [showDialog] except it can be used with a [Navigator] to
|
||||||
|
/// show a [Hero] animation.
|
||||||
|
class HeroDialogRoute<T> extends PageRoute<T> {
|
||||||
|
HeroDialogRoute({
|
||||||
|
required this.builder,
|
||||||
|
this.onBackgroundTap,
|
||||||
|
}) : super();
|
||||||
|
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
/// Called when the background is tapped.
|
||||||
|
final VoidCallback? onBackgroundTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get opaque => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get barrierDismissible => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
) {
|
||||||
|
final Widget child = builder(context);
|
||||||
|
final Widget result = Semantics(
|
||||||
|
scopesRoute: true,
|
||||||
|
explicitChildNodes: true,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
6
lib/plugin/pl_gallery/index.dart
Normal file
6
lib/plugin/pl_gallery/index.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
library pl_gallery;
|
||||||
|
|
||||||
|
export './hero_dialog_route.dart';
|
||||||
|
export './custom_dismissible.dart';
|
||||||
|
export './interactiveviewer_gallery.dart';
|
||||||
|
export './interactive_viewer_boundary.dart';
|
117
lib/plugin/pl_gallery/interactive_viewer_boundary.dart
Normal file
117
lib/plugin/pl_gallery/interactive_viewer_boundary.dart
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A callback for the [InteractiveViewerBoundary] that is called when the scale
|
||||||
|
/// changed.
|
||||||
|
typedef ScaleChanged = void Function(double scale);
|
||||||
|
|
||||||
|
/// Builds an [InteractiveViewer] and provides callbacks that are called when a
|
||||||
|
/// horizontal boundary has been hit.
|
||||||
|
///
|
||||||
|
/// The callbacks are called when an interaction ends by listening to the
|
||||||
|
/// [InteractiveViewer.onInteractionEnd] callback.
|
||||||
|
class InteractiveViewerBoundary extends StatefulWidget {
|
||||||
|
const InteractiveViewerBoundary({
|
||||||
|
required this.child,
|
||||||
|
required this.boundaryWidth,
|
||||||
|
this.controller,
|
||||||
|
this.onScaleChanged,
|
||||||
|
this.onLeftBoundaryHit,
|
||||||
|
this.onRightBoundaryHit,
|
||||||
|
this.onNoBoundaryHit,
|
||||||
|
this.maxScale,
|
||||||
|
this.minScale,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// The max width this widget can have.
|
||||||
|
///
|
||||||
|
/// If the [InteractiveViewer] can take up the entire screen width, this
|
||||||
|
/// should be set to `MediaQuery.of(context).size.width`.
|
||||||
|
final double boundaryWidth;
|
||||||
|
|
||||||
|
/// The [TransformationController] for the [InteractiveViewer].
|
||||||
|
final TransformationController? controller;
|
||||||
|
|
||||||
|
/// Called when the scale changed after an interaction ended.
|
||||||
|
final ScaleChanged? onScaleChanged;
|
||||||
|
|
||||||
|
/// Called when the left boundary has been hit after an interaction ended.
|
||||||
|
final VoidCallback? onLeftBoundaryHit;
|
||||||
|
|
||||||
|
/// Called when the right boundary has been hit after an interaction ended.
|
||||||
|
final VoidCallback? onRightBoundaryHit;
|
||||||
|
|
||||||
|
/// Called when no boundary has been hit after an interaction ended.
|
||||||
|
final VoidCallback? onNoBoundaryHit;
|
||||||
|
|
||||||
|
final double? maxScale;
|
||||||
|
|
||||||
|
final double? minScale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
InteractiveViewerBoundaryState createState() =>
|
||||||
|
InteractiveViewerBoundaryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary> {
|
||||||
|
TransformationController? _controller;
|
||||||
|
|
||||||
|
double? _scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = widget.controller ?? TransformationController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller!.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateBoundaryDetection() {
|
||||||
|
final double scale = _controller!.value.row0[0];
|
||||||
|
|
||||||
|
if (_scale != scale) {
|
||||||
|
// the scale changed
|
||||||
|
_scale = scale;
|
||||||
|
widget.onScaleChanged?.call(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale <= 1.01) {
|
||||||
|
// cant hit any boundaries when the child is not scaled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final double xOffset = _controller!.value.row0[3];
|
||||||
|
final double boundaryWidth = widget.boundaryWidth;
|
||||||
|
final double boundaryEnd = boundaryWidth * scale;
|
||||||
|
final double xPos = boundaryEnd + xOffset;
|
||||||
|
|
||||||
|
if (boundaryEnd.round() == xPos.round()) {
|
||||||
|
// left boundary hit
|
||||||
|
widget.onLeftBoundaryHit?.call();
|
||||||
|
} else if (boundaryWidth.round() == xPos.round()) {
|
||||||
|
// right boundary hit
|
||||||
|
widget.onRightBoundaryHit?.call();
|
||||||
|
} else {
|
||||||
|
widget.onNoBoundaryHit?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InteractiveViewer(
|
||||||
|
maxScale: widget.maxScale!,
|
||||||
|
minScale: widget.minScale!,
|
||||||
|
transformationController: _controller,
|
||||||
|
onInteractionEnd: (_) => _updateBoundaryDetection(),
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
399
lib/plugin/pl_gallery/interactiveviewer_gallery.dart
Normal file
399
lib/plugin/pl_gallery/interactiveviewer_gallery.dart
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
library interactiveviewer_gallery;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:pilipala/utils/download.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:status_bar_control/status_bar_control.dart';
|
||||||
|
import 'custom_dismissible.dart';
|
||||||
|
import 'interactive_viewer_boundary.dart';
|
||||||
|
|
||||||
|
/// Builds a carousel controlled by a [PageView] for the tweet media sources.
|
||||||
|
///
|
||||||
|
/// Used for showing a full screen view of the [TweetMedia] sources.
|
||||||
|
///
|
||||||
|
/// The sources can be panned and zoomed interactively using an
|
||||||
|
/// [InteractiveViewer].
|
||||||
|
/// An [InteractiveViewerBoundary] is used to detect when the boundary of the
|
||||||
|
/// source is hit after zooming in to disable or enable the swiping gesture of
|
||||||
|
/// the [PageView].
|
||||||
|
///
|
||||||
|
typedef IndexedFocusedWidgetBuilder = Widget Function(
|
||||||
|
BuildContext context, int index, bool isFocus, bool enablePageView);
|
||||||
|
|
||||||
|
typedef IndexedTagStringBuilder = String Function(int index);
|
||||||
|
|
||||||
|
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||||
|
const InteractiveviewerGallery({
|
||||||
|
required this.sources,
|
||||||
|
required this.initIndex,
|
||||||
|
required this.itemBuilder,
|
||||||
|
this.maxScale = 4.5,
|
||||||
|
this.minScale = 1.0,
|
||||||
|
this.onPageChanged,
|
||||||
|
this.onDismissed,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The sources to show.
|
||||||
|
final List<T> sources;
|
||||||
|
|
||||||
|
/// The index of the first source in [sources] to show.
|
||||||
|
final int initIndex;
|
||||||
|
|
||||||
|
/// The item content
|
||||||
|
final IndexedFocusedWidgetBuilder itemBuilder;
|
||||||
|
|
||||||
|
final double maxScale;
|
||||||
|
|
||||||
|
final double minScale;
|
||||||
|
|
||||||
|
final ValueChanged<int>? onPageChanged;
|
||||||
|
|
||||||
|
final ValueChanged<int>? onDismissed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InteractiveviewerGallery> createState() =>
|
||||||
|
_InteractiveviewerGalleryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
PageController? _pageController;
|
||||||
|
TransformationController? _transformationController;
|
||||||
|
|
||||||
|
/// The controller to animate the transformation value of the
|
||||||
|
/// [InteractiveViewer] when it should reset.
|
||||||
|
late AnimationController _animationController;
|
||||||
|
Animation<Matrix4>? _animation;
|
||||||
|
|
||||||
|
/// `true` when an source is zoomed in and not at the at a horizontal boundary
|
||||||
|
/// to disable the [PageView].
|
||||||
|
bool _enablePageView = true;
|
||||||
|
|
||||||
|
/// `true` when an source is zoomed in to disable the [CustomDismissible].
|
||||||
|
bool _enableDismiss = true;
|
||||||
|
|
||||||
|
late Offset _doubleTapLocalPosition;
|
||||||
|
|
||||||
|
int? currentIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_pageController = PageController(initialPage: widget.initIndex);
|
||||||
|
|
||||||
|
_transformationController = TransformationController();
|
||||||
|
|
||||||
|
_animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
)
|
||||||
|
..addListener(() {
|
||||||
|
_transformationController!.value =
|
||||||
|
_animation?.value ?? Matrix4.identity();
|
||||||
|
})
|
||||||
|
..addStatusListener((AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.completed && !_enableDismiss) {
|
||||||
|
setState(() {
|
||||||
|
_enableDismiss = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
currentIndex = widget.initIndex;
|
||||||
|
setStatusBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatusBar() async {
|
||||||
|
if (Platform.isIOS || Platform.isAndroid) {
|
||||||
|
await StatusBarControl.setHidden(true,
|
||||||
|
animation: StatusBarAnimation.FADE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController!.dispose();
|
||||||
|
_animationController.dispose();
|
||||||
|
try {
|
||||||
|
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
||||||
|
} catch (_) {}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the source gets scaled up, the swipe up / down to dismiss gets
|
||||||
|
/// disabled.
|
||||||
|
///
|
||||||
|
/// When the scale resets, the dismiss and the page view swiping gets enabled.
|
||||||
|
void _onScaleChanged(double scale) {
|
||||||
|
final bool initialScale = scale <= widget.minScale;
|
||||||
|
|
||||||
|
if (initialScale) {
|
||||||
|
if (!_enableDismiss) {
|
||||||
|
setState(() {
|
||||||
|
_enableDismiss = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_enablePageView) {
|
||||||
|
setState(() {
|
||||||
|
_enablePageView = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_enableDismiss) {
|
||||||
|
setState(() {
|
||||||
|
_enableDismiss = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_enablePageView) {
|
||||||
|
setState(() {
|
||||||
|
_enablePageView = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the left boundary has been hit after scaling up the source, the page
|
||||||
|
/// view swiping gets enabled if it has a page to swipe to.
|
||||||
|
void _onLeftBoundaryHit() {
|
||||||
|
if (!_enablePageView && _pageController!.page!.floor() > 0) {
|
||||||
|
setState(() {
|
||||||
|
_enablePageView = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the right boundary has been hit after scaling up the source, the page
|
||||||
|
/// view swiping gets enabled if it has a page to swipe to.
|
||||||
|
void _onRightBoundaryHit() {
|
||||||
|
if (!_enablePageView &&
|
||||||
|
_pageController!.page!.floor() < widget.sources.length - 1) {
|
||||||
|
setState(() {
|
||||||
|
_enablePageView = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the source has been scaled up and no horizontal boundary has been hit,
|
||||||
|
/// the page view swiping gets disabled.
|
||||||
|
void _onNoBoundaryHit() {
|
||||||
|
if (_enablePageView) {
|
||||||
|
setState(() {
|
||||||
|
_enablePageView = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the page view changed its page, the source will animate back into the
|
||||||
|
/// original scale if it was scaled up.
|
||||||
|
///
|
||||||
|
/// Additionally the swipe up / down to dismiss gets enabled.
|
||||||
|
void _onPageChanged(int page) {
|
||||||
|
setState(() {
|
||||||
|
currentIndex = page;
|
||||||
|
});
|
||||||
|
widget.onPageChanged?.call(page);
|
||||||
|
if (_transformationController!.value != Matrix4.identity()) {
|
||||||
|
// animate the reset for the transformation of the interactive viewer
|
||||||
|
|
||||||
|
_animation = Matrix4Tween(
|
||||||
|
begin: _transformationController!.value,
|
||||||
|
end: Matrix4.identity(),
|
||||||
|
).animate(
|
||||||
|
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||||
|
);
|
||||||
|
|
||||||
|
_animationController.forward(from: 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InteractiveViewerBoundary(
|
||||||
|
controller: _transformationController,
|
||||||
|
boundaryWidth: MediaQuery.of(context).size.width,
|
||||||
|
onScaleChanged: _onScaleChanged,
|
||||||
|
onLeftBoundaryHit: _onLeftBoundaryHit,
|
||||||
|
onRightBoundaryHit: _onRightBoundaryHit,
|
||||||
|
onNoBoundaryHit: _onNoBoundaryHit,
|
||||||
|
maxScale: widget.maxScale,
|
||||||
|
minScale: widget.minScale,
|
||||||
|
child: Stack(children: [
|
||||||
|
CustomDismissible(
|
||||||
|
onDismissed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onDismissed?.call(_pageController!.page!.floor());
|
||||||
|
},
|
||||||
|
enabled: _enableDismiss,
|
||||||
|
child: PageView.builder(
|
||||||
|
onPageChanged: _onPageChanged,
|
||||||
|
controller: _pageController,
|
||||||
|
physics:
|
||||||
|
_enablePageView ? null : const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: widget.sources.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return GestureDetector(
|
||||||
|
onDoubleTapDown: (TapDownDetails details) {
|
||||||
|
_doubleTapLocalPosition = details.localPosition;
|
||||||
|
},
|
||||||
|
onDoubleTap: onDoubleTap,
|
||||||
|
child: widget.itemBuilder(
|
||||||
|
context,
|
||||||
|
index,
|
||||||
|
index == currentIndex,
|
||||||
|
_enablePageView,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
12, 8, 20, MediaQuery.of(context).padding.bottom + 8),
|
||||||
|
decoration: _enablePageView
|
||||||
|
? BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.black.withOpacity(0.3)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onDismissed?.call(_pageController!.page!.floor());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
widget.sources.length > 1
|
||||||
|
? Text(
|
||||||
|
"${currentIndex! + 1}/${widget.sources.length}",
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 0,
|
||||||
|
onTap: () => onShareImg(widget.sources[currentIndex!]),
|
||||||
|
child: const Text("分享图片"),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 1,
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(
|
||||||
|
text:
|
||||||
|
widget.sources[currentIndex!].toString()))
|
||||||
|
.then((value) {
|
||||||
|
SmartDialog.showToast('已复制到粘贴板');
|
||||||
|
}).catchError((err) {
|
||||||
|
SmartDialog.showNotify(
|
||||||
|
msg: err.toString(),
|
||||||
|
notifyType: NotifyType.error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text("复制图片"),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 2,
|
||||||
|
onTap: () {
|
||||||
|
DownloadUtils.downloadImg(
|
||||||
|
widget.sources[currentIndex!]);
|
||||||
|
},
|
||||||
|
child: const Text("保存图片"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.more_horiz, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片分享
|
||||||
|
void onShareImg(String imgUrl) async {
|
||||||
|
SmartDialog.showLoading();
|
||||||
|
var response = await Dio()
|
||||||
|
.get(imgUrl, options: Options(responseType: ResponseType.bytes));
|
||||||
|
final temp = await getTemporaryDirectory();
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
String imgName =
|
||||||
|
"plpl_pic_${DateTime.now().toString().split('-').join()}.jpg";
|
||||||
|
var path = '${temp.path}/$imgName';
|
||||||
|
File(path).writeAsBytesSync(response.data);
|
||||||
|
Share.shareXFiles([XFile(path)], subject: imgUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleTap() {
|
||||||
|
Matrix4 matrix = _transformationController!.value.clone();
|
||||||
|
double currentScale = matrix.row0.x;
|
||||||
|
|
||||||
|
double targetScale = widget.minScale;
|
||||||
|
|
||||||
|
if (currentScale <= widget.minScale) {
|
||||||
|
targetScale = widget.maxScale * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
double offSetX = targetScale == 1.0
|
||||||
|
? 0.0
|
||||||
|
: -_doubleTapLocalPosition.dx * (targetScale - 1);
|
||||||
|
double offSetY = targetScale == 1.0
|
||||||
|
? 0.0
|
||||||
|
: -_doubleTapLocalPosition.dy * (targetScale - 1);
|
||||||
|
|
||||||
|
matrix = Matrix4.fromList([
|
||||||
|
targetScale,
|
||||||
|
matrix.row1.x,
|
||||||
|
matrix.row2.x,
|
||||||
|
matrix.row3.x,
|
||||||
|
matrix.row0.y,
|
||||||
|
targetScale,
|
||||||
|
matrix.row2.y,
|
||||||
|
matrix.row3.y,
|
||||||
|
matrix.row0.z,
|
||||||
|
matrix.row1.z,
|
||||||
|
targetScale,
|
||||||
|
matrix.row3.z,
|
||||||
|
offSetX,
|
||||||
|
offSetY,
|
||||||
|
matrix.row2.w,
|
||||||
|
matrix.row3.w
|
||||||
|
]);
|
||||||
|
|
||||||
|
_animation = Matrix4Tween(
|
||||||
|
begin: _transformationController!.value,
|
||||||
|
end: matrix,
|
||||||
|
).animate(
|
||||||
|
CurveTween(curve: Curves.easeOut).animate(_animationController),
|
||||||
|
);
|
||||||
|
_animationController
|
||||||
|
.forward(from: 0)
|
||||||
|
.whenComplete(() => _onScaleChanged(targetScale));
|
||||||
|
}
|
||||||
|
}
|
@ -70,14 +70,6 @@ class Routes {
|
|||||||
CustomGetPage(name: '/hot', page: () => const HotPage()),
|
CustomGetPage(name: '/hot', page: () => const HotPage()),
|
||||||
// 视频详情
|
// 视频详情
|
||||||
CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
|
CustomGetPage(name: '/video', page: () => const VideoDetailPage()),
|
||||||
// 图片预览
|
|
||||||
// GetPage(
|
|
||||||
// name: '/preview',
|
|
||||||
// page: () => const ImagePreview(),
|
|
||||||
// transition: Transition.fade,
|
|
||||||
// transitionDuration: const Duration(milliseconds: 300),
|
|
||||||
// showCupertinoParallax: false,
|
|
||||||
// ),
|
|
||||||
//
|
//
|
||||||
CustomGetPage(name: '/webview', page: () => const WebviewPage()),
|
CustomGetPage(name: '/webview', page: () => const WebviewPage()),
|
||||||
// 设置
|
// 设置
|
||||||
|
24
pubspec.lock
24
pubspec.lock
@ -441,22 +441,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.0.1"
|
||||||
extended_image:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: extended_image
|
|
||||||
sha256: d7f091d068fcac7246c4b22a84b8dac59a62e04d29a5c172710c696e67a22f94
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "8.2.0"
|
|
||||||
extended_image_library:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: extended_image_library
|
|
||||||
sha256: c9caee8fe9b6547bd41c960c4f2d1ef8e34321804de6a1777f1d614a24247ad6
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.4"
|
|
||||||
extended_list:
|
extended_list:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -726,14 +710,6 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
http_client_helper:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_client_helper
|
|
||||||
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
http_multi_server:
|
http_multi_server:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -49,7 +49,6 @@ dependencies:
|
|||||||
|
|
||||||
# 图片
|
# 图片
|
||||||
cached_network_image: ^3.3.0
|
cached_network_image: ^3.3.0
|
||||||
extended_image: ^8.2.0
|
|
||||||
saver_gallery: ^3.0.1
|
saver_gallery: ^3.0.1
|
||||||
|
|
||||||
# 存储
|
# 存储
|
||||||
|
Reference in New Issue
Block a user