Mudança nos icones de ios.
@@ -1,6 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<application
|
<application
|
||||||
android:label="app"
|
android:label="EPVChat"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.8 KiB |
BIN
assets/icon/icon_logos.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
@@ -431,7 +431,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -488,7 +488,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 885 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 672 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 885 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'database_helper.dart';
|
||||||
|
|
||||||
class ChatScreen extends StatefulWidget {
|
class ChatScreen extends StatefulWidget {
|
||||||
const ChatScreen({super.key});
|
const ChatScreen({super.key});
|
||||||
@@ -12,17 +13,15 @@ class ChatScreen extends StatefulWidget {
|
|||||||
State<ChatScreen> createState() => _ChatScreenState();
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessage {
|
|
||||||
final String text;
|
|
||||||
final bool isAssistant;
|
|
||||||
|
|
||||||
ChatMessage({required this.text, this.isAssistant = false});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
||||||
final List<ChatMessage> _messages = [];
|
final List<ChatMessage> _messages = [];
|
||||||
|
final List<ChatSession> _sessions = [];
|
||||||
|
ChatSession? _currentSession;
|
||||||
|
|
||||||
final TextEditingController _textController = TextEditingController();
|
final TextEditingController _textController = TextEditingController();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
bool _isTyping = false;
|
bool _isTyping = false;
|
||||||
late AnimationController _typingController;
|
late AnimationController _typingController;
|
||||||
|
|
||||||
@@ -34,26 +33,62 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
duration: const Duration(milliseconds: 1200),
|
duration: const Duration(milliseconds: 1200),
|
||||||
)..repeat();
|
)..repeat();
|
||||||
|
|
||||||
Timer(const Duration(seconds: 2), () => _checkAvailableModels());
|
_loadInitialData();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkAvailableModels() async {
|
Future<void> _loadInitialData() async {
|
||||||
try {
|
await _loadSessions();
|
||||||
final response = await http.get(
|
if (_sessions.isNotEmpty) {
|
||||||
Uri.parse('http://89.114.196.110:11434/api/tags'),
|
await _selectSession(_sessions.first);
|
||||||
).timeout(const Duration(seconds: 15));
|
} else {
|
||||||
|
await _createNewSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
Future<void> _loadSessions() async {
|
||||||
final data = jsonDecode(response.body);
|
final sessions = await DatabaseHelper.instance.getSessions();
|
||||||
print("--- MODELOS DISPONÍVEIS ---");
|
if (mounted) {
|
||||||
if (data['models'] != null) {
|
setState(() {
|
||||||
for (var m in data['models']) {
|
_sessions.clear();
|
||||||
print("- ${m['name']}");
|
_sessions.addAll(sessions);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectSession(ChatSession session) async {
|
||||||
|
final messages = await DatabaseHelper.instance.getMessages(session.id!);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentSession = session;
|
||||||
|
_messages.clear();
|
||||||
|
_messages.addAll(messages.reversed);
|
||||||
|
_isTyping = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_scaffoldKey.currentState?.isDrawerOpen ?? false) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createNewSession() async {
|
||||||
|
final title = "Chat ${DateTime.now().hour}:${DateTime.now().minute}";
|
||||||
|
final id = await DatabaseHelper.instance.createSession(title);
|
||||||
|
await _loadSessions();
|
||||||
|
final newSession = _sessions.firstWhere((s) => s.id == id);
|
||||||
|
await _selectSession(newSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteSession(int id) async {
|
||||||
|
await DatabaseHelper.instance.deleteSession(id);
|
||||||
|
await _loadSessions();
|
||||||
|
if (_currentSession?.id == id) {
|
||||||
|
if (_sessions.isNotEmpty) {
|
||||||
|
await _selectSession(_sessions.first);
|
||||||
|
} else {
|
||||||
|
await _createNewSession();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} else {
|
||||||
print("Erro ao listar modelos: $e");
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,61 +100,76 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearChat() {
|
|
||||||
setState(() {
|
|
||||||
_messages.clear();
|
|
||||||
_isTyping = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleSubmitted(String text) async {
|
Future<void> _handleSubmitted(String text) async {
|
||||||
if (text.trim().isEmpty || _isTyping) return;
|
if (text.trim().isEmpty || _isTyping) return;
|
||||||
|
|
||||||
|
// Se por algum motivo não houver sessão, cria uma antes de enviar
|
||||||
|
if (_currentSession == null) {
|
||||||
|
await _createNewSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
final userMsgText = text.trim();
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
|
|
||||||
|
final userMsg = ChatMessage(
|
||||||
|
sessionId: _currentSession!.id!,
|
||||||
|
text: userMsgText,
|
||||||
|
isAssistant: false,
|
||||||
|
timestamp: DateTime.now().toIso8601String(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await DatabaseHelper.instance.insertMessage(userMsg);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_messages.insert(0, ChatMessage(text: text));
|
_messages.insert(0, userMsg);
|
||||||
_isTyping = true;
|
_isTyping = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final url = Uri.parse('http://89.114.196.110:11434/api/chat');
|
final url = Uri.parse('http://89.114.196.110:11434/api/chat');
|
||||||
|
|
||||||
final response = await http
|
final response = await http.post(
|
||||||
.post(
|
|
||||||
url,
|
url,
|
||||||
headers: {
|
headers: {'Content-Type': 'application/json'},
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: jsonEncode({
|
body: jsonEncode({
|
||||||
'model': 'qwen3:4b',
|
'model': 'qwen3:4b',
|
||||||
'messages': [{'role': 'user', 'content': text}],
|
'messages': [{'role': 'user', 'content': userMsgText}],
|
||||||
'stream': false,
|
'stream': false,
|
||||||
}),
|
}),
|
||||||
)
|
).timeout(const Duration(seconds: 60));
|
||||||
.timeout(const Duration(seconds: 60));
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
final reply = data['message']?['content'] ?? 'Sem resposta.';
|
final replyText = data['message']?['content'] ?? 'Sem resposta.';
|
||||||
|
|
||||||
|
final assistantMsg = ChatMessage(
|
||||||
|
sessionId: _currentSession!.id!,
|
||||||
|
text: replyText,
|
||||||
|
isAssistant: true,
|
||||||
|
timestamp: DateTime.now().toIso8601String(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await DatabaseHelper.instance.insertMessage(assistantMsg);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isTyping = false;
|
_isTyping = false;
|
||||||
_messages.insert(0, ChatMessage(text: reply, isAssistant: true));
|
_messages.insert(0, assistantMsg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Erro HTTP ${response.statusCode}: ${response.body}');
|
throw Exception('Erro HTTP ${response.statusCode}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isTyping = false;
|
_isTyping = false;
|
||||||
_messages.insert(
|
_messages.insert(0, ChatMessage(
|
||||||
0,
|
sessionId: _currentSession!.id!,
|
||||||
ChatMessage(text: "Erro: $e", isAssistant: true),
|
text: "Erro de ligação. Verifique se o servidor está online.",
|
||||||
);
|
isAssistant: true,
|
||||||
|
timestamp: DateTime.now().toIso8601String(),
|
||||||
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +197,9 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
bottomLeft: Radius.circular(isAssistant ? 4 : 20),
|
bottomLeft: Radius.circular(isAssistant ? 4 : 20),
|
||||||
bottomRight: Radius.circular(isAssistant ? 20 : 4),
|
bottomRight: Radius.circular(isAssistant ? 20 : 4),
|
||||||
),
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: isAssistant
|
child: isAssistant
|
||||||
? MarkdownBody(
|
? MarkdownBody(
|
||||||
@@ -154,13 +207,9 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
styleSheet: MarkdownStyleSheet(
|
styleSheet: MarkdownStyleSheet(
|
||||||
p: const TextStyle(color: Colors.black87, fontSize: 15, height: 1.4),
|
p: const TextStyle(color: Colors.black87, fontSize: 15, height: 1.4),
|
||||||
strong: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
strong: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
|
||||||
listBullet: const TextStyle(color: Colors.black87),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(message.text, style: const TextStyle(color: Colors.white, fontSize: 15)),
|
||||||
message.text,
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 15, height: 1.4),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isAssistant) _buildAvatar(Icons.person),
|
if (!isAssistant) _buildAvatar(Icons.person),
|
||||||
@@ -218,61 +267,47 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTextComposer() {
|
Widget _buildTextComposer() {
|
||||||
return ClipRRect(
|
return Container(
|
||||||
child: BackdropFilter(
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
decoration: BoxDecoration(
|
||||||
child: Container(
|
color: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
border: Border(top: BorderSide(color: Colors.black.withOpacity(0.05))),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: Colors.white.withOpacity(0.8),
|
child: SafeArea(
|
||||||
border: Border(top: BorderSide(color: Colors.black.withOpacity(0.05), width: 0.5)),
|
child: Row(
|
||||||
),
|
children: [
|
||||||
child: SafeArea(
|
Expanded(
|
||||||
child: Row(
|
child: Container(
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
IconButton(
|
color: const Color(0xFFF1F5F9),
|
||||||
icon: const Icon(Icons.delete_sweep_rounded, color: Colors.redAccent),
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
onPressed: _isTyping ? null : _clearChat,
|
|
||||||
),
|
),
|
||||||
Expanded(
|
child: TextField(
|
||||||
child: Container(
|
controller: _textController,
|
||||||
decoration: BoxDecoration(
|
enabled: !_isTyping,
|
||||||
color: const Color(0xFFF1F5F9),
|
onSubmitted: _handleSubmitted,
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
decoration: const InputDecoration(
|
||||||
border: Border.all(color: Colors.black.withOpacity(0.05)),
|
hintText: "Mensagem EPVChat...",
|
||||||
),
|
border: InputBorder.none,
|
||||||
child: TextField(
|
contentPadding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 14.0),
|
||||||
controller: _textController,
|
|
||||||
enabled: !_isTyping,
|
|
||||||
onSubmitted: _handleSubmitted,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: _isTyping ? "Aguarde a resposta..." : "Mensagem EPVChat...",
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 14.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: _isTyping
|
|
||||||
? [Colors.grey, Colors.grey]
|
|
||||||
: [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.send_rounded, color: Colors.white),
|
|
||||||
onPressed: _isTyping ? null : () => _handleSubmitted(_textController.text),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: _isTyping ? [Colors.grey, Colors.grey] : [const Color(0xFF8ad5c9), const Color(0xFF57a7ed)],
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.send_rounded, color: Colors.white),
|
||||||
|
onPressed: _isTyping ? null : () => _handleSubmitted(_textController.text),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -283,45 +318,123 @@ class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
|
|||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
drawer: _buildSidebar(),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: Column(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
// Sombreado Mint
|
||||||
child: ListView(
|
Positioned(
|
||||||
controller: _scrollController,
|
top: -screenWidth * 0.45,
|
||||||
reverse: true,
|
left: -screenWidth * 0.2,
|
||||||
children: [
|
right: -screenWidth * 0.2,
|
||||||
...(_isTyping ? [_buildTypingIndicator()] : []),
|
child: Container(
|
||||||
..._messages.map((msg) => _buildMessage(msg)),
|
height: screenWidth * 1.1,
|
||||||
Padding(
|
decoration: BoxDecoration(
|
||||||
padding: const EdgeInsets.only(bottom: 50, top: 20),
|
shape: BoxShape.circle,
|
||||||
child: Stack(
|
gradient: RadialGradient(
|
||||||
alignment: Alignment.center,
|
center: Alignment.center,
|
||||||
|
radius: 0.5,
|
||||||
|
colors: [const Color(0xFF8ad5c9).withOpacity(0.6), const Color(0xFF8ad5c9).withOpacity(0.0)],
|
||||||
|
stops: const [0.2, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
// Área do Topo (Menu e Logo)
|
||||||
|
SafeArea(
|
||||||
|
child: Container(
|
||||||
|
height: 100,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
IconButton(
|
||||||
height: screenWidth * 0.8,
|
icon: const Icon(Icons.menu_rounded, color: Color(0xFF57a7ed), size: 32),
|
||||||
decoration: BoxDecoration(
|
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
|
||||||
shape: BoxShape.circle,
|
|
||||||
gradient: RadialGradient(
|
|
||||||
colors: [
|
|
||||||
const Color(0xFF8ad5c9).withOpacity(0.4),
|
|
||||||
const Color(0xFF8ad5c9).withOpacity(0.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Image.asset(
|
|
||||||
'assets/logo.png',
|
|
||||||
height: 170,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
),
|
||||||
|
Image.asset('assets/logo.png', height: 100, errorBuilder: (c,e,s) => const SizedBox(width: 100)),
|
||||||
|
const SizedBox(width: 48), // Equilíbrio para o ícone do menu
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
|
reverse: true,
|
||||||
|
itemCount: _messages.length + (_isTyping ? 1 : 0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (_isTyping && index == 0) return _buildTypingIndicator();
|
||||||
|
int msgIndex = _isTyping ? index - 1 : index;
|
||||||
|
return _buildMessage(_messages[msgIndex]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildTextComposer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSidebar() {
|
||||||
|
return Drawer(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.75,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
DrawerHeader(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(colors: [Color(0xFF8ad5c9), Color(0xFF57a7ed)]),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.auto_awesome, color: Colors.white, size: 40),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text('Histórico de Chats', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.add_comment_rounded, color: Color(0xFF57a7ed)),
|
||||||
|
title: const Text('Nova Conversa', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onTap: _createNewSession,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: _sessions.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final session = _sessions[index];
|
||||||
|
final isSelected = _currentSession?.id == session.id;
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? const Color(0xFF8ad5c9).withOpacity(0.1) : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(session.title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline_rounded, color: Colors.redAccent),
|
||||||
|
onPressed: () => _deleteSession(session.id!),
|
||||||
|
),
|
||||||
|
onTap: () => _selectSession(session),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildTextComposer(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
144
lib/database_helper.dart
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
|
class ChatSession {
|
||||||
|
final int? id;
|
||||||
|
final String title;
|
||||||
|
final String timestamp;
|
||||||
|
|
||||||
|
ChatSession({this.id, required this.title, required this.timestamp});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': title,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ChatSession.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ChatSession(
|
||||||
|
id: map['id'],
|
||||||
|
title: map['title'],
|
||||||
|
timestamp: map['timestamp'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessage {
|
||||||
|
final int? id;
|
||||||
|
final int sessionId;
|
||||||
|
final String text;
|
||||||
|
final bool isAssistant;
|
||||||
|
final String timestamp;
|
||||||
|
|
||||||
|
ChatMessage({
|
||||||
|
this.id,
|
||||||
|
required this.sessionId,
|
||||||
|
required this.text,
|
||||||
|
required this.isAssistant,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'sessionId': sessionId,
|
||||||
|
'text': text,
|
||||||
|
'isAssistant': isAssistant ? 1 : 0,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory ChatMessage.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ChatMessage(
|
||||||
|
id: map['id'],
|
||||||
|
sessionId: map['sessionId'],
|
||||||
|
text: map['text'],
|
||||||
|
isAssistant: map['isAssistant'] == 1,
|
||||||
|
timestamp: map['timestamp'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseHelper {
|
||||||
|
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||||
|
static Database? _database;
|
||||||
|
|
||||||
|
DatabaseHelper._init();
|
||||||
|
|
||||||
|
Future<Database> get database async {
|
||||||
|
if (_database != null) return _database!;
|
||||||
|
_database = await _initDB('chat.db');
|
||||||
|
return _database!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Database> _initDB(String filePath) async {
|
||||||
|
final dbPath = await getDatabasesPath();
|
||||||
|
final path = join(dbPath, filePath);
|
||||||
|
|
||||||
|
return await openDatabase(
|
||||||
|
path,
|
||||||
|
version: 1,
|
||||||
|
onCreate: _createDB,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future _createDB(Database db, int version) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
timestamp TEXT NOT NULL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sessionId INTEGER NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
isAssistant INTEGER NOT NULL,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (sessionId) REFERENCES sessions (id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session operations
|
||||||
|
Future<int> createSession(String title) async {
|
||||||
|
final db = await instance.database;
|
||||||
|
return await db.insert('sessions', {
|
||||||
|
'title': title,
|
||||||
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChatSession>> getSessions() async {
|
||||||
|
final db = await instance.database;
|
||||||
|
final result = await db.query('sessions', orderBy: 'timestamp DESC');
|
||||||
|
return result.map((json) => ChatSession.fromMap(json)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteSession(int id) async {
|
||||||
|
final db = await instance.database;
|
||||||
|
await db.delete('sessions', where: 'id = ?', whereArgs: [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message operations
|
||||||
|
Future<int> insertMessage(ChatMessage message) async {
|
||||||
|
final db = await instance.database;
|
||||||
|
return await db.insert('messages', message.toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ChatMessage>> getMessages(int sessionId) async {
|
||||||
|
final db = await instance.database;
|
||||||
|
final result = await db.query(
|
||||||
|
'messages',
|
||||||
|
where: 'sessionId = ?',
|
||||||
|
whereArgs: [sessionId],
|
||||||
|
orderBy: 'timestamp ASC',
|
||||||
|
);
|
||||||
|
return result.map((json) => ChatMessage.fromMap(json)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ class MyApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'EPVChat! Clone',
|
title: 'EPVChat!',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
scaffoldBackgroundColor: Colors.white,
|
scaffoldBackgroundColor: Colors.white,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
156
pubspec.lock
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -33,6 +41,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -65,11 +89,27 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -107,6 +147,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.11.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -172,13 +228,45 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -192,6 +280,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.2"
|
version: "1.10.2"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_android
|
||||||
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2+3"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
sqflite_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_darwin
|
||||||
|
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
sqflite_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_platform_interface
|
||||||
|
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -216,6 +344,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -264,6 +400,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
|
yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.1 <4.0.0"
|
dart: ">=3.11.1 <4.0.0"
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.38.0"
|
||||||
|
|||||||
@@ -13,13 +13,22 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
http: ^1.6.0
|
http: ^1.6.0
|
||||||
flutter_markdown: ^0.7.3
|
flutter_markdown: ^0.7.3
|
||||||
|
sqflite: ^2.3.0
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
|
flutter_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icon/icon_logos.png"
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
assets:
|
assets:
|
||||||
- assets/logo.png
|
- assets/logo.png
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 33 KiB |