AI界面的开发
This commit is contained in:
1
assets/svgs/image_icon.svg
Normal file
1
assets/svgs/image_icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image-icon lucide-image"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>
|
||||||
|
After Width: | Height: | Size: 374 B |
1
assets/svgs/send_icon.svg
Normal file
1
assets/svgs/send_icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send-icon lucide-send"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></svg>
|
||||||
|
After Width: | Height: | Size: 411 B |
@@ -32,6 +32,8 @@ class UserStorageImpl implements UserStorage {
|
|||||||
@override
|
@override
|
||||||
Future<void> deleteUser() async {
|
Future<void> deleteUser() async {
|
||||||
await _prefs.remove(_userKey);
|
await _prefs.remove(_userKey);
|
||||||
|
//返回登陆页面
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
17
lib/features/ai/data/models/chat_message.dart
Normal file
17
lib/features/ai/data/models/chat_message.dart
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
13
lib/features/ai/data/models/session.dart
Normal file
13
lib/features/ai/data/models/session.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class Session {
|
||||||
|
final String sessId;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
Session({required this.sessId, required this.title});
|
||||||
|
|
||||||
|
factory Session.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Session(
|
||||||
|
sessId: json['sessId'],
|
||||||
|
title: json['title'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
lib/features/ai/presentation/bloc/ai_cubit.dart
Normal file
178
lib/features/ai/presentation/bloc/ai_cubit.dart
Normal file
@@ -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<AiState> {
|
||||||
|
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<void> 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<void> 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<void> 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<ChatMessage>.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<ChatMessage>.from(state.messages);
|
||||||
|
updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false);
|
||||||
|
emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
final updatedMessages = List<ChatMessage>.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.
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
47
lib/features/ai/presentation/bloc/ai_state.dart
Normal file
47
lib/features/ai/presentation/bloc/ai_state.dart
Normal file
@@ -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 <ChatMessage>[],
|
||||||
|
this.sessions = const <Session>[],
|
||||||
|
this.selectedImageFile,
|
||||||
|
this.sessionId,
|
||||||
|
this.error = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
final AiStatus status;
|
||||||
|
final List<ChatMessage> messages;
|
||||||
|
final List<Session> sessions;
|
||||||
|
final File? selectedImageFile;
|
||||||
|
final String? sessionId;
|
||||||
|
final String error;
|
||||||
|
|
||||||
|
AiState copyWith({
|
||||||
|
AiStatus? status,
|
||||||
|
List<ChatMessage>? messages,
|
||||||
|
List<Session>? 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<Object?> get props => [status, messages, sessions, selectedImageFile, sessionId, error];
|
||||||
|
}
|
||||||
@@ -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/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 {
|
class AiPage extends StatelessWidget {
|
||||||
const AiPage({super.key});
|
const AiPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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<AiView> createState() => _AiViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AiViewState extends State<AiView> {
|
||||||
|
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<String>(
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'new_session') {
|
||||||
|
context.read<AiCubit>().createNewSession();
|
||||||
|
} else if (value == 'history') {
|
||||||
|
_showHistoryDialog(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||||
|
const PopupMenuItem<String>(value: 'new_session', child: Text('New Session')),
|
||||||
|
const PopupMenuItem<String>(value: 'history', child: Text('History')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<AiCubit, AiState>(
|
||||||
|
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<AiCubit>().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<AiCubit>().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<AiCubit>().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<AiCubit>().deselectImage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSendPressed(BuildContext context) {
|
||||||
|
final text = _textController.text;
|
||||||
|
final selectedImageFile = context.read<AiCubit>().state.selectedImageFile;
|
||||||
|
if (text.isEmpty && selectedImageFile == null) return;
|
||||||
|
|
||||||
|
context.read<AiCubit>().analyze(prompt: text, image: selectedImageFile);
|
||||||
|
|
||||||
|
_textController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleImageSelection(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext _) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Wrap(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library),
|
||||||
|
title: const Text('Gallery'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.read<AiCubit>().selectImage(ImageSource.gallery);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_camera),
|
||||||
|
title: const Text('Camera'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.read<AiCubit>().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<AiCubit>();
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('History'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: BlocBuilder<AiCubit, AiState>(
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.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';
|
||||||
import '../web_view/web_view_page.dart'; // 相对路径
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ class MyPage extends StatelessWidget {
|
|||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
|
||||||
// 退出登录按钮
|
// 退出登录按钮
|
||||||
_buildLogoutButton(),
|
_buildLogoutButton(context),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -172,9 +174,12 @@ class MyPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLogoutButton() {
|
Widget _buildLogoutButton(BuildContext context) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: () {},
|
onPressed: () async {
|
||||||
|
await GetIt.I<UserStorage>().deleteUser();
|
||||||
|
context.go('/auth/login');
|
||||||
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
foregroundColor: Colors.red,
|
foregroundColor: Colors.red,
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <isar_community_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_community_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
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 =
|
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
isar_community_flutter_libs
|
isar_community_flutter_libs
|
||||||
sentry_flutter
|
sentry_flutter
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
|
import file_selector_macos
|
||||||
import flutter_webrtc
|
import flutter_webrtc
|
||||||
import isar_community_flutter_libs
|
import isar_community_flutter_libs
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
@@ -16,6 +17,7 @@ import webview_flutter_wkwebview
|
|||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||||
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
|
|||||||
114
pubspec.lock
114
pubspec.lock
@@ -184,6 +184,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
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:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -288,6 +296,38 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
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:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -325,6 +365,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
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:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -447,6 +495,70 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.2"
|
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:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1110,4 +1222,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.38.0"
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ dependencies:
|
|||||||
|
|
||||||
# ===== 图片 =====
|
# ===== 图片 =====
|
||||||
flutter_svg: ^2.2.3
|
flutter_svg: ^2.2.3
|
||||||
|
image_picker: ^1.1.2
|
||||||
|
|
||||||
# ===== 屏幕适配,高刷等 =====
|
# ===== 屏幕适配,高刷等 =====
|
||||||
# flutter_displaymode: ^0.7.0
|
# flutter_displaymode: ^0.7.0
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <isar_community_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_community_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterWebRTCPluginRegisterWithRegistrar(
|
FlutterWebRTCPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
|
||||||
IsarFlutterLibsPluginRegisterWithRegistrar(
|
IsarFlutterLibsPluginRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
isar_community_flutter_libs
|
isar_community_flutter_libs
|
||||||
sentry_flutter
|
sentry_flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user