mirror of
https://github.com/ZhuJHua/moodiary.git
synced 2026-04-05 16:31:45 +08:00
refactor(media): reconstruct the media library, with more animation and better performance
This commit is contained in:
66
lib/components/base/image.dart
Normal file
66
lib/components/base/image.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/utils/media_util.dart';
|
||||
|
||||
class ThumbnailImage extends StatelessWidget {
|
||||
final String imagePath;
|
||||
final int size;
|
||||
final BoxFit? fit;
|
||||
final Function() onTap;
|
||||
|
||||
const ThumbnailImage({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
required this.size,
|
||||
this.fit,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fileImage = FileImage(File(imagePath));
|
||||
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final Future getAspectRatio = MediaUtil.getImageAspectRatio(fileImage);
|
||||
return FutureBuilder(
|
||||
future: getAspectRatio,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
final aspectRatio = snapshot.data as double;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Hero(
|
||||
tag: imagePath,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ResizeImage(
|
||||
fileImage,
|
||||
width: aspectRatio < 1.0
|
||||
? size * devicePixelRatio.toInt()
|
||||
: null,
|
||||
height: aspectRatio >= 1.0
|
||||
? (size * devicePixelRatio).toInt()
|
||||
: null,
|
||||
),
|
||||
fit: fit ?? BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: colorScheme.surfaceContainer,
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.image_search_rounded,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:rive_animated_icon/rive_animated_icon.dart';
|
||||
|
||||
class Processing extends StatelessWidget {
|
||||
const Processing({super.key});
|
||||
final Color? color;
|
||||
|
||||
const Processing({super.key, this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -11,7 +13,7 @@ class Processing extends StatelessWidget {
|
||||
riveIcon: RiveIcon.reload,
|
||||
width: 80,
|
||||
height: 80,
|
||||
color: colorScheme.onSurface,
|
||||
color: color ?? colorScheme.onSurface,
|
||||
strokeWidth: 4.0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'parser.dart';
|
||||
@@ -639,22 +640,28 @@ class MarkdownToolbarState extends State<MarkdownToolbar> {
|
||||
widget.useIncludedTextField
|
||||
? _includedFocusNode.requestFocus()
|
||||
: widget.focusNode?.requestFocus();
|
||||
Format.toolbarItemPressed(
|
||||
markdownToolbarOption: markdownToolbarOption,
|
||||
controller: widget.useIncludedTextField
|
||||
? _includedController
|
||||
: widget.controller ?? _includedController,
|
||||
selection: widget.useIncludedTextField
|
||||
? _includedController.selection
|
||||
: widget.controller?.selection ?? _includedController.selection,
|
||||
option: option,
|
||||
customBoldCharacter: widget.boldCharacter,
|
||||
customItalicCharacter: widget.italicCharacter,
|
||||
customCodeCharacter: widget.codeCharacter,
|
||||
customBulletedListCharacter: widget.bulletedListCharacter,
|
||||
customHorizontalRuleCharacter: widget.horizontalRuleCharacter,
|
||||
mediaPath: mediaPath,
|
||||
);
|
||||
try {
|
||||
Format.toolbarItemPressed(
|
||||
markdownToolbarOption: markdownToolbarOption,
|
||||
controller: widget.useIncludedTextField
|
||||
? _includedController
|
||||
: widget.controller ?? _includedController,
|
||||
selection: widget.useIncludedTextField
|
||||
? _includedController.selection
|
||||
: widget.controller?.selection ?? _includedController.selection,
|
||||
option: option,
|
||||
customBoldCharacter: widget.boldCharacter,
|
||||
customItalicCharacter: widget.italicCharacter,
|
||||
customCodeCharacter: widget.codeCharacter,
|
||||
customBulletedListCharacter: widget.bulletedListCharacter,
|
||||
customHorizontalRuleCharacter: widget.horizontalRuleCharacter,
|
||||
mediaPath: mediaPath,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -18,7 +18,7 @@ class MediaAudioComponent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
DateFormat.yMMMEd().format(dateTime),
|
||||
style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/router/app_routes.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
|
||||
@@ -19,7 +17,6 @@ class MediaImageComponent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final textStyle = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
@@ -28,9 +25,9 @@ class MediaImageComponent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
DateFormat.yMMMEd().format(dateTime),
|
||||
DateFormat.yMMMMEEEEd().format(dateTime),
|
||||
style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary),
|
||||
),
|
||||
),
|
||||
@@ -38,27 +35,19 @@ class MediaImageComponent extends StatelessWidget {
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 120,
|
||||
childAspectRatio: 1.0,
|
||||
crossAxisSpacing: 1.0,
|
||||
mainAxisSpacing: 1.0,
|
||||
),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
return ThumbnailImage(
|
||||
imagePath: imageList[index],
|
||||
size: 120,
|
||||
onTap: () {
|
||||
_toPhotoView(index, imageList);
|
||||
},
|
||||
child: Hero(
|
||||
tag: imageList[index],
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Image.file(
|
||||
File(imageList[index]),
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: 120 * pixelRatio.toInt(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: imageList.length,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/router/app_routes.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
@@ -22,7 +20,6 @@ class MediaVideoComponent extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final textStyle = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
// 将视频路径转换为缩略图路径
|
||||
@@ -36,7 +33,7 @@ class MediaVideoComponent extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
DateFormat.yMMMEd().format(dateTime),
|
||||
style: textStyle.titleSmall?.copyWith(color: colorScheme.secondary),
|
||||
@@ -44,34 +41,26 @@ class MediaVideoComponent extends StatelessWidget {
|
||||
),
|
||||
GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 120,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
maxCrossAxisExtent: 120,
|
||||
childAspectRatio: 1.0,
|
||||
crossAxisSpacing: 1.0,
|
||||
mainAxisSpacing: 1.0),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
onTap: () {
|
||||
_toVideoView(videoList, index);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Card(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Image.file(
|
||||
File(thumbnailList[index]),
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: 120 * pixelRatio.toInt(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const FaIcon(FontAwesomeIcons.play)
|
||||
],
|
||||
),
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ThumbnailImage(
|
||||
imagePath: thumbnailList[index],
|
||||
size: 120,
|
||||
onTap: () {
|
||||
_toVideoView(videoList, index);
|
||||
},
|
||||
),
|
||||
const FaIcon(FontAwesomeIcons.play)
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: thumbnailList.length,
|
||||
|
||||
@@ -41,9 +41,11 @@ class DiaryDetailsLogic extends GetxController {
|
||||
}
|
||||
|
||||
//点击图片跳转到图片预览页面
|
||||
void toPhotoView(List<String> imagePathList, int index) {
|
||||
void toPhotoView(
|
||||
List<String> imagePathList, int index, BuildContext context) {
|
||||
HapticFeedback.selectionClick();
|
||||
Get.toNamed(AppRoutes.photoPage, arguments: [imagePathList, index]);
|
||||
// showImagePreview(context, imagePathList, index);
|
||||
}
|
||||
|
||||
//点击视频跳转到视频预览页面
|
||||
|
||||
@@ -256,6 +256,7 @@ class DiaryDetailsPage extends StatelessWidget {
|
||||
'image', state.diary.imageName[i]);
|
||||
}),
|
||||
index,
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: Image.file(
|
||||
|
||||
@@ -306,9 +306,9 @@ class EditLogic extends GetxController {
|
||||
}
|
||||
|
||||
//预览图片
|
||||
void toPhotoView(List<String> imagePath, int index) {
|
||||
Get.toNamed(AppRoutes.photoPage, arguments: [imagePath, index]);
|
||||
}
|
||||
// void toPhotoView(List<String> imagePath, int index) {
|
||||
// Get.toNamed(AppRoutes.photoPage, arguments: [imagePath, index]);
|
||||
// }
|
||||
|
||||
//预览视频
|
||||
void toVideoView(List<String> videoPath, int index) {
|
||||
|
||||
@@ -36,7 +36,6 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
@override
|
||||
void onReady() {
|
||||
getHitokoto();
|
||||
|
||||
//监听 tab
|
||||
tabController.addListener(_tabBarListener);
|
||||
//监听 inner
|
||||
@@ -79,7 +78,7 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
checkPageChange();
|
||||
// 检查是否显示顶部内容
|
||||
_checkShowTop();
|
||||
//homeLogic.resetNavigatorBar();
|
||||
homeLogic.resetNavigatorBar();
|
||||
}
|
||||
|
||||
/// 跳转到指定分类
|
||||
|
||||
@@ -31,7 +31,7 @@ class HomeLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
CurvedAnimation(
|
||||
parent: _barAnimationController, curve: Curves.easeInOut));
|
||||
|
||||
//late final PageController pageController = PageController();
|
||||
late final PageController pageController = PageController();
|
||||
|
||||
late final FrostedGlassOverlayLogic frostedGlassOverlayLogic =
|
||||
Bind.find<FrostedGlassOverlayLogic>();
|
||||
@@ -58,7 +58,7 @@ class HomeLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
void onClose() {
|
||||
_fabAnimationController.dispose();
|
||||
_barAnimationController.dispose();
|
||||
//pageController.dispose();
|
||||
pageController.dispose();
|
||||
_appLifecycleListener.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class HomeLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
void changeNavigator(int index) {
|
||||
navigatorIndex.value = index;
|
||||
shouldShow.value = index == 0;
|
||||
//pageController.jumpToPage(index);
|
||||
pageController.jumpToPage(index);
|
||||
}
|
||||
|
||||
Future<void> hideNavigatorBar() async {
|
||||
|
||||
@@ -109,23 +109,17 @@ class HomePage extends StatelessWidget {
|
||||
key: const ValueKey('body'),
|
||||
builder: (_) {
|
||||
return AdaptiveBackground(
|
||||
child: Obx(
|
||||
() {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
reverseDuration:
|
||||
const Duration(milliseconds: 100),
|
||||
key: logic.bodyKey,
|
||||
child: switch (logic.navigatorIndex.value) {
|
||||
0 => const DiaryPage(),
|
||||
1 => const CalendarPage(),
|
||||
2 => const MediaPage(),
|
||||
3 => const SettingPage(),
|
||||
_ => throw UnimplementedError(),
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
child: PageView(
|
||||
key: logic.bodyKey,
|
||||
controller: logic.pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
DiaryPage(),
|
||||
CalendarPage(),
|
||||
MediaPage(),
|
||||
SettingPage(),
|
||||
],
|
||||
));
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
@@ -126,7 +126,9 @@ class MediaPage extends StatelessWidget {
|
||||
state.datetimeMediaMap[datetime]!;
|
||||
return switch (state.mediaType.value) {
|
||||
MediaType.image => MediaImageComponent(
|
||||
dateTime: datetime, imageList: fileList),
|
||||
dateTime: datetime,
|
||||
imageList: fileList,
|
||||
),
|
||||
MediaType.audio => MediaAudioComponent(
|
||||
dateTime: datetime, audioList: fileList),
|
||||
MediaType.video => MediaVideoComponent(
|
||||
|
||||
@@ -5,8 +5,13 @@ import 'image_state.dart';
|
||||
|
||||
class ImageLogic extends GetxController {
|
||||
final ImageState state = ImageState();
|
||||
late final PageController pageController =
|
||||
PageController(initialPage: state.imageIndex.value);
|
||||
late final PageController pageController;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
pageController = PageController(initialPage: state.imageIndex.value);
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
|
||||
@@ -3,12 +3,12 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/common/values/media_type.dart';
|
||||
import 'package:moodiary/components/base/button.dart';
|
||||
import 'package:moodiary/components/loading/loading.dart';
|
||||
import 'package:moodiary/pages/image/image_logic.dart';
|
||||
import 'package:moodiary/utils/media_util.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:refreshed/refreshed.dart';
|
||||
|
||||
import 'image_logic.dart';
|
||||
import 'package:refreshed/get_state_manager/get_state_manager.dart';
|
||||
|
||||
class ImagePage extends StatelessWidget {
|
||||
const ImagePage({super.key});
|
||||
@@ -24,15 +24,15 @@ class ImagePage extends StatelessWidget {
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: colorScheme.scrim.withAlpha((255 * 0.6).toInt()),
|
||||
title: Obx(() {
|
||||
return Visibility(
|
||||
visible: state.imagePathList.length > 1,
|
||||
child: Text(
|
||||
title: Visibility(
|
||||
visible: state.imagePathList.length > 1,
|
||||
child: Obx(() {
|
||||
return Text(
|
||||
'${state.imageIndex.value + 1}/${state.imagePathList.length}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -49,7 +49,12 @@ class ImagePage extends StatelessWidget {
|
||||
children: [
|
||||
PhotoViewGallery.builder(
|
||||
scrollPhysics: const PageScrollPhysics(),
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.black,
|
||||
),
|
||||
pageController: logic.pageController,
|
||||
wantKeepAlive: true,
|
||||
gaplessPlayback: true,
|
||||
builder: (BuildContext context, int index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider: FileImage(File(state.imagePathList[index])),
|
||||
@@ -60,8 +65,14 @@ class ImagePage extends StatelessWidget {
|
||||
},
|
||||
itemCount: state.imagePathList.length,
|
||||
onPageChanged: logic.changePage,
|
||||
loadingBuilder: (context, event) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
loadingBuilder: (context, event) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: const Center(
|
||||
child: Processing(color: Colors.white),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
return Visibility(
|
||||
|
||||
@@ -58,7 +58,7 @@ class WebViewPage extends StatelessWidget {
|
||||
child: InAppWebView(
|
||||
initialUrlRequest: URLRequest(url: WebUri(state.url)),
|
||||
initialSettings: logic.webSettings,
|
||||
// pullToRefreshController: logic.pullToRefreshController,
|
||||
// pullToRefreshController: logic.pullToRefreshController,
|
||||
onWebViewCreated: (controller) {
|
||||
logic.webViewController = controller;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
|
||||
@@ -185,24 +186,47 @@ class MediaUtil {
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
//获取图片宽高比例
|
||||
static const int _maxCacheSize = 1000;
|
||||
static final LinkedHashMap<String, double> _cache = LinkedHashMap();
|
||||
|
||||
static Future<double> getImageAspectRatio(ImageProvider imageProvider) async {
|
||||
final Completer<double> completer = Completer<double>();
|
||||
final ImageStream stream =
|
||||
imageProvider.resolve(const ImageConfiguration());
|
||||
stream.addListener(
|
||||
ImageStreamListener(
|
||||
(ImageInfo info, bool _) {
|
||||
final double aspectRatio = double.parse(
|
||||
(info.image.width.toDouble() / info.image.height.toDouble())
|
||||
.toStringAsPrecision(2));
|
||||
completer.complete(aspectRatio);
|
||||
},
|
||||
),
|
||||
final key = _getImageKey(imageProvider);
|
||||
|
||||
if (_cache.containsKey(key)) {
|
||||
return _cache[key]!;
|
||||
}
|
||||
|
||||
final completer = Completer<double>();
|
||||
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||
imageStream.addListener(
|
||||
ImageStreamListener((ImageInfo info, bool _) {
|
||||
final aspectRatio = info.image.width / info.image.height;
|
||||
_addToCache(key, aspectRatio);
|
||||
completer.complete(aspectRatio);
|
||||
}),
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
static void _addToCache(String key, double ratio) {
|
||||
if (_cache.length >= _maxCacheSize) {
|
||||
_cache.remove(_cache.keys.first);
|
||||
}
|
||||
_cache[key] = ratio;
|
||||
}
|
||||
|
||||
static String _getImageKey(ImageProvider imageProvider) {
|
||||
if (imageProvider is AssetImage) {
|
||||
return 'asset_${imageProvider.assetName}';
|
||||
} else if (imageProvider is NetworkImage) {
|
||||
return 'network_${imageProvider.url}';
|
||||
} else if (imageProvider is FileImage) {
|
||||
return 'file_${imageProvider.file.path}';
|
||||
}
|
||||
return imageProvider.toString();
|
||||
}
|
||||
|
||||
//获取单个图片,拍照或者相册
|
||||
static Future<XFile?> pickPhoto(ImageSource imageSource) async {
|
||||
return await _picker.pickImage(
|
||||
|
||||
Reference in New Issue
Block a user