Преглед изворни кода

权限管理、拍照、相册等。

PC\19500 пре 3 недеља
родитељ
комит
9ce896d7a0

+ 2 - 0
android/app/src/profile/AndroidManifest.xml

@@ -4,4 +4,6 @@
          to allow setting breakpoints, to provide hot reload, etc.
     -->
     <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.CAMERA"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
 </manifest>

+ 164 - 0
lib/media/camera_service.dart

@@ -0,0 +1,164 @@
+import 'dart:io';
+import 'package:camera/camera.dart' as camera_pkg;
+import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'media_exception.dart';
+
+// 使用别名避免命名冲突
+typedef CameraController = camera_pkg.CameraController;
+typedef ResolutionPreset = camera_pkg.ResolutionPreset;
+typedef CameraLensDirection = camera_pkg.CameraLensDirection;
+typedef XFile = camera_pkg.XFile;
+
+/// 专业相机服务
+/// 
+/// 适合医疗影像、证件采集、高质量拍照、连拍/自定义UI
+/// 
+/// 使用示例:
+/// ```dart
+/// final cameraService = CameraService();
+/// await cameraService.init();
+/// final photo = await cameraService.takePicture();
+/// cameraService.dispose();
+/// ```
+class CameraService {
+  CameraController? _controller;
+  bool _isInitialized = false;
+  bool _isDisposed = false;
+
+  /// 初始化相机
+  /// 
+  /// - [resolutionPreset] 分辨率预设,默认 veryHigh
+  /// - [enableAudio] 是否启用音频,默认 false
+  /// - [lensDirection] 镜头方向,默认后置摄像头
+  /// 
+  /// 抛出 [MediaCameraException] 如果初始化失败
+  Future<void> init({
+    ResolutionPreset resolutionPreset = camera_pkg.ResolutionPreset.veryHigh,
+    bool enableAudio = false,
+    CameraLensDirection lensDirection = camera_pkg.CameraLensDirection.back,
+  }) async {
+    try {
+      if (_isDisposed) {
+        throw MediaCameraException('相机服务已释放,无法初始化');
+      }
+
+      if (_isInitialized) {
+        AppLogger.d('相机已初始化,跳过重复初始化');
+        return;
+      }
+
+      AppLogger.d('开始初始化相机...');
+      final cameras = await camera_pkg.availableCameras();
+      
+      if (cameras.isEmpty) {
+        throw MediaCameraException('未找到可用相机');
+      }
+
+      final camera = cameras.firstWhere(
+        (c) => c.lensDirection == lensDirection,
+        orElse: () => cameras.first,
+      );
+
+      _controller = camera_pkg.CameraController(
+        camera,
+        resolutionPreset,
+        enableAudio: enableAudio,
+      );
+
+      await _controller!.initialize();
+      _isInitialized = true;
+      AppLogger.d('相机初始化成功: ${camera.name}');
+    } catch (e) {
+      _isInitialized = false;
+      if (e is camera_pkg.CameraException) {
+        AppLogger.e('相机初始化失败(原生异常)', e);
+        throw MediaCameraException(
+          '相机初始化失败: ${e.toString()}',
+          originalError: e,
+        );
+      }
+      if (e is MediaCameraException) {
+        rethrow;
+      }
+      AppLogger.e('相机初始化失败', e);
+      throw MediaCameraException(
+        '相机初始化失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 获取相机控制器
+  /// 
+  /// 抛出 [MediaCameraException] 如果相机未初始化
+  CameraController get controller {
+    if (!_isInitialized || _controller == null) {
+      throw MediaCameraException('相机未初始化,请先调用 init()');
+    }
+    return _controller!;
+  }
+
+  /// 检查相机是否已初始化
+  bool get isInitialized => _isInitialized && _controller?.value.isInitialized == true;
+
+  /// 拍照
+  /// 
+  /// 返回拍摄的照片文件,如果失败返回 null
+  /// 
+  /// 抛出 [MediaCameraException] 如果拍照失败
+  Future<File?> takePicture() async {
+    try {
+      if (!isInitialized) {
+        throw MediaCameraException('相机未初始化,无法拍照');
+      }
+
+      AppLogger.d('开始拍照...');
+      final camera_pkg.XFile file = await _controller!.takePicture();
+      final photoFile = File(file.path);
+
+      if (!await photoFile.exists()) {
+        throw MediaCameraException('拍照失败:文件不存在');
+      }
+
+      AppLogger.d('拍照成功: ${photoFile.path}');
+      return photoFile;
+    } catch (e) {
+      if (e is camera_pkg.CameraException) {
+        AppLogger.e('拍照失败(原生异常)', e);
+        throw MediaCameraException(
+          '拍照失败: ${e.toString()}',
+          originalError: e,
+        );
+      }
+      if (e is MediaCameraException) {
+        rethrow;
+      }
+      AppLogger.e('拍照失败', e);
+      throw MediaCameraException(
+        '拍照失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 释放相机资源
+  /// 
+  /// 必须在不再使用相机时调用,否则可能导致资源泄漏
+  Future<void> dispose() async {
+    if (_isDisposed) {
+      return;
+    }
+
+    try {
+      if (_controller != null) {
+        await _controller!.dispose();
+        _controller = null;
+      }
+      _isInitialized = false;
+      _isDisposed = true;
+      AppLogger.d('相机资源已释放');
+    } catch (e) {
+      AppLogger.e('释放相机资源失败', e);
+    }
+  }
+}

+ 144 - 0
lib/media/image_processor.dart

@@ -0,0 +1,144 @@
+import 'dart:io';
+import 'package:image/image.dart' as img;
+import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'media_exception.dart';
+
+/// 图片处理封装(压缩 / 裁剪 / 医疗脱敏)
+class ImageProcessor {
+  /// 默认压缩宽度(医疗场景推荐)
+  static const int defaultMaxWidth = 1080;
+  
+  /// 默认压缩质量(85% 平衡质量和体积)
+  static const int defaultQuality = 85;
+
+  /// 压缩图片
+  /// 
+  /// 医疗场景:降低上传体积、去EXIF、图片标准化
+  /// 
+  /// - [file] 原始图片文件
+  /// - [maxWidth] 最大宽度,默认 1080px
+  /// - [quality] 压缩质量(1-100),默认 85
+  /// 
+  /// 返回压缩后的文件(覆盖原文件)
+  static Future<File> compress(
+    File file, {
+    int maxWidth = defaultMaxWidth,
+    int quality = defaultQuality,
+  }) async {
+    try {
+      if (!await file.exists()) {
+        throw ImageProcessingException('图片文件不存在: ${file.path}');
+      }
+
+      final bytes = await file.readAsBytes();
+      if (bytes.isEmpty) {
+        throw ImageProcessingException('图片文件为空: ${file.path}');
+      }
+
+      final image = img.decodeImage(bytes);
+      if (image == null) {
+        throw ImageProcessingException('无法解析图片: ${file.path}');
+      }
+
+      // 如果图片宽度小于等于最大宽度,只进行质量压缩
+      File? resultFile;
+      if (image.width <= maxWidth) {
+        final compressed = img.encodeJpg(image, quality: quality);
+        resultFile = File(file.path)..writeAsBytesSync(compressed);
+      } else {
+        // 先缩放再压缩
+        final resized = img.copyResize(
+          image,
+          width: maxWidth,
+          maintainAspect: true,
+        );
+        final compressed = img.encodeJpg(resized, quality: quality);
+        resultFile = File(file.path)..writeAsBytesSync(compressed);
+      }
+
+      AppLogger.d('图片压缩完成: ${file.path}, 原始大小: ${bytes.length} bytes, 压缩后: ${resultFile.lengthSync()} bytes');
+      return resultFile;
+    } catch (e) {
+      if (e is ImageProcessingException) {
+        rethrow;
+      }
+      AppLogger.e('图片压缩失败: ${file.path}', e);
+      throw ImageProcessingException(
+        '图片压缩失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 获取图片信息
+  /// 
+  /// 返回图片的宽度、高度和文件大小
+  static Future<Map<String, dynamic>> getImageInfo(File file) async {
+    try {
+      if (!await file.exists()) {
+        throw ImageProcessingException('图片文件不存在: ${file.path}');
+      }
+
+      final bytes = await file.readAsBytes();
+      final image = img.decodeImage(bytes);
+      
+      if (image == null) {
+        throw ImageProcessingException('无法解析图片: ${file.path}');
+      }
+
+      return {
+        'width': image.width,
+        'height': image.height,
+        'size': bytes.length,
+        'format': image.format.toString(),
+      };
+    } catch (e) {
+      if (e is ImageProcessingException) {
+        rethrow;
+      }
+      AppLogger.e('获取图片信息失败: ${file.path}', e);
+      throw ImageProcessingException(
+        '获取图片信息失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 医疗脱敏处理(去除 EXIF 信息)
+  /// 
+  /// 移除图片中的 EXIF 元数据,保护患者隐私
+  /// 
+  /// - [file] 原始图片文件
+  /// 
+  /// 返回脱敏后的文件(覆盖原文件)
+  static Future<File> removeExif(File file) async {
+    try {
+      if (!await file.exists()) {
+        throw ImageProcessingException('图片文件不存在: ${file.path}');
+      }
+
+      final bytes = await file.readAsBytes();
+      final image = img.decodeImage(bytes);
+      
+      if (image == null) {
+        throw ImageProcessingException('无法解析图片: ${file.path}');
+      }
+
+      // 重新编码图片会移除 EXIF 信息
+      final cleaned = img.encodeJpg(image, quality: 100);
+      final cleanedFile = File(file.path)..writeAsBytesSync(cleaned);
+
+      AppLogger.d('图片脱敏完成: ${file.path}');
+      return cleanedFile;
+    } catch (e) {
+      if (e is ImageProcessingException) {
+        rethrow;
+      }
+      AppLogger.e('图片脱敏失败: ${file.path}', e);
+      throw ImageProcessingException(
+        '图片脱敏失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+}

+ 33 - 0
lib/media/media.dart

@@ -0,0 +1,33 @@
+/// 媒体服务模块
+/// 
+/// 提供统一的图片选择、拍照、压缩和处理功能
+/// 
+/// 使用示例:
+/// ```dart
+/// import 'package:sino_med_cloud/media/media.dart';
+/// 
+/// // 快捷方式:从相册选择并自动压缩
+/// final file = await MediaService.pickFromGallery();
+/// 
+/// // 快捷方式:使用系统相机拍照并自动压缩
+/// final photo = await MediaService.pickFromCamera();
+/// 
+/// // 专业相机(需要手动管理生命周期)
+/// final cameraService = CameraService();
+/// await cameraService.init();
+/// final photo = await cameraService.takePicture();
+/// cameraService.dispose();
+/// 
+/// // 图片处理
+/// final compressed = await MediaService.compressImage(photo!);
+/// final info = await MediaService.getImageInfo(compressed);
+/// final cleaned = await MediaService.removeExif(compressed);
+/// ```
+library media;
+
+export 'media_service.dart';
+export 'picker_service.dart';
+export 'camera_service.dart';
+export 'image_processor.dart';
+export 'media_exception.dart';
+

+ 37 - 0
lib/media/media_exception.dart

@@ -0,0 +1,37 @@
+/// 媒体服务异常类
+class MediaException implements Exception {
+  final String message;
+  final String? code;
+  final dynamic originalError;
+
+  MediaException(this.message, {this.code, this.originalError});
+
+  @override
+  String toString() {
+    if (code != null) {
+      return 'MediaException[$code]: $message';
+    }
+    return 'MediaException: $message';
+  }
+}
+
+/// 权限异常
+class MediaPermissionException extends MediaException {
+  MediaPermissionException(super.message, {super.code, super.originalError});
+}
+
+/// 文件操作异常
+class MediaFileException extends MediaException {
+  MediaFileException(super.message, {super.code, super.originalError});
+}
+
+/// 图片处理异常
+class ImageProcessingException extends MediaException {
+  ImageProcessingException(super.message, {super.code, super.originalError});
+}
+
+/// 相机异常(媒体服务模块)
+class MediaCameraException extends MediaException {
+  MediaCameraException(super.message, {super.code, super.originalError});
+}
+

+ 168 - 0
lib/media/media_service.dart

@@ -0,0 +1,168 @@
+import 'dart:io';
+import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'picker_service.dart';
+import 'image_processor.dart';
+import 'media_exception.dart';
+
+/// 统一媒体服务入口
+/// 
+/// 提供快捷的图片选择、拍照和处理功能
+/// 
+/// 使用示例:
+/// ```dart
+/// // 从相册选择并自动压缩
+/// final file = await MediaService.pickFromGallery();
+/// 
+/// // 使用系统相机拍照并自动压缩
+/// final photo = await MediaService.pickFromCamera();
+/// 
+/// // 使用专业相机拍照(需要手动管理生命周期)
+/// final cameraService = CameraService();
+/// await cameraService.init();
+/// final photo = await cameraService.takePicture();
+/// final compressed = await MediaService.compressImage(photo!);
+/// cameraService.dispose();
+/// ```
+class MediaService {
+  /// 使用系统相机拍照(快捷方式)
+  /// 
+  /// 自动压缩图片,适合快速拍照场景
+  /// 
+  /// - [imageQuality] 图片质量(1-100),默认 90
+  /// - [maxWidth] 压缩后的最大宽度,默认 1080px
+  /// - [compressQuality] 压缩质量(1-100),默认 85
+  /// 
+  /// 返回压缩后的照片文件,如果用户取消或失败返回 null
+  /// 
+  /// 抛出 [MediaException] 如果操作失败
+  static Future<File?> pickFromCamera({
+    int imageQuality = PickerService.defaultImageQuality,
+    int maxWidth = ImageProcessor.defaultMaxWidth,
+    int compressQuality = ImageProcessor.defaultQuality,
+  }) async {
+    try {
+      AppLogger.d('开始使用系统相机拍照...');
+      final file = await PickerService.pickFromCamera(
+        imageQuality: imageQuality,
+      );
+      
+      if (file == null) {
+        return null;
+      }
+
+      AppLogger.d('开始压缩图片...');
+      final compressed = await ImageProcessor.compress(
+        file,
+        maxWidth: maxWidth,
+        quality: compressQuality,
+      );
+
+      AppLogger.d('拍照并压缩完成: ${compressed.path}');
+      return compressed;
+    } on MediaException {
+      rethrow;
+    } catch (e) {
+      AppLogger.e('拍照并压缩失败', e);
+      throw MediaException(
+        '拍照并压缩失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 从相册选择图片(快捷方式)
+  /// 
+  /// 自动压缩图片,适合快速选择场景
+  /// 
+  /// - [imageQuality] 图片质量(1-100),默认 90
+  /// - [maxWidth] 压缩后的最大宽度,默认 1080px
+  /// - [compressQuality] 压缩质量(1-100),默认 85
+  /// 
+  /// 返回压缩后的图片文件,如果用户取消或失败返回 null
+  /// 
+  /// 抛出 [MediaException] 如果操作失败
+  static Future<File?> pickFromGallery({
+    int imageQuality = PickerService.defaultImageQuality,
+    int maxWidth = ImageProcessor.defaultMaxWidth,
+    int compressQuality = ImageProcessor.defaultQuality,
+  }) async {
+    try {
+      AppLogger.d('开始从相册选择图片...');
+      final file = await PickerService.pickFromGallery(
+        imageQuality: imageQuality,
+      );
+      
+      if (file == null) {
+        return null;
+      }
+
+      AppLogger.d('开始压缩图片...');
+      final compressed = await ImageProcessor.compress(
+        file,
+        maxWidth: maxWidth,
+        quality: compressQuality,
+      );
+
+      AppLogger.d('选择并压缩完成: ${compressed.path}');
+      return compressed;
+    } on MediaException {
+      rethrow;
+    } catch (e) {
+      AppLogger.e('选择并压缩失败', e);
+      throw MediaException(
+        '选择并压缩失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 压缩图片
+  /// 
+  /// - [file] 原始图片文件
+  /// - [maxWidth] 最大宽度,默认 1080px
+  /// - [quality] 压缩质量(1-100),默认 85
+  /// 
+  /// 返回压缩后的文件(覆盖原文件)
+  /// 
+  /// 抛出 [ImageProcessingException] 如果压缩失败
+  static Future<File> compressImage(
+    File file, {
+    int maxWidth = ImageProcessor.defaultMaxWidth,
+    int quality = ImageProcessor.defaultQuality,
+  }) async {
+    return await ImageProcessor.compress(
+      file,
+      maxWidth: maxWidth,
+      quality: quality,
+    );
+  }
+
+  /// 获取图片信息
+  /// 
+  /// 返回图片的宽度、高度和文件大小
+  /// 
+  /// 抛出 [ImageProcessingException] 如果获取失败
+  static Future<Map<String, dynamic>> getImageInfo(File file) async {
+    return await ImageProcessor.getImageInfo(file);
+  }
+
+  /// 医疗脱敏处理(去除 EXIF 信息)
+  /// 
+  /// 移除图片中的 EXIF 元数据,保护患者隐私
+  /// 
+  /// 返回脱敏后的文件(覆盖原文件)
+  /// 
+  /// 抛出 [ImageProcessingException] 如果处理失败
+  static Future<File> removeExif(File file) async {
+    return await ImageProcessor.removeExif(file);
+  }
+
+  /// 从相册选择视频
+  /// 
+  /// 返回选择的视频文件,如果用户取消选择返回 null
+  /// 
+  /// 抛出 [MediaException] 如果选择失败
+  static Future<File?> pickVideoFromGallery() async {
+    return await PickerService.pickVideoFromGallery();
+  }
+}

+ 168 - 0
lib/media/picker_service.dart

@@ -0,0 +1,168 @@
+import 'dart:io';
+import 'package:image_picker/image_picker.dart';
+import 'package:sino_med_cloud/core/utils/logger.dart';
+import 'media_exception.dart';
+
+/// 相册和快速拍照服务
+/// 
+/// 适合普通拍照、表单上传、医疗资料拍照(非实时)
+/// 
+/// 使用示例:
+/// ```dart
+/// // 从相册选择
+/// final file = await PickerService.pickFromGallery();
+/// 
+/// // 使用系统相机拍照
+/// final photo = await PickerService.pickFromCamera();
+/// ```
+class PickerService {
+  static final ImagePicker _picker = ImagePicker();
+
+  /// 默认图片质量(1-100)
+  static const int defaultImageQuality = 90;
+
+  /// 从相册选择图片
+  /// 
+  /// - [imageQuality] 图片质量(1-100),默认 90
+  /// - [maxWidth] 最大宽度,null 表示不限制
+  /// - [maxHeight] 最大高度,null 表示不限制
+  /// 
+  /// 返回选择的图片文件,如果用户取消选择返回 null
+  /// 
+  /// 抛出 [MediaException] 如果选择失败
+  static Future<File?> pickFromGallery({
+    int imageQuality = defaultImageQuality,
+    double? maxWidth,
+    double? maxHeight,
+  }) async {
+    try {
+      AppLogger.d('开始从相册选择图片...');
+      final XFile? file = await _picker.pickImage(
+        source: ImageSource.gallery,
+        imageQuality: imageQuality,
+        maxWidth: maxWidth,
+        maxHeight: maxHeight,
+      );
+
+      if (file == null) {
+        AppLogger.d('用户取消了相册选择');
+        return null;
+      }
+
+      final imageFile = File(file.path);
+      if (!await imageFile.exists()) {
+        throw MediaFileException('选择的图片文件不存在: ${file.path}');
+      }
+
+      AppLogger.d('相册选择成功: ${file.path}');
+      return imageFile;
+    } on MediaException {
+      rethrow;
+    } catch (e) {
+      AppLogger.e('相册选择失败', e);
+      if (e.toString().contains('permission') || e.toString().contains('权限')) {
+        throw MediaPermissionException(
+          '相册访问权限被拒绝,请在设置中开启相册权限',
+          originalError: e,
+        );
+      }
+      throw MediaException(
+        '相册选择失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 使用系统相机拍照(轻量级)
+  /// 
+  /// - [imageQuality] 图片质量(1-100),默认 90
+  /// - [maxWidth] 最大宽度,null 表示不限制
+  /// - [maxHeight] 最大高度,null 表示不限制
+  /// 
+  /// 返回拍摄的照片文件,如果用户取消拍照返回 null
+  /// 
+  /// 抛出 [MediaException] 如果拍照失败
+  static Future<File?> pickFromCamera({
+    int imageQuality = defaultImageQuality,
+    double? maxWidth,
+    double? maxHeight,
+  }) async {
+    try {
+      AppLogger.d('开始使用系统相机拍照...');
+      final XFile? file = await _picker.pickImage(
+        source: ImageSource.camera,
+        imageQuality: imageQuality,
+        maxWidth: maxWidth,
+        maxHeight: maxHeight,
+      );
+
+      if (file == null) {
+        AppLogger.d('用户取消了拍照');
+        return null;
+      }
+
+      final photoFile = File(file.path);
+      if (!await photoFile.exists()) {
+        throw MediaFileException('拍摄的照片文件不存在: ${file.path}');
+      }
+
+      AppLogger.d('拍照成功: ${file.path}');
+      return photoFile;
+    } on MediaException {
+      rethrow;
+    } catch (e) {
+      AppLogger.e('相机拍照失败', e);
+      if (e.toString().contains('permission') || e.toString().contains('权限')) {
+        throw MediaPermissionException(
+          '相机权限被拒绝,请在设置中开启相机权限',
+          originalError: e,
+        );
+      }
+      throw MediaException(
+        '相机拍照失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+
+  /// 从相册选择视频
+  /// 
+  /// 返回选择的视频文件,如果用户取消选择返回 null
+  /// 
+  /// 抛出 [MediaException] 如果选择失败
+  static Future<File?> pickVideoFromGallery() async {
+    try {
+      AppLogger.d('开始从相册选择视频...');
+      final XFile? file = await _picker.pickVideo(
+        source: ImageSource.gallery,
+      );
+
+      if (file == null) {
+        AppLogger.d('用户取消了视频选择');
+        return null;
+      }
+
+      final videoFile = File(file.path);
+      if (!await videoFile.exists()) {
+        throw MediaFileException('选择的视频文件不存在: ${file.path}');
+      }
+
+      AppLogger.d('视频选择成功: ${file.path}');
+      return videoFile;
+    } on MediaException {
+      rethrow;
+    } catch (e) {
+      AppLogger.e('视频选择失败', e);
+      if (e.toString().contains('permission') || e.toString().contains('权限')) {
+        throw MediaPermissionException(
+          '相册访问权限被拒绝,请在设置中开启相册权限',
+          originalError: e,
+        );
+      }
+      throw MediaException(
+        '视频选择失败: ${e.toString()}',
+        originalError: e,
+      );
+    }
+  }
+}

+ 4 - 0
linux/flutter/generated_plugin_registrant.cc

@@ -6,6 +6,10 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <file_selector_linux/file_selector_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
+  g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
+  file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
 }

+ 1 - 0
linux/flutter/generated_plugins.cmake

@@ -3,6 +3,7 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  file_selector_linux
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST

+ 2 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -7,6 +7,7 @@ import Foundation
 
 import connectivity_plus
 import device_info_plus
+import file_selector_macos
 import package_info_plus
 import path_provider_foundation
 import shared_preferences_foundation
@@ -15,6 +16,7 @@ import sqflite_darwin
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
+  FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
   FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

+ 176 - 0
pubspec.lock

@@ -17,6 +17,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "8.4.1"
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.7"
   args:
     dependency: transitive
     description:
@@ -89,6 +97,46 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "8.12.1"
+  camera:
+    dependency: "direct main"
+    description:
+      name: camera
+      sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.11.3"
+  camera_android_camerax:
+    dependency: transitive
+    description:
+      name: camera_android_camerax
+      sha256: "474d8355961658d43f1c976e2fa1ca715505bea1adbd56df34c581aaa70ec41f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.26+2"
+  camera_avfoundation:
+    dependency: transitive
+    description:
+      name: camera_avfoundation
+      sha256: "087a9fadef20325cb246b4c13344a3ce8e408acfc3e0c665ebff0ec3144d7163"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.22+8"
+  camera_platform_interface:
+    dependency: transitive
+    description:
+      name: camera_platform_interface
+      sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.12.0"
+  camera_web:
+    dependency: transitive
+    description:
+      name: camera_web
+      sha256: "3bc7bb1657a0f29c34116453c5d5e528c23efcf5e75aac0a3387cf108040bf65"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.5+2"
   characters:
     dependency: transitive
     description:
@@ -169,6 +217,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.15.0"
+  cross_file:
+    dependency: transitive
+    description:
+      name: cross_file
+      sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.5+1"
   crypto:
     dependency: "direct main"
     description:
@@ -257,6 +313,38 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "7.0.1"
+  file_selector_linux:
+    dependency: transitive
+    description:
+      name: file_selector_linux
+      sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.4"
+  file_selector_macos:
+    dependency: transitive
+    description:
+      name: file_selector_macos
+      sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.5"
+  file_selector_platform_interface:
+    dependency: transitive
+    description:
+      name: file_selector_platform_interface
+      sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.7.0"
+  file_selector_windows:
+    dependency: transitive
+    description:
+      name: file_selector_windows
+      sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.3+5"
   fixnum:
     dependency: transitive
     description:
@@ -283,6 +371,14 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_plugin_android_lifecycle:
+    dependency: transitive
+    description:
+      name: flutter_plugin_android_lifecycle
+      sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.33"
   flutter_riverpod:
     dependency: "direct main"
     description:
@@ -381,6 +477,78 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.1.2"
+  image:
+    dependency: "direct main"
+    description:
+      name: image
+      sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.7.2"
+  image_picker:
+    dependency: "direct main"
+    description:
+      name: image_picker
+      sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  image_picker_android:
+    dependency: transitive
+    description:
+      name: image_picker_android
+      sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.8.13+10"
+  image_picker_for_web:
+    dependency: transitive
+    description:
+      name: image_picker_for_web
+      sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
+  image_picker_ios:
+    dependency: transitive
+    description:
+      name: image_picker_ios
+      sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.8.13+3"
+  image_picker_linux:
+    dependency: transitive
+    description:
+      name: image_picker_linux
+      sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.2"
+  image_picker_macos:
+    dependency: transitive
+    description:
+      name: image_picker_macos
+      sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.2+1"
+  image_picker_platform_interface:
+    dependency: transitive
+    description:
+      name: image_picker_platform_interface
+      sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.1"
+  image_picker_windows:
+    dependency: transitive
+    description:
+      name: image_picker_windows
+      sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.2"
   intl:
     dependency: "direct main"
     description:
@@ -685,6 +853,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.5.2"
+  posix:
+    dependency: transitive
+    description:
+      name: posix
+      sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.3"
   pretty_dio_logger:
     dependency: "direct main"
     description:

+ 3 - 0
pubspec.yaml

@@ -57,6 +57,9 @@ dependencies:
   fluttertoast: ^9.0.0
   json_annotation: ^4.9.0
   json_serializable: ^6.11.2
+  image_picker: ^1.2.1
+  camera: ^0.11.3
+  image: ^4.7.2
 
 dev_dependencies:
   flutter_test:

+ 3 - 0
windows/flutter/generated_plugin_registrant.cc

@@ -7,11 +7,14 @@
 #include "generated_plugin_registrant.h"
 
 #include <connectivity_plus/connectivity_plus_windows_plugin.h>
+#include <file_selector_windows/file_selector_windows.h>
 #include <permission_handler_windows/permission_handler_windows_plugin.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   ConnectivityPlusWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
+  FileSelectorWindowsRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("FileSelectorWindows"));
   PermissionHandlerWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
 }

+ 1 - 0
windows/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   connectivity_plus
+  file_selector_windows
   permission_handler_windows
 )