mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: live photo
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||||
|
show SourceModel;
|
||||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
|
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
@@ -68,7 +70,8 @@ Widget articleContent({
|
|||||||
} else {
|
} else {
|
||||||
context.imageView(
|
context.imageView(
|
||||||
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
|
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
|
||||||
imgList: imgList,
|
imgList:
|
||||||
|
imgList.map((url) => SourceModel(url: url)).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||||
|
show SourceModel;
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_html/flutter_html.dart';
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
@@ -54,7 +56,7 @@ Widget htmlRender({
|
|||||||
callback([imgUrl], 0);
|
callback([imgUrl], 0);
|
||||||
} else {
|
} else {
|
||||||
context.imageView(
|
context.imageView(
|
||||||
imgList: [imgUrl],
|
imgList: [SourceModel(url: imgUrl)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||||
|
show SourceModel, SourceType;
|
||||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/common/widgets/nine_grid_view.dart';
|
import 'package:PiliPlus/common/widgets/nine_grid_view.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
|
import 'package:PiliPlus/utils/storage.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class ImageModel {
|
class ImageModel {
|
||||||
@@ -12,16 +15,20 @@ class ImageModel {
|
|||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
required this.url,
|
required this.url,
|
||||||
|
this.liveUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
dynamic width;
|
dynamic width;
|
||||||
dynamic height;
|
dynamic height;
|
||||||
String url;
|
String url;
|
||||||
|
String? liveUrl;
|
||||||
bool? _isLongPic;
|
bool? _isLongPic;
|
||||||
|
bool? _isLivePhoto;
|
||||||
|
|
||||||
dynamic get safeWidth => width ?? 1;
|
dynamic get safeWidth => width ?? 1;
|
||||||
dynamic get safeHeight => height ?? 1;
|
dynamic get safeHeight => height ?? 1;
|
||||||
bool get isLongPic => _isLongPic ??= (safeHeight / safeWidth) > (22 / 9);
|
bool get isLongPic => _isLongPic ??= (safeHeight / safeWidth) > (22 / 9);
|
||||||
|
bool get isLivePhoto => _isLivePhoto ??= liveUrl?.isNotEmpty == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget imageview(
|
Widget imageview(
|
||||||
@@ -83,6 +90,8 @@ Widget imageview(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late final enableLivePhoto = GStorage.enableLivePhoto;
|
||||||
|
|
||||||
return NineGridView(
|
return NineGridView(
|
||||||
type: NineGridType.weiBo,
|
type: NineGridType.weiBo,
|
||||||
margin: const EdgeInsets.only(top: 6),
|
margin: const EdgeInsets.only(top: 6),
|
||||||
@@ -102,7 +111,17 @@ Widget imageview(
|
|||||||
onViewImage?.call();
|
onViewImage?.call();
|
||||||
context.imageView(
|
context.imageView(
|
||||||
initialPage: index,
|
initialPage: index,
|
||||||
imgList: picArr.map((item) => item.url).toList(),
|
imgList: picArr
|
||||||
|
.map(
|
||||||
|
(item) => SourceModel(
|
||||||
|
sourceType: item.isLivePhoto && enableLivePhoto
|
||||||
|
? SourceType.livePhoto
|
||||||
|
: SourceType.networkImage,
|
||||||
|
url: item.url,
|
||||||
|
liveUrl: item.liveUrl,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
onDismissed: onDismissed,
|
onDismissed: onDismissed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143,7 +162,14 @@ Widget imageview(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (picArr[index].isLongPic)
|
if (picArr[index].liveUrl?.isNotEmpty == true)
|
||||||
|
const PBadge(
|
||||||
|
text: 'Live',
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
type: 'gray',
|
||||||
|
)
|
||||||
|
else if (picArr[index].isLongPic)
|
||||||
const PBadge(
|
const PBadge(
|
||||||
text: '长图',
|
text: '长图',
|
||||||
right: 8,
|
right: 8,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:status_bar_control/status_bar_control.dart';
|
import 'package:status_bar_control/status_bar_control.dart';
|
||||||
@@ -33,6 +35,20 @@ typedef IndexedFocusedWidgetBuilder = Widget Function(
|
|||||||
|
|
||||||
typedef IndexedTagStringBuilder = String Function(int index);
|
typedef IndexedTagStringBuilder = String Function(int index);
|
||||||
|
|
||||||
|
enum SourceType { fileImage, networkImage, livePhoto }
|
||||||
|
|
||||||
|
class SourceModel {
|
||||||
|
final SourceType sourceType;
|
||||||
|
final String url;
|
||||||
|
final String? liveUrl;
|
||||||
|
|
||||||
|
const SourceModel({
|
||||||
|
this.sourceType = SourceType.networkImage,
|
||||||
|
required this.url,
|
||||||
|
this.liveUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class InteractiveviewerGallery<T> extends StatefulWidget {
|
class InteractiveviewerGallery<T> extends StatefulWidget {
|
||||||
const InteractiveviewerGallery({
|
const InteractiveviewerGallery({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -45,17 +61,14 @@ class InteractiveviewerGallery<T> extends StatefulWidget {
|
|||||||
this.onDismissed,
|
this.onDismissed,
|
||||||
this.setStatusBar,
|
this.setStatusBar,
|
||||||
this.onClose,
|
this.onClose,
|
||||||
this.isFile,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool? isFile;
|
|
||||||
|
|
||||||
final VoidCallback? onClose;
|
final VoidCallback? onClose;
|
||||||
|
|
||||||
final bool? setStatusBar;
|
final bool? setStatusBar;
|
||||||
|
|
||||||
/// The sources to show.
|
/// The sources to show.
|
||||||
final List<String> sources;
|
final List<SourceModel> sources;
|
||||||
|
|
||||||
/// The index of the first source in [sources] to show.
|
/// The index of the first source in [sources] to show.
|
||||||
final int initIndex;
|
final int initIndex;
|
||||||
@@ -92,7 +105,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
|
|
||||||
late Offset _doubleTapLocalPosition;
|
late Offset _doubleTapLocalPosition;
|
||||||
|
|
||||||
int? currentIndex;
|
late final RxInt currentIndex = widget.initIndex.obs;
|
||||||
|
|
||||||
late List<bool> _thumbList;
|
late List<bool> _thumbList;
|
||||||
late final int _quality = GStorage.previewQ;
|
late final int _quality = GStorage.previewQ;
|
||||||
@@ -115,10 +128,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
_animation?.value ?? Matrix4.identity();
|
_animation?.value ?? Matrix4.identity();
|
||||||
});
|
});
|
||||||
|
|
||||||
currentIndex = widget.initIndex;
|
|
||||||
if (widget.setStatusBar != false) {
|
if (widget.setStatusBar != false) {
|
||||||
setStatusBar();
|
setStatusBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.sources[currentIndex.value].sourceType == SourceType.livePhoto) {
|
||||||
|
_onPlay(currentIndex.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatusBar() async {
|
setStatusBar() async {
|
||||||
@@ -132,6 +148,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() async {
|
void dispose() async {
|
||||||
|
_player?.dispose();
|
||||||
_pageController?.dispose();
|
_pageController?.dispose();
|
||||||
_animationController.removeListener(() {});
|
_animationController.removeListener(() {});
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
@@ -140,8 +157,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
StatusBarControl.setHidden(false, animation: StatusBarAnimation.FADE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (widget.isFile != true) {
|
for (int index = 0; index < widget.sources.length; index++) {
|
||||||
for (int index = 0; index < widget.sources.length; index++) {
|
if (widget.sources[index].sourceType == SourceType.networkImage) {
|
||||||
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
CachedNetworkImageProvider(_getActualUrl(index)).evict();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,14 +218,22 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onPlay(int index) {
|
||||||
|
_player ??= Player();
|
||||||
|
_videoController ??= VideoController(_player!);
|
||||||
|
_player!.open(Media(widget.sources[index].liveUrl!));
|
||||||
|
}
|
||||||
|
|
||||||
/// When the page view changed its page, the source will animate back into the
|
/// When the page view changed its page, the source will animate back into the
|
||||||
/// original scale if it was scaled up.
|
/// original scale if it was scaled up.
|
||||||
///
|
///
|
||||||
/// Additionally the swipe up / down to dismiss gets enabled.
|
/// Additionally the swipe up / down to dismiss gets enabled.
|
||||||
void _onPageChanged(int page) {
|
void _onPageChanged(int page) {
|
||||||
setState(() {
|
_player?.pause();
|
||||||
currentIndex = page;
|
currentIndex.value = page;
|
||||||
});
|
if (widget.sources[page].sourceType == SourceType.livePhoto) {
|
||||||
|
_onPlay(page);
|
||||||
|
}
|
||||||
widget.onPageChanged?.call(page);
|
widget.onPageChanged?.call(page);
|
||||||
if (_transformationController!.value != Matrix4.identity()) {
|
if (_transformationController!.value != Matrix4.identity()) {
|
||||||
// animate the reset for the transformation of the interactive viewer
|
// animate the reset for the transformation of the interactive viewer
|
||||||
@@ -226,7 +251,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
|
|
||||||
String _getActualUrl(int index) => _thumbList[index] && _quality != 100
|
String _getActualUrl(int index) => _thumbList[index] && _quality != 100
|
||||||
? '${widget.sources[index]}@${_quality}q.webp'.http2https
|
? '${widget.sources[index]}@${_quality}q.webp'.http2https
|
||||||
: widget.sources[index].http2https;
|
: widget.sources[index].url.http2https;
|
||||||
|
|
||||||
void onClose() {
|
void onClose() {
|
||||||
if (widget.onClose != null) {
|
if (widget.onClose != null) {
|
||||||
@@ -237,6 +262,9 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Player? _player;
|
||||||
|
VideoController? _videoController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -272,12 +300,15 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
_doubleTapLocalPosition = details.localPosition;
|
_doubleTapLocalPosition = details.localPosition;
|
||||||
},
|
},
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
onLongPress: widget.isFile == true ? null : onLongPress,
|
onLongPress:
|
||||||
|
widget.sources[index].sourceType == SourceType.fileImage
|
||||||
|
? null
|
||||||
|
: onLongPress,
|
||||||
child: widget.itemBuilder != null
|
child: widget.itemBuilder != null
|
||||||
? widget.itemBuilder!(
|
? widget.itemBuilder!(
|
||||||
context,
|
context,
|
||||||
index,
|
index,
|
||||||
index == currentIndex,
|
index == currentIndex.value,
|
||||||
_enablePageView,
|
_enablePageView,
|
||||||
)
|
)
|
||||||
: _itemBuilder(index),
|
: _itemBuilder(index),
|
||||||
@@ -321,47 +352,60 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
if (widget.sources.length > 1)
|
if (widget.sources.length > 1)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Obx(
|
||||||
"${currentIndex! + 1}/${widget.sources.length}",
|
() => Text(
|
||||||
style: const TextStyle(color: Colors.white),
|
"${currentIndex.value + 1}/${widget.sources.length}",
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.isFile != true)
|
if (widget.sources[currentIndex.value].sourceType !=
|
||||||
|
SourceType.fileImage)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: PopupMenuButton(
|
child: PopupMenuButton(
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 0,
|
onTap: () => onShareImg(
|
||||||
onTap: () =>
|
widget.sources[currentIndex.value].url),
|
||||||
onShareImg(widget.sources[currentIndex!]),
|
|
||||||
child: const Text("分享图片"),
|
child: const Text("分享图片"),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 1,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Utils.copyText(widget.sources[currentIndex!]);
|
Utils.copyText(
|
||||||
|
widget.sources[currentIndex.value].url);
|
||||||
},
|
},
|
||||||
child: const Text("复制链接"),
|
child: const Text("复制链接"),
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 2,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
DownloadUtils.downloadImg(
|
DownloadUtils.downloadImg(
|
||||||
context,
|
context,
|
||||||
[widget.sources[currentIndex!]],
|
[widget.sources[currentIndex.value].url],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text("保存图片"),
|
child: const Text("保存图片"),
|
||||||
),
|
),
|
||||||
|
if (widget.sources[currentIndex.value].sourceType ==
|
||||||
|
SourceType.livePhoto)
|
||||||
|
PopupMenuItem(
|
||||||
|
onTap: () {
|
||||||
|
DownloadUtils.downloadVideo(
|
||||||
|
context,
|
||||||
|
widget.sources[currentIndex.value].liveUrl!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text("保存 Live"),
|
||||||
|
),
|
||||||
if (widget.sources.length > 1)
|
if (widget.sources.length > 1)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 3,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
DownloadUtils.downloadImg(
|
DownloadUtils.downloadImg(
|
||||||
context,
|
context,
|
||||||
widget.sources,
|
widget.sources
|
||||||
|
.map((item) => item.url)
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text("保存全部图片"),
|
child: const Text("保存全部图片"),
|
||||||
@@ -396,34 +440,37 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
Widget _itemBuilder(index) {
|
Widget _itemBuilder(index) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: widget.sources[index],
|
tag: widget.sources[index].url,
|
||||||
child: widget.isFile == true
|
child: switch (widget.sources[index].sourceType) {
|
||||||
? Image(
|
SourceType.fileImage => Image(
|
||||||
filterQuality: FilterQuality.low,
|
filterQuality: FilterQuality.low,
|
||||||
image: FileImage(File(widget.sources[index])),
|
image: FileImage(File(widget.sources[index].url)),
|
||||||
)
|
),
|
||||||
: CachedNetworkImage(
|
SourceType.networkImage => CachedNetworkImage(
|
||||||
fadeInDuration: const Duration(milliseconds: 0),
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
fadeOutDuration: const Duration(milliseconds: 0),
|
fadeOutDuration: const Duration(milliseconds: 0),
|
||||||
imageUrl: _getActualUrl(index),
|
imageUrl: _getActualUrl(index),
|
||||||
// fit: BoxFit.contain,
|
// fit: BoxFit.contain,
|
||||||
progressIndicatorBuilder: (context, url, progress) {
|
progressIndicatorBuilder: (context, url, progress) {
|
||||||
return Center(
|
return Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 150.0,
|
width: 150.0,
|
||||||
child: LinearProgressIndicator(
|
child:
|
||||||
value: progress.progress ?? 0),
|
LinearProgressIndicator(value: progress.progress ?? 0),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
// errorListener: (value) {
|
// errorListener: (value) {
|
||||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// setState(() {
|
// setState(() {
|
||||||
// _thumbList[index] = false;
|
// _thumbList[index] = false;
|
||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
// },
|
// },
|
||||||
),
|
),
|
||||||
|
SourceType.livePhoto =>
|
||||||
|
IgnorePointer(child: Video(controller: _videoController!)),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -487,7 +534,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
onShareImg(widget.sources[currentIndex!]);
|
onShareImg(widget.sources[currentIndex.value].url);
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -496,7 +543,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
ListTile(
|
ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
Utils.copyText(widget.sources[currentIndex!]);
|
Utils.copyText(widget.sources[currentIndex.value].url);
|
||||||
},
|
},
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
title: const Text('复制链接', style: TextStyle(fontSize: 14)),
|
||||||
@@ -506,19 +553,35 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
|
|||||||
Get.back();
|
Get.back();
|
||||||
DownloadUtils.downloadImg(
|
DownloadUtils.downloadImg(
|
||||||
context,
|
context,
|
||||||
[widget.sources[currentIndex!]],
|
[widget.sources[currentIndex.value].url],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
title: const Text('保存图片', style: TextStyle(fontSize: 14)),
|
||||||
),
|
),
|
||||||
|
if (widget.sources[currentIndex.value].sourceType ==
|
||||||
|
SourceType.livePhoto)
|
||||||
|
ListTile(
|
||||||
|
onTap: () {
|
||||||
|
Get.back();
|
||||||
|
DownloadUtils.downloadVideo(
|
||||||
|
context,
|
||||||
|
widget.sources[currentIndex.value].liveUrl!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
title: const Text(
|
||||||
|
'保存 Live',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (widget.sources.length > 1)
|
if (widget.sources.length > 1)
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.back();
|
Get.back();
|
||||||
DownloadUtils.downloadImg(
|
DownloadUtils.downloadImg(
|
||||||
context,
|
context,
|
||||||
widget.sources,
|
widget.sources.map((item) => item.url).toList(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
dense: true,
|
dense: true,
|
||||||
|
|||||||
@@ -673,6 +673,7 @@ class OpusPicsModel {
|
|||||||
int? size;
|
int? size;
|
||||||
String? src;
|
String? src;
|
||||||
String? url;
|
String? url;
|
||||||
|
String? liveUrl;
|
||||||
|
|
||||||
OpusPicsModel.fromJson(Map<String, dynamic> json) {
|
OpusPicsModel.fromJson(Map<String, dynamic> json) {
|
||||||
width = json['width'];
|
width = json['width'];
|
||||||
@@ -680,6 +681,7 @@ class OpusPicsModel {
|
|||||||
size = json['size'] != null ? json['size'].toInt() : 0;
|
size = json['size'] != null ? json['size'].toInt() : 0;
|
||||||
src = json['src'];
|
src = json['src'];
|
||||||
url = json['url'];
|
url = json['url'];
|
||||||
|
liveUrl = json['live_url'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/http_error.dart';
|
import 'package:PiliPlus/common/widgets/http_error.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||||
|
show SourceModel;
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -224,9 +226,11 @@ class _BangumiInfoState extends State<BangumiInfo>
|
|||||||
videoDetailCtr.onViewImage();
|
videoDetailCtr.onViewImage();
|
||||||
context.imageView(
|
context.imageView(
|
||||||
imgList: [
|
imgList: [
|
||||||
!widget.isLoading
|
SourceModel(
|
||||||
? widget.bangumiDetail!.cover!
|
url: !widget.isLoading
|
||||||
: bangumiItem!.cover!
|
? widget.bangumiDetail!.cover!
|
||||||
|
: bangumiItem!.cover!,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
onDismissed: videoDetailCtr.onDismissed,
|
onDismissed: videoDetailCtr.onDismissed,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'dart:math';
|
|||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
import 'package:PiliPlus/common/widgets/icon_button.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||||
|
show SourceModel, SourceType;
|
||||||
import 'package:PiliPlus/http/msg.dart';
|
import 'package:PiliPlus/http/msg.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:chat_bottom_container/chat_bottom_container.dart';
|
import 'package:chat_bottom_container/chat_bottom_container.dart';
|
||||||
@@ -274,8 +276,12 @@ abstract class CommonPublishPageState<T extends CommonPublishPage>
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
controller.keepChatPanel();
|
controller.keepChatPanel();
|
||||||
context.imageView(
|
context.imageView(
|
||||||
isFile: true,
|
imgList: pathList
|
||||||
imgList: pathList,
|
.map((path) => SourceModel(
|
||||||
|
url: path,
|
||||||
|
sourceType: SourceType.fileImage,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
initialPage: index,
|
initialPage: index,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Widget content(context, item, source, callback) {
|
|||||||
width: item.width,
|
width: item.width,
|
||||||
height: item.height,
|
height: item.height,
|
||||||
url: item.url ?? '',
|
url: item.url ?? '',
|
||||||
|
liveUrl: item.liveUrl,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ InlineSpan picsNodes(List<OpusPicsModel> pics, callback) {
|
|||||||
width: item.width,
|
width: item.width,
|
||||||
height: item.height,
|
height: item.height,
|
||||||
url: item.url ?? '',
|
url: item.url ?? '',
|
||||||
|
liveUrl: item.liveUrl,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Widget picWidget(item, context, callback) {
|
|||||||
width: item.width,
|
width: item.width,
|
||||||
height: item.height,
|
height: item.height,
|
||||||
url: item.url ?? '',
|
url: item.url ?? '',
|
||||||
|
liveUrl: item.liveUrl,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
|
||||||
|
show SourceModel;
|
||||||
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||||
import 'package:PiliPlus/models/space/card.dart' as space;
|
import 'package:PiliPlus/models/space/card.dart' as space;
|
||||||
@@ -81,7 +83,7 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.imageView(
|
context.imageView(
|
||||||
imgList: [imgUrl ?? 'bgTag'],
|
imgList: [SourceModel(url: imgUrl ?? 'bgTag')],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
@@ -458,7 +460,7 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.imageView(
|
context.imageView(
|
||||||
imgList: [card.face ?? 'avatarTag'],
|
imgList: [SourceModel(url: card.face ?? 'avatarTag')],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
|
|||||||
@@ -1914,6 +1914,14 @@ List<SettingsModel> get extraSettings => [
|
|||||||
defaultVal: true,
|
defaultVal: true,
|
||||||
onChanged: (value) => ModuleAuthorModel.showDynDecorate = value,
|
onChanged: (value) => ModuleAuthorModel.showDynDecorate = value,
|
||||||
),
|
),
|
||||||
|
SettingsModel(
|
||||||
|
settingsType: SettingsType.sw1tch,
|
||||||
|
title: '预览 Live Photo',
|
||||||
|
subtitle: '开启则以视频形式预览Live Photo,否则预览静态图片',
|
||||||
|
leading: Icon(Icons.image_outlined),
|
||||||
|
setKey: SettingBoxKey.enableLivePhoto,
|
||||||
|
defaultVal: true,
|
||||||
|
),
|
||||||
SettingsModel(
|
SettingsModel(
|
||||||
settingsType: SettingsType.sw1tch,
|
settingsType: SettingsType.sw1tch,
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:saver_gallery/saver_gallery.dart';
|
import 'package:saver_gallery/saver_gallery.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@@ -85,6 +86,38 @@ class DownloadUtils {
|
|||||||
return await requestStoragePer(context);
|
return await requestStoragePer(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future downloadVideo(BuildContext context, String url) async {
|
||||||
|
try {
|
||||||
|
if (!await checkPermissionDependOnSdkInt(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SmartDialog.showLoading(msg: '正在下载');
|
||||||
|
String videoName =
|
||||||
|
"video_${DateTime.now().toString().replaceAll(' ', '_').replaceAll(':', '-').split('.').first}.${url.split('.').lastOrNull ?? 'mp4'}";
|
||||||
|
String savePath = '${(await getTemporaryDirectory()).path}/$videoName';
|
||||||
|
await Request.dio.download(url, savePath);
|
||||||
|
SmartDialog.showLoading(msg: '正在保存');
|
||||||
|
final SaveResult result = await SaverGallery.saveFile(
|
||||||
|
filePath: savePath,
|
||||||
|
fileName: videoName,
|
||||||
|
androidRelativePath: "Pictures/PiliPlus",
|
||||||
|
skipIfExists: false,
|
||||||
|
);
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
if (result.isSuccess) {
|
||||||
|
SmartDialog.showToast('「$videoName」已保存 ');
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast('保存失败,${result.errorMessage}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
SmartDialog.showToast(err.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static Future downloadImg(
|
static Future downloadImg(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<String> imgList, {
|
List<String> imgList, {
|
||||||
|
|||||||
@@ -73,15 +73,13 @@ extension BuildContextExt on BuildContext {
|
|||||||
|
|
||||||
void imageView({
|
void imageView({
|
||||||
int? initialPage,
|
int? initialPage,
|
||||||
required List<String> imgList,
|
required List<SourceModel> imgList,
|
||||||
ValueChanged<int>? onDismissed,
|
ValueChanged<int>? onDismissed,
|
||||||
bool? isFile,
|
|
||||||
}) {
|
}) {
|
||||||
Navigator.of(this).push(
|
Navigator.of(this).push(
|
||||||
HeroDialogRoute(
|
HeroDialogRoute(
|
||||||
builder: (context) => InteractiveviewerGallery(
|
builder: (context) => InteractiveviewerGallery(
|
||||||
sources: imgList,
|
sources: imgList,
|
||||||
isFile: isFile,
|
|
||||||
initIndex: initialPage ?? 0,
|
initIndex: initialPage ?? 0,
|
||||||
onPageChanged: (int pageIndex) {},
|
onPageChanged: (int pageIndex) {},
|
||||||
onDismissed: onDismissed,
|
onDismissed: onDismissed,
|
||||||
|
|||||||
@@ -354,6 +354,9 @@ class GStorage {
|
|||||||
static bool get showDynDecorate =>
|
static bool get showDynDecorate =>
|
||||||
GStorage.setting.get(SettingBoxKey.showDynDecorate, defaultValue: true);
|
GStorage.setting.get(SettingBoxKey.showDynDecorate, defaultValue: true);
|
||||||
|
|
||||||
|
static bool get enableLivePhoto =>
|
||||||
|
GStorage.setting.get(SettingBoxKey.enableLivePhoto, defaultValue: true);
|
||||||
|
|
||||||
static List<double> get dynamicDetailRatio => List<double>.from(setting
|
static List<double> get dynamicDetailRatio => List<double>.from(setting
|
||||||
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
|
.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]));
|
||||||
|
|
||||||
@@ -581,6 +584,7 @@ class SettingBoxKey {
|
|||||||
mainTabBarView = 'mainTabBarView',
|
mainTabBarView = 'mainTabBarView',
|
||||||
searchSuggestion = 'searchSuggestion',
|
searchSuggestion = 'searchSuggestion',
|
||||||
showDynDecorate = 'showDynDecorate',
|
showDynDecorate = 'showDynDecorate',
|
||||||
|
enableLivePhoto = 'enableLivePhoto',
|
||||||
|
|
||||||
// Sponsor Block
|
// Sponsor Block
|
||||||
enableSponsorBlock = 'enableSponsorBlock',
|
enableSponsorBlock = 'enableSponsorBlock',
|
||||||
|
|||||||
Reference in New Issue
Block a user