diff --git a/assets/svgs/image_icon.svg b/assets/svgs/image_icon.svg new file mode 100644 index 00000000..6016457c --- /dev/null +++ b/assets/svgs/image_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/svgs/send_icon.svg b/assets/svgs/send_icon.svg new file mode 100644 index 00000000..add1dc20 --- /dev/null +++ b/assets/svgs/send_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/core/storage/impl/user_storage_impl.dart b/lib/core/storage/impl/user_storage_impl.dart index d1ffcb44..f77b8314 100644 --- a/lib/core/storage/impl/user_storage_impl.dart +++ b/lib/core/storage/impl/user_storage_impl.dart @@ -32,6 +32,8 @@ class UserStorageImpl implements UserStorage { @override Future deleteUser() async { await _prefs.remove(_userKey); + //返回登陆页面 + } @override diff --git a/lib/features/ai/data/models/chat_message.dart b/lib/features/ai/data/models/chat_message.dart new file mode 100644 index 00000000..160f89b9 --- /dev/null +++ b/lib/features/ai/data/models/chat_message.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +class ChatMessage { + final String? text; + final String? thinkingText; + final File? image; + final String? imageBase64; + final bool isUserMessage; + + ChatMessage({ + this.text, + this.thinkingText, + this.image, + this.imageBase64, + required this.isUserMessage, + }); +} diff --git a/lib/features/ai/data/models/session.dart b/lib/features/ai/data/models/session.dart new file mode 100644 index 00000000..717f763b --- /dev/null +++ b/lib/features/ai/data/models/session.dart @@ -0,0 +1,13 @@ +class Session { + final String sessId; + final String title; + + Session({required this.sessId, required this.title}); + + factory Session.fromJson(Map json) { + return Session( + sessId: json['sessId'], + title: json['title'], + ); + } +} diff --git a/lib/features/ai/presentation/bloc/ai_cubit.dart b/lib/features/ai/presentation/bloc/ai_cubit.dart new file mode 100644 index 00000000..bff6cd00 --- /dev/null +++ b/lib/features/ai/presentation/bloc/ai_cubit.dart @@ -0,0 +1,178 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'ai_state.dart'; +import '../../data/models/chat_message.dart'; +import '../../data/models/session.dart'; + +class AiCubit extends Cubit { + AiCubit() : super(const AiState()) { + createNewSession(); + fetchSessions(); + } + + final Dio _dio = Dio(); + final ImagePicker _picker = ImagePicker(); + static const String BASE_URL = 'http://1.95.137.212:30009'; + final String _userId = '1'; + + Future fetchSessions() async { + try { + final response = await _dio.get('$BASE_URL/sessions', queryParameters: {'userId': _userId}); + if (response.statusCode == 200 && response.data['code'] == 0) { + final sessionsData = response.data['sessions'] as List; + emit(state.copyWith(sessions: sessionsData.map((data) => Session.fromJson(data)).toList())); + } + } catch (e) { + emit(state.copyWith(error: "Failed to fetch sessions: $e")); + } + } + + void createNewSession() { + emit(AiState( + sessionId: DateTime.now().millisecondsSinceEpoch.toString(), + messages: [ChatMessage(text: "Welcome to the AI chat!", isUserMessage: false)], + status: AiStatus.initial, + selectedImageFile: null, // Explicitly set to null + sessions: state.sessions, // Preserve existing sessions + )); + } + + Future loadSessionHistory(String sessionId) async { + emit(state.copyWith(status: AiStatus.loading, messages: [], sessionId: sessionId)); + + try { + final response = await _dio.get('$BASE_URL/history', queryParameters: {'sessionId': sessionId}); + if (response.statusCode == 200 && response.data['code'] == 0) { + final messagesData = response.data['messages'] as List; + final historyMessages = messagesData.map((data) { + return ChatMessage( + text: data['content'], + thinkingText: data['role'] == 'assistant' ? data['thinking'] : null, + imageBase64: data['role'] == 'user' ? data['image_base64'] : null, + isUserMessage: data['role'] == 'user', + ); + }).toList(); + emit(state.copyWith(status: AiStatus.success, messages: historyMessages.reversed.toList())); + } + } catch (e) { + emit(state.copyWith(status: AiStatus.failure, error: "Failed to load session history: $e")); + } + } + + Future analyze({String? prompt, File? image}) async { + if (state.status == AiStatus.loading) return; + + String? imageBase64; + if (image != null) { + final bytes = await image.readAsBytes(); + imageBase64 = 'data:image/png;base64,${base64Encode(bytes)}'; + } + + final userMessage = ChatMessage(text: prompt, image: image, isUserMessage: true); + final aiPlaceholder = ChatMessage(text: '', thinkingText: '', isUserMessage: false); + + // Create a new state to ensure selectedImageFile is set to null + final newState = AiState( + status: AiStatus.loading, + messages: [aiPlaceholder, userMessage, ...state.messages], + selectedImageFile: null, // Clear the selected image + sessions: state.sessions, + sessionId: state.sessionId, + error: state.error, + ); + emit(newState); + + try { + final response = await _dio.post( + '$BASE_URL/analyze', + data: { + 'userId': _userId, + 'sessionId': state.sessionId, + 'userPrompt': prompt, + 'imageBase64': imageBase64, + }, + options: Options(responseType: ResponseType.stream), + ); + + String currentPhase = 'thinking'; + StringBuffer thinkingBuffer = StringBuffer(); + StringBuffer answerBuffer = StringBuffer(); + + final streamSubscription = response.data.stream.listen( + (chunk) { + final decodedChunk = utf8.decode(chunk); + final events = decodedChunk.split('\n\n').where((s) => s.isNotEmpty); + + for (final event in events) { + if (event.startsWith('data: ')) { + final dataString = event.substring(6); + if (dataString == '[DONE]') continue; + try { + final data = jsonDecode(dataString); + final type = data['type']; + final content = data['content'] as String; + + if (type == 'phase') { + currentPhase = content; + } else if (type == 'thinking' && currentPhase == 'thinking') { + thinkingBuffer.write(content); + } else if (type == 'answer' && currentPhase == 'answer') { + answerBuffer.write(content); + } + + final updatedMessages = List.from(state.messages); + updatedMessages[0] = ChatMessage( + thinkingText: thinkingBuffer.toString(), + text: answerBuffer.toString(), + isUserMessage: false, + ); + emit(state.copyWith(messages: updatedMessages)); + + } catch (e) { + print("Error parsing stream data: $e"); + } + } + } + }, + onDone: () { + emit(state.copyWith(status: AiStatus.success)); + fetchSessions(); + }, + onError: (error) { + final updatedMessages = List.from(state.messages); + updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false); + emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages)); + }, + ); + } catch (e) { + final updatedMessages = List.from(state.messages); + updatedMessages[0] = ChatMessage(text: "Failed to connect to the server: $e", isUserMessage: false); + emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages)); + } + } + + void selectImage(ImageSource source) async { + final pickedFile = await _picker.pickImage(source: source); + if (pickedFile != null) { + emit(state.copyWith(selectedImageFile: File(pickedFile.path))); + } + } + + void deselectImage() { + // Manually create a new state to ensure selectedImageFile is set to null, + // bypassing the limitation of the existing copyWith method. + emit(AiState( + status: state.status, + messages: state.messages, + sessions: state.sessions, + sessionId: state.sessionId, + error: state.error, + selectedImageFile: null, // This will now correctly clear the file. + )); + } +} diff --git a/lib/features/ai/presentation/bloc/ai_state.dart b/lib/features/ai/presentation/bloc/ai_state.dart new file mode 100644 index 00000000..8ae597e7 --- /dev/null +++ b/lib/features/ai/presentation/bloc/ai_state.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:equatable/equatable.dart'; + +import '../../data/models/chat_message.dart'; +import '../../data/models/session.dart'; + +enum AiStatus { initial, loading, success, failure } + +class AiState extends Equatable { + const AiState({ + this.status = AiStatus.initial, + this.messages = const [], + this.sessions = const [], + this.selectedImageFile, + this.sessionId, + this.error = '', + }); + + final AiStatus status; + final List messages; + final List sessions; + final File? selectedImageFile; + final String? sessionId; + final String error; + + AiState copyWith({ + AiStatus? status, + List? messages, + List? sessions, + File? selectedImageFile, + String? sessionId, + String? error, + }) { + return AiState( + status: status ?? this.status, + messages: messages ?? this.messages, + sessions: sessions ?? this.sessions, + selectedImageFile: selectedImageFile ?? this.selectedImageFile, + sessionId: sessionId ?? this.sessionId, + error: error ?? this.error, + ); + } + + @override + List get props => [status, messages, sessions, selectedImageFile, sessionId, error]; +} diff --git a/lib/features/ai/presentation/pages/ai_page.dart b/lib/features/ai/presentation/pages/ai_page.dart index f5b84d5d..6452d19d 100644 --- a/lib/features/ai/presentation/pages/ai_page.dart +++ b/lib/features/ai/presentation/pages/ai_page.dart @@ -1,10 +1,414 @@ + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../bloc/ai_cubit.dart'; +import '../bloc/ai_state.dart'; +import '../../data/models/chat_message.dart'; +import '../../data/models/session.dart'; + +// --- MAIN WIDGET --- class AiPage extends StatelessWidget { const AiPage({super.key}); @override Widget build(BuildContext context) { - return Scaffold(body: Center(child: Text('Welcome AI page'))); + return BlocProvider( + create: (_) => AiCubit(), + child: const AiView(), + ); + } +} + +class AiView extends StatefulWidget { + const AiView({super.key}); + + @override + State createState() => _AiViewState(); +} + +class _AiViewState extends State { + final TextEditingController _textController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _textController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('AI'), + actions: [ + PopupMenuButton( + onSelected: (value) { + if (value == 'new_session') { + context.read().createNewSession(); + } else if (value == 'history') { + _showHistoryDialog(context); + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem(value: 'new_session', child: Text('New Session')), + const PopupMenuItem(value: 'history', child: Text('History')), + ], + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + // Scroll to bottom when new messages are added + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.animateTo(0.0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + }); + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8.0), + controller: _scrollController, + reverse: true, + itemCount: state.messages.length, + itemBuilder: (context, index) => _buildMessageBubble(context, state.messages[index]), + ); + }, + ), + ), + _buildInputArea(context), + ], + ), + ), + ); + } + + Widget _buildMessageBubble(BuildContext context, ChatMessage message) { + final isUserMessage = message.isUserMessage; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0), + child: Row( + mainAxisAlignment: isUserMessage ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!isUserMessage) + const CircleAvatar( + backgroundImage: AssetImage('assets/images/app_logo_black.png'), + radius: 16, + ), + const SizedBox(width: 10), + Flexible( + child: isUserMessage ? _buildUserMessageContent(context, message) : _buildAiMessageContent(context, message), + ), + if (isUserMessage) + const SizedBox(width: 10), // <-- This was the missing part + if (isUserMessage) + const CircleAvatar(child: Icon(Icons.person, size: 20), radius: 16), + ], + ), + ); + } + + Widget _buildUserMessageContent(BuildContext context, ChatMessage message) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0), + decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(18)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (message.image != null) + GestureDetector( + onTap: () => _showImagePreview(context, imageFile: message.image!), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Image.file(message.image!, width: 200, height: 200, fit: BoxFit.cover), + ), + ) + else if (message.imageBase64 != null) + ClipRRect( + borderRadius: BorderRadius.circular(12.0), + child: Image.memory(base64Decode(message.imageBase64!.split(',').last), width: 200, height: 200, fit: BoxFit.cover), + ), + if (message.text != null && message.text!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text(message.text!, style: const TextStyle(color: Colors.white, fontSize: 16)), + ), + ], + )); + } + + Widget _buildAiMessageContent(BuildContext context, ChatMessage message) { + final state = context.watch().state; + final hasThinking = message.thinkingText != null && message.thinkingText!.isNotEmpty; + final hasAnswer = message.text != null && message.text!.isNotEmpty; + final showThinkingBlock = state.status == AiStatus.loading || hasThinking; + + if (!showThinkingBlock && !hasAnswer) return const SizedBox.shrink(); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(18), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showThinkingBlock) + _buildSection( + context, + icon: Icons.psychology_outlined, + title: "深度思考", + content: message.thinkingText, + iconColor: Colors.blue.shade700, + showSpinner: state.status == AiStatus.loading && !hasThinking, + ), + if (showThinkingBlock && hasAnswer) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Divider(height: 1, color: Colors.grey.shade300), + ), + if (hasAnswer) + _buildSection( + context, + icon: Icons.lightbulb_outline, + title: "最终结论", + content: message.text!, + iconColor: Colors.green.shade700, + ), + ], + ), + ); + } + + Widget _buildSection(BuildContext context, {required IconData icon, required String title, String? content, Color? iconColor, bool showSpinner = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Icon(icon, size: 20, color: iconColor), + const SizedBox(width: 8), + Text(title, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: iconColor)), + if (showSpinner) ...[ + const SizedBox(width: 8), + SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2.0, color: iconColor)), + ] + ]), + if (content != null && content.isNotEmpty) ...[ + const SizedBox(height: 8), + Text(content, style: const TextStyle(fontSize: 16, color: Colors.black87, height: 1.4)), + ], + ], + ), + ); + } + + Widget _buildInputArea(BuildContext context) { + final state = context.watch().state; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (state.selectedImageFile != null) _buildImagePreviewThumbnail(context), + Container( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: SvgPicture.asset('assets/svgs/image_icon.svg', width: 24, height: 24), + onPressed: state.status == AiStatus.loading ? null : () => _handleImageSelection(context), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: _textController, + enabled: state.status != AiStatus.loading, + decoration: InputDecoration( + hintText: state.status == AiStatus.loading ? 'AI is thinking...' : 'Enter a message...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30.0), + borderSide: const BorderSide(width: 0, style: BorderStyle.none), + ), + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceVariant, + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + onSubmitted: state.status == AiStatus.loading ? null : (text) => _handleSendPressed(context), + ), + ), + const SizedBox(width: 8), + IconButton( + icon: SvgPicture.asset('assets/svgs/send_icon.svg', width: 24, height: 24), + onPressed: state.status == AiStatus.loading ? null : () => _handleSendPressed(context), + ), + ], + ), + ), + ], + ); + } + + Widget _buildImagePreviewThumbnail(BuildContext context) { + final state = context.read().state; + return Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 8.0), + child: Align( + alignment: Alignment.centerLeft, + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: Image.file(state.selectedImageFile!, width: 72, height: 72, fit: BoxFit.cover), + ), + Positioned( + top: -12, + right: -12, + child: IconButton( + icon: const CircleAvatar( + backgroundColor: Colors.black54, + radius: 12, + child: Icon(Icons.close, color: Colors.white, size: 16), + ), + onPressed: () => context.read().deselectImage(), + ), + ), + ], + ), + ), + ); + } + + void _handleSendPressed(BuildContext context) { + final text = _textController.text; + final selectedImageFile = context.read().state.selectedImageFile; + if (text.isEmpty && selectedImageFile == null) return; + + context.read().analyze(prompt: text, image: selectedImageFile); + + _textController.clear(); + } + + void _handleImageSelection(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext _) { + return SafeArea( + child: Wrap( + children: [ + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text('Gallery'), + onTap: () { + Navigator.of(context).pop(); + context.read().selectImage(ImageSource.gallery); + }, + ), + ListTile( + leading: const Icon(Icons.photo_camera), + title: const Text('Camera'), + onTap: () { + Navigator.of(context).pop(); + context.read().selectImage(ImageSource.camera); + }, + ), + ], + ), + ); + }, + ); + } + + + void _showImagePreview(BuildContext context, {File? imageFile, String? imageBase64}) { + if (imageFile == null && imageBase64 == null) return; + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ImagePreviewPage(imageFile: imageFile, imageBase64: imageBase64), + ), + ); + } + + void _showHistoryDialog(BuildContext context) { + final cubit = context.read(); + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: const Text('History'), + content: SizedBox( + width: double.maxFinite, + child: BlocBuilder( + bloc: cubit, + builder: (context, state) { + return ListView.builder( + shrinkWrap: true, + itemCount: state.sessions.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(state.sessions[index].title), + onTap: () { + Navigator.of(dialogContext).pop(); + cubit.loadSessionHistory(state.sessions[index].sessId); + }, + ); + }, + ); + }, + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Close')), + ], + ); + }, + ); + } +} + +// --- IMAGE PREVIEW PAGE --- + +class ImagePreviewPage extends StatelessWidget { + final File? imageFile; + final String? imageBase64; + + const ImagePreviewPage({super.key, this.imageFile, this.imageBase64}); + + @override + Widget build(BuildContext context) { + ImageProvider? imageProvider; + if (imageFile != null) { + imageProvider = FileImage(imageFile!); + } else if (imageBase64 != null) { + imageProvider = MemoryImage(base64Decode(imageBase64!.split(',').last)); + } + + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.white), + ), + body: Center( + child: InteractiveViewer( + child: imageProvider != null ? Image(image: imageProvider) : const Text('No Image', style: TextStyle(color: Colors.white)), + ), + ), + ); } } diff --git a/lib/features/my/presentation/pages/my_page.dart b/lib/features/my/presentation/pages/my_page.dart index 8f83ed7e..1ec775b5 100644 --- a/lib/features/my/presentation/pages/my_page.dart +++ b/lib/features/my/presentation/pages/my_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/storage/user_storage.dart'; import '../../../web_view/web_view_page.dart'; -import '../web_view/web_view_page.dart'; // 相对路径 @@ -33,7 +35,7 @@ class MyPage extends StatelessWidget { SizedBox(height: 16), // 退出登录按钮 - _buildLogoutButton(), + _buildLogoutButton(context), ], ), ), @@ -172,9 +174,12 @@ class MyPage extends StatelessWidget { ); } - Widget _buildLogoutButton() { + Widget _buildLogoutButton(BuildContext context) { return ElevatedButton( - onPressed: () {}, + onPressed: () async { + await GetIt.I().deleteUser(); + context.go('/auth/login'); + }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.red, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e13d69c2..e24fb1eb 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include 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); g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e5d24820..1394e659 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux flutter_webrtc isar_community_flutter_libs sentry_flutter diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 201371fe..0118cc03 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import device_info_plus +import file_selector_macos import flutter_webrtc import isar_community_flutter_libs import package_info_plus @@ -16,6 +17,7 @@ import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 0d16fe0b..f1d66c55 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -184,6 +184,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -288,6 +296,38 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -325,6 +365,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.33" flutter_svg: dependency: "direct main" description: @@ -447,6 +495,70 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.7.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.13+13" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.2" intl: dependency: "direct main" description: @@ -1110,4 +1222,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.0 <4.0.0" - flutter: ">=3.35.0" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2b9161c9..16a57052 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: # ===== 图片 ===== flutter_svg: ^2.2.3 + image_picker: ^1.1.2 # ===== 屏幕适配,高刷等 ===== # flutter_displaymode: ^0.7.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4c7a7825..55e5b696 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0683793d..93798c8d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows flutter_webrtc isar_community_flutter_libs sentry_flutter