감정표현, 채팅응답대기 추가

This commit is contained in:
2026-06-24 15:31:06 +09:00
parent 8465be1d06
commit 44100e387b
29 changed files with 326 additions and 1153 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,11 +2,13 @@ class ChatMessage {
final String text; final String text;
final bool isUser; final bool isUser;
final DateTime timestamp; final DateTime timestamp;
final int? emotionCategory; // ← 새로 추가 (봇 메시지의 감정)
ChatMessage({ ChatMessage({
required this.text, required this.text,
required this.isUser, required this.isUser,
required this.timestamp, required this.timestamp,
this.emotionCategory,
}); });
// JSON에서 ChatMessage로 변환 // JSON에서 ChatMessage로 변환
@@ -15,6 +17,7 @@ class ChatMessage {
text: json['text'] as String, text: json['text'] as String,
isUser: json['isUser'] as bool, isUser: json['isUser'] as bool,
timestamp: DateTime.parse(json['timestamp'] as String), timestamp: DateTime.parse(json['timestamp'] as String),
emotionCategory: json['emotionCategory'] as int?,
); );
} }
@@ -24,6 +27,7 @@ class ChatMessage {
'text': text, 'text': text,
'isUser': isUser, 'isUser': isUser,
'timestamp': timestamp.toIso8601String(), 'timestamp': timestamp.toIso8601String(),
'emotionCategory': emotionCategory,
}; };
} }
} }

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import '../models/chat_message.dart'; import '../models/chat_message.dart';
import '../services/chat_service.dart'; import '../services/chat_service.dart';
import 'dart:math'; //26.06.24 추가
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
const ChatScreen({Key? key}) : super(key: key); const ChatScreen({Key? key}) : super(key: key);
@@ -18,31 +20,73 @@ class _ChatScreenState extends State<ChatScreen> {
late VideoPlayerController _videoController; late VideoPlayerController _videoController;
bool _isVideoInitialized = false; bool _isVideoInitialized = false;
bool _isLoading = false; bool _isLoading = false;
int _currentEmotion = 1; // ← 추가 (현재 감정, 기본값: 1 = DEFAULT)
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeVideo(); // 더미 컨트롤러로 먼저 초기화
_videoController = VideoPlayerController.asset('assets/videos/default.mp4');
// 실제 감정에 따른 비디오 초기화
_initializeVideo(_currentEmotion);
_addInitialMessage(); _addInitialMessage();
} }
void _initializeVideo() { //26.06.24 추가 시작
_videoController = String _getVideoFileName(int emotionCategory) {
VideoPlayerController.asset('assets/videos/basic_img.mp4') switch(emotionCategory) {
..initialize() case 1:
.then((_) { return 'default.mp4'; // DEFAULT
_videoController.setLooping(true); case 2:
_videoController.setVolume(0.0); return 'joy.mp4'; // JOY
_videoController.play(); case 3:
setState(() { return 'thinking.mp4'; // THINKING
_isVideoInitialized = true; case 4:
}); return 'realization.mp4'; // REALIZATION
}) case 5:
.catchError((error) { return 'angry.mp4'; // ANGER
print('비디오 로드 오류: $error'); case 6:
return 'thirst.mp4'; // THIRST
case 7:
return 'cold.mp4'; // COLD
default:
return 'default.mp4'; // 기본값
}
}
void _initializeVideo(int emotionCategory) {
final videoFileName = _getVideoFileName(emotionCategory);
print('비디오 변경: emotion=$emotionCategory$videoFileName');
// 새 비디오 컨트롤러 생성
final newController = VideoPlayerController.asset('assets/videos/$videoFileName');
// 새 비디오를 먼저 로드
newController.initialize().then((_) {
// 로드 완료 후에 기존 컨트롤러 교체
try {
if (_videoController.hasListeners) {
_videoController.dispose();
}
} catch (e) {
print('기존 컨트롤러 dispose 스킵: $e');
}
// 새 컨트롤러로 교체
_videoController = newController;
_videoController.setLooping(true);
_videoController.setVolume(0.0);
_videoController.play();
setState(() {
_isVideoInitialized = true;
}); });
}).catchError((error) {
print('비디오 로드 오류: $error');
});
} }
//26.06.24 추가 끝
void _addInitialMessage() { void _addInitialMessage() {
setState(() { setState(() {
@@ -51,35 +95,68 @@ class _ChatScreenState extends State<ChatScreen> {
text: '상태 분석이 완료되었습니다.\n무엇을 도와드릴까요?', text: '상태 분석이 완료되었습니다.\n무엇을 도와드릴까요?',
isUser: false, isUser: false,
timestamp: DateTime.now(), timestamp: DateTime.now(),
emotionCategory: _currentEmotion, // 감정 추가
), ),
); );
}); });
} }
//26.06.24 추가 시작
void _sendMessage(String text) async { void _sendMessage(String text) async {
if (text.isEmpty) return; if (text.isEmpty) return;
// 사용자 메시지 추가 // 사용자 메시지 추가
setState(() { setState(() {
_messages.add( _messages.add(
ChatMessage(text: text, isUser: true, timestamp: DateTime.now()), ChatMessage(
text: text,
isUser: true,
timestamp: DateTime.now(),
emotionCategory: null, // 사용자 메시지는 감정값 없음
),
); );
_isLoading = true;
}); });
_textController.clear(); _textController.clear();
_scrollToBottom(); _scrollToBottom();
// 로딩 메시지 추가 (... 표시)
setState(() {
_messages.add(
ChatMessage(
text: '로딩 중...',
isUser: false,
timestamp: DateTime.now(),
emotionCategory: _currentEmotion, // 현재 감정값 유지
),
);
_isLoading = true;
});
_scrollToBottom();
// 서버로 메시지 전송 // 서버로 메시지 전송
try { try {
final response = await ChatService.sendMessage(text); final (responseMessage, emotionCategory) = await ChatService.sendMessage(text);
print('받은 감정값: $emotionCategory (기존: $_currentEmotion)');
setState(() { setState(() {
// 마지막 로딩 메시지를 실제 응답으로 교체
_messages.removeLast(); // 로딩 메시지 제거
// 감정값이 변경되었으면 비디오 변경
if (emotionCategory != _currentEmotion) {
print('감정 변경! $emotionCategory로 비디오 교체');
_currentEmotion = emotionCategory;
// 백그라운드에서 비디오 로드 (화면 깜빡임 없음)
_initializeVideo(_currentEmotion);
}
_messages.add( _messages.add(
ChatMessage( ChatMessage(
text: response, text: responseMessage,
isUser: false, isUser: false,
timestamp: DateTime.now(), timestamp: DateTime.now(),
emotionCategory: emotionCategory, // ← 추가
), ),
); );
_isLoading = false; _isLoading = false;
@@ -88,11 +165,13 @@ class _ChatScreenState extends State<ChatScreen> {
} catch (e) { } catch (e) {
print('메시지 전송 오류: $e'); print('메시지 전송 오류: $e');
setState(() { setState(() {
_messages.removeLast(); // ← 로딩 메시지 제거
_messages.add( _messages.add(
ChatMessage( ChatMessage(
text: '오류가 발생했습니다. 다시 시도해주세요.', text: '오류가 발생했습니다. 다시 시도해주세요.',
isUser: false, isUser: false,
timestamp: DateTime.now(), timestamp: DateTime.now(),
emotionCategory: _currentEmotion, // ← 추가
), ),
); );
_isLoading = false; _isLoading = false;
@@ -100,6 +179,7 @@ class _ChatScreenState extends State<ChatScreen> {
_scrollToBottom(); _scrollToBottom();
} }
} }
//26.06.24 추가 끝
void _scrollToBottom() { void _scrollToBottom() {
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
@@ -453,10 +533,18 @@ class _ChatScreenState extends State<ChatScreen> {
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
itemCount: _messages.length, itemCount: _messages.length,
//26.06.24 추가 시작
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = _messages[index]; final message = _messages[index];
return _ChatBubble(message: message); final isLoadingMessage = _isLoading &&
index == _messages.length - 1 &&
!message.isUser;
return _ChatBubble(
message: message,
isLoading: isLoadingMessage,
);
}, },
//26.06.24 추가 끝
), ),
), ),
), ),
@@ -748,8 +836,12 @@ class _ChatScreenState extends State<ChatScreen> {
class _ChatBubble extends StatelessWidget { class _ChatBubble extends StatelessWidget {
final ChatMessage message; final ChatMessage message;
final bool isLoading; //26.06.24 추가
const _ChatBubble({required this.message}); const _ChatBubble({
required this.message,
this.isLoading = false //26.06.24 추가
});
String _formatTime(DateTime dateTime) { String _formatTime(DateTime dateTime) {
final hour = dateTime.hour; final hour = dateTime.hour;
@@ -819,16 +911,20 @@ class _ChatBubble extends StatelessWidget {
), ),
], ],
), ),
child: SelectableText( // 26.06.24 추가 시작
child: isLoading
? _LoadingAnimation() // 로딩 중
: SelectableText(
message.text, message.text,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
color: message.isUser color: message.isUser
? Colors.black ? Color(0xFF000000)
: Colors.black, : Color(0xFF424242),
height: 1.4, height: 1.4,
), ),
), ),
// 26.06.24 추가 끝
), ),
), ),
if (!message.isUser) const SizedBox(width: 8), if (!message.isUser) const SizedBox(width: 8),
@@ -845,3 +941,127 @@ class _ChatBubble extends StatelessWidget {
); );
} }
} }
//26.06.24 추가 시작
class _LoadingAnimation extends StatefulWidget {
const _LoadingAnimation();
@override
State<_LoadingAnimation> createState() => _LoadingAnimationState();
}
class _LoadingAnimationState extends State<_LoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 900), //움직이는 속도
vsync: this,
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double _getOffset(double progress, int index) {
final delay = index * 0.15;
final adjustedProgress = (progress + delay) % 1.0;
return sin(adjustedProgress * pi * 2) * 2;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final progress = _controller.value;
return SizedBox(
height: 20, // 일반 텍스트 높이와 동일
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_AnimatedDot(
progress: progress,
index: 0,
getOffset: _getOffset,
),
const SizedBox(width: 3),
_AnimatedDot(
progress: progress,
index: 1,
getOffset: _getOffset,
),
const SizedBox(width: 3),
_AnimatedDot(
progress: progress,
index: 2,
getOffset: _getOffset,
),
],
),
);
},
);
}
}
// 개별 원형 점 위젯
class _AnimatedDot extends StatelessWidget {
final double progress;
final int index;
final double Function(double, int) getOffset;
const _AnimatedDot({
required this.progress,
required this.index,
required this.getOffset,
});
@override
Widget build(BuildContext context) {
final offset = getOffset(progress, index);
return Transform.translate(
offset: Offset(0, offset),
child: SizedBox( //원형 사이즈
width: 5,
height: 5,
child: CustomPaint(
painter: _DotPainter(),
),
),
);
}
}
// CustomPaint로 원 그리기
class _DotPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF424242)
..style = PaintingStyle.fill;
// 원의 중심과 반지름
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(_DotPainter oldDelegate) => false;
}
//26.06.24 추가 끝
//26.06.24 추가 시작
//26.06.24

View File

@@ -10,7 +10,8 @@ class ChatService {
static const int timeoutSeconds = 30; static const int timeoutSeconds = 30;
/// 사용자 메시지를 서버로 전송하고 응답을 받는 메서드 /// 사용자 메시지를 서버로 전송하고 응답을 받는 메서드
static Future<String> sendMessage(String userMessage) async { /// 반환값: (메시지, emotion_category) 튜플
static Future<(String, int)> sendMessage(String userMessage) async {
try { try {
// 요청 생성 // 요청 생성
final requestBody = { final requestBody = {
@@ -21,15 +22,7 @@ class ChatService {
'user_id': 'user-123', 'user_id': 'user-123',
}; };
// JSON 형식 확인 print('서버로 요청 전송: $requestBody');
final jsonBody = jsonEncode(requestBody);
print('서버로 요청 전송 (Map): $requestBody');
print('서버로 요청 전송 (JSON): $jsonBody');
final uri = Uri.parse(serverUrl);
print('요청 URI: $uri');
print('요청 헤더: {Content-Type: application/json, accept: application/json}');
print('요청 바디: ${jsonEncode(requestBody)}');
// POST 요청 전송 // POST 요청 전송
final response = await http.post( final response = await http.post(
@@ -49,36 +42,37 @@ class ChatService {
// 상태 코드 확인 // 상태 코드 확인
if (response.statusCode == 200) { if (response.statusCode == 200) {
// 성공
final jsonResponse = jsonDecode(response.body); final jsonResponse = jsonDecode(response.body);
print('전체 응답: $jsonResponse'); // 응답에서 'message' 필드 추출
// 'message' 필드 추출
final responseMessage = jsonResponse['message'] as String?; final responseMessage = jsonResponse['message'] as String?;
// 응답에서 'emotion_category' 필드 추출 (기본값: 1)
final emotionCategory = jsonResponse['emotion_category'] as int? ?? 1;
if (responseMessage != null && responseMessage.isNotEmpty) { if (responseMessage != null && responseMessage.isNotEmpty) {
print('응답 메시지: $responseMessage'); print('응답 메시지: $responseMessage');
return responseMessage; print('감정 카테고리: $emotionCategory');
return (responseMessage, emotionCategory);
} else { } else {
print('message 필드가 없습니다'); print('응답이 비어있습니다');
// 혹시 다른 필드에 응답이 있는지 확인 return ('응답을 받지 못했습니다', 1);
print('응답 전체: ${jsonResponse.toString()}');
return '응답을 받지 못했습니다';
} }
} else { } else {
// 실패 // 실패
print('서버 오류: ${response.statusCode}'); print('서버 오류: ${response.statusCode}');
return '서버 오류 (${response.statusCode})'; return ('서버 오류 (${response.statusCode})', 1);
} }
} on TimeoutException catch (e) { } on TimeoutException catch (e) {
print('타임아웃: $e'); print('타임아웃: $e');
return '서버 응답이 없습니다. 시간 초과'; return ('서버 응답이 없습니다. 시간 초과', 1);
} on FormatException catch (e) { } on FormatException catch (e) {
print('JSON 파싱 오류: $e'); print('JSON 파싱 오류: $e');
return 'JSON 형식 오류'; return ('JSON 형식 오류', 1);
} catch (e) { } catch (e) {
print('오류 발생: $e'); print('오류 발생: $e');
return '오류가 발생했습니다: $e'; return ('오류가 발생했습니다: $e', 1);
} }
} }
@@ -105,6 +99,62 @@ class ChatService {
return false; return false;
} }
} }
/// 서버 상태 상세 테스트
static Future<void> testServerDetailed() async {
print('\n=== 서버 상세 테스트 시작 ===\n');
try {
print('서버 주소: $serverUrl');
print('연결 시도 중...\n');
// 1. 연결 테스트 (GET)
print('⃣ GET 요청 테스트...');
try {
final getResponse = await http.get(
Uri.parse(serverUrl),
).timeout(const Duration(seconds: 5));
print('GET 상태코드: ${getResponse.statusCode}');
print('GET 응답: ${getResponse.body}\n');
} catch (e) {
print('GET 오류: $e\n');
}
// 2. POST 테스트 (실제 요청)
print('POST 요청 테스트...');
final testBody = {
'device_id': 1,
'locale': 'ko-KR',
'message': '테스트',
'session_id': 'session-001',
'user_id': 'user-123',
};
print('전송할 데이터: $testBody\n');
try {
final postResponse = await http.post(
Uri.parse(serverUrl),
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
},
body: jsonEncode(testBody),
).timeout(const Duration(seconds: 5));
print('POST 상태코드: ${postResponse.statusCode}');
print('POST 응답: ${postResponse.body}\n');
} catch (e) {
print('POST 오류: $e\n');
}
} catch (e) {
print('테스트 오류: $e\n');
}
print('=== 테스트 완료 ===\n');
}
} }
/// 타임아웃 예외 /// 타임아웃 예외

View File

@@ -1,253 +0,0 @@
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; // 비디오 패키지 임포트
class ChatbotDetailPage extends StatefulWidget {
final VoidCallback onBack;
const ChatbotDetailPage({super.key, required this.onBack});
@override
State<ChatbotDetailPage> createState() => _ChatbotDetailPageState();
}
class _ChatbotDetailPageState extends State<ChatbotDetailPage> {
late VideoPlayerController _knowVideoController;
bool _isVideoInitialized = false;
bool _hasVideoError = false;
@override
void initState() {
super.initState();
_initializeKnowVideo();
}
void _initializeKnowVideo() {
const String videoAssetPath = 'assets/assets/videos/know.mp4';
_knowVideoController = VideoPlayerController.asset(videoAssetPath);
_knowVideoController.initialize().then((_) {
if (!mounted) return;
setState(() {
_isVideoInitialized = true;
});
_knowVideoController.setLooping(true);
_knowVideoController.setVolume(0.0);
_knowVideoController.play();
}).catchError((error) {
debugPrint("1차 지식 영상 로딩 실패, 백업 경로 시도 중...: $error");
_knowVideoController = VideoPlayerController.asset('assets/videos/know.mp4');
_knowVideoController.initialize().then((_) {
if (!mounted) return;
setState(() {
_isVideoInitialized = true;
});
_knowVideoController.setLooping(true);
_knowVideoController.setVolume(0.0);
_knowVideoController.play();
}).catchError((retryError) {
if (!mounted) return;
setState(() {
_hasVideoError = true;
});
debugPrint("최종 지식 비디오 초기화 에러: $retryError");
});
});
}
@override
void dispose() {
_knowVideoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 상단 미니 센서 바
SizedBox(
height: 100,
child: Row(
children: [
_buildMiniSensor('온도', '24.5°C', Colors.orangeAccent),
const SizedBox(width: 10),
_buildMiniSensor('습도', '65%', Colors.blueAccent),
const SizedBox(width: 10),
_buildMiniSensor('CO2', '420 ppm', Colors.greenAccent),
const SizedBox(width: 10),
_buildMiniSensor('TVOC', '0.15 mg/m³', Colors.tealAccent),
const SizedBox(width: 10),
_buildMiniSensor('미세먼지', '15', Colors.white70),
],
),
),
const SizedBox(height: 20),
// 메인 콘텐츠 영역
Expanded(
child: Row(
children: [
// 🤖 [좌측] 푸미 캐릭터 전용 패널 (✨ flex: 60 으로 수정)
Expanded(
flex: 60,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1D23),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Stack(
children: [
// 🔙 "목록으로 돌아가기" 버튼
Positioned(
top: 20,
left: 20,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.onBack,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFB4E49C).withOpacity(0.4)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.arrow_back_ios_new, color: Color(0xFFB4E49C), size: 14),
SizedBox(width: 8),
Text(
'목록으로 돌아가기',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
],
),
),
),
),
),
// 정중앙 know.mp4 대형 배치
Center(
child: _hasVideoError
? Icon(Icons.smart_toy, size: 160, color: const Color(0xFFB4E49C).withOpacity(0.3))
: _isVideoInitialized
? SizedBox(
width: 600, // ✨ 메인 화면과 짝을 맞춰 550 스케일로 정돈 극대화!
child: AspectRatio(
aspectRatio: _knowVideoController.value.aspectRatio,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: VideoPlayer(_knowVideoController),
),
),
)
: const SizedBox(
width: 100,
height: 100,
child: Center(child: CircularProgressIndicator(color: Color(0xFFB4E49C))),
),
),
],
),
),
),
const SizedBox(width: 20),
// 💬 [우측] 채팅창 (✨ flex: 40 으로 수정)
Expanded(
flex: 40,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1D23),
borderRadius: BorderRadius.circular(24),
),
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(20),
child: Row(
children: [
CircleAvatar(backgroundColor: Color(0xFFB4E49C), radius: 4),
SizedBox(width: 10),
Text('푸미 일지', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
_buildChatBubble("상태 분석이 완료되었습니다. 무엇을 도와드릴까요?", true),
_buildChatBubble("현재 습도가 적당하니?", false),
_buildChatBubble("네, 습도 65%로 최적의 상태입니다.", true),
],
),
),
// 하단 입력창
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: InputDecoration(
hintText: 'Ask AI...',
hintStyle: const TextStyle(color: Colors.white24, fontSize: 14),
filled: true,
fillColor: Colors.white.withOpacity(0.05),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
suffixIcon: const Icon(Icons.send, color: Color(0xFFB4E49C), size: 20),
),
),
),
],
),
),
),
],
),
),
],
);
}
// 채팅 말풍선 헬퍼
Widget _buildChatBubble(String msg, bool isBot) {
return Align(
alignment: isBot ? Alignment.centerLeft : Alignment.centerRight,
child: Container(
margin: const EdgeInsets.symmetric(vertical: 5),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isBot ? Colors.white.withOpacity(0.08) : const Color(0xFFB4E49C).withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
),
child: Text(msg, style: const TextStyle(color: Colors.white, fontSize: 13)),
),
);
}
// 미니 센서 카드 헬퍼
Widget _buildMiniSensor(String t, String v, Color c) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0xFF1A1D23), borderRadius: BorderRadius.circular(15)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(t, style: const TextStyle(color: Colors.white54, fontSize: 11)),
Text(v, style: TextStyle(color: c, fontSize: 16, fontWeight: FontWeight.bold)),
],
),
),
);
}
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class Co2DataPage extends StatelessWidget {
const Co2DataPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'🍃 실시간 CO2 농도 변화 추이 분석',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60),
),
);
}
}

View File

@@ -1,198 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
/// 📊 [공통 위젯] 모든 센서(온도/습도/CO2 등)가 공유하는 실시간 차트 카드.
///
/// DashboardGrid 의 `_buildSensorCard` 스타일(배경 #1F242C, 라운드 16,
/// white10 테두리, 헤더 + 강조 색상)을 그대로 따르며, 하단에 fl_chart
/// 라인 차트를 붙여 [stream] 으로 흘러오는 데이터를 실시간으로 그린다.
///
/// 사용 예:
/// ```dart
/// SensorChartCard(
/// title: '온도',
/// icon: Icons.thermostat,
/// accentColor: Colors.orangeAccent,
/// unit: '°C',
/// stream: tempService.stream,
/// )
/// ```
class SensorChartCard extends StatelessWidget {
const SensorChartCard({
super.key,
required this.title,
required this.icon,
required this.accentColor,
required this.unit,
required this.stream,
});
/// 카드 제목 (예: '온도', '습도', '이산화탄소')
final String title;
/// 헤더 우측에 표시할 아이콘
final IconData icon;
/// 라인/아이콘/현재값에 쓰일 강조 색상
final Color accentColor;
/// 값 뒤에 붙는 단위 (예: '°C', '%', 'ppm')
final String unit;
/// 갱신될 때마다 "전체 측정값 리스트"가 흘러나오는 스트림
final Stream<List<double>> stream;
static const Color _cardColor = Color(0xFF1F242C);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: _cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white10),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: StreamBuilder<List<double>>(
stream: stream,
builder: (context, snapshot) {
final data = snapshot.data ?? const <double>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(data),
const SizedBox(height: 20),
Expanded(child: _buildChart(data)),
],
);
},
),
);
}
// 🏷️ 헤더: 제목 + 현재값 + 아이콘 (센서 카드와 동일한 톤)
Widget _buildHeader(List<double> data) {
final hasData = data.isNotEmpty;
final latest = hasData ? data.last : null;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
hasData ? latest!.toStringAsFixed(1) : '--',
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(width: 4),
Text(
unit,
style: const TextStyle(fontSize: 14, color: Colors.white38),
),
],
),
],
),
Icon(icon, color: accentColor.withOpacity(0.8), size: 22),
],
);
}
// 📈 라인 차트 (fl_chart)
Widget _buildChart(List<double> data) {
if (data.isEmpty) {
return const Center(
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFB4E49C),
),
),
);
}
final spots = <FlSpot>[
for (var i = 0; i < data.length; i++) FlSpot(i.toDouble(), data[i]),
];
final minY = data.reduce((a, b) => a < b ? a : b);
final maxY = data.reduce((a, b) => a > b ? a : b);
// 위아래로 약간 여백을 둬서 선이 테두리에 붙지 않게 한다.
final padding = ((maxY - minY).abs() * 0.15).clamp(1.0, double.infinity);
return LineChart(
LineChartData(
minX: 0,
maxX: (data.length - 1).toDouble(),
minY: minY - padding,
maxY: maxY + padding,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: ((maxY + padding) - (minY - padding)) / 4,
getDrawingHorizontalLine: (value) =>
const FlLine(color: Colors.white10, strokeWidth: 1),
),
titlesData: const FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (_) => _cardColor.withOpacity(0.9),
getTooltipItems: (touchedSpots) => [
for (final s in touchedSpots)
LineTooltipItem(
'${s.y.toStringAsFixed(1)} $unit',
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
lineBarsData: [
LineChartBarData(
spots: spots,
isCurved: true,
color: accentColor,
barWidth: 2.5,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
accentColor.withOpacity(0.25),
accentColor.withOpacity(0.0),
],
),
),
),
],
),
);
}
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class DeviceManagePage extends StatelessWidget {
const DeviceManagePage({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'⚙️ 모터, 팬, LED 광원 장비 제어 관리 시스템',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60),
),
);
}
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class DustDataPage extends StatelessWidget {
const DustDataPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'😷 실시간 미세먼지(PM2.5 / PM10) 모니터링',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60),
),
);
}
}

View File

@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
class Header extends StatelessWidget {
// ✨ 메인 화면(main.dart)으로부터 동적 타이틀과 토글 함수를 전달받도록 변수 설정
final String title;
final bool isSidebarOpen;
final VoidCallback onToggleSidebar;
const Header({
super.key,
required this.title, // ✨ 필수 파라미터로 등록
required this.isSidebarOpen,
required this.onToggleSidebar,
});
@override
Widget build(BuildContext context) {
return Container(
height: 70,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: const Color(0xFF13161A),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// 사이드바 토글 메뉴 버튼
IconButton(
icon: Icon(
isSidebarOpen ? Icons.menu_open : Icons.menu,
color: const Color(0xFFB4E49C),
),
onPressed: onToggleSidebar,
),
const SizedBox(width: 8),
// ✨ 고정된 텍스트 대신 전달받은 title 변수를 출력합니다!
Text(
title,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(width: 8),
const Icon(Icons.help_outline, size: 16, color: Colors.white38),
],
),
const Icon(Icons.notifications_none, color: Colors.white70, size: 24),
],
),
);
}
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class HumidDataPage extends StatelessWidget {
const HumidDataPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'💧 실시간 토양 및 대기 습도 데이터 현황',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60),
),
);
}
}

View File

@@ -1,324 +0,0 @@
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; // 비디오 패키지 임포트
class DashboardGrid extends StatefulWidget {
final VoidCallback onCharacterTap;
const DashboardGrid({super.key, required this.onCharacterTap});
@override
State<DashboardGrid> createState() => _DashboardGridState();
}
class _DashboardGridState extends State<DashboardGrid> {
late VideoPlayerController _videoController;
bool _isVideoInitialized = false;
bool _hasVideoError = false;
@override
void initState() {
super.initState();
_initializeWebVideo();
}
void _initializeWebVideo() {
const String videoAssetPath = 'assets/assets/videos/basic.mp4';
_videoController = VideoPlayerController.asset(videoAssetPath);
_videoController.initialize().then((_) {
if (!mounted) return;
setState(() {
_isVideoInitialized = true;
});
_videoController.setLooping(true); // 🔄 무한 반복 재생
_videoController.setVolume(0.0); // 🔇 크롬 자동재생 정책 우회
_videoController.play(); // ▶️ 자동 재생 시작
}).catchError((error) {
debugPrint("1차 웹 비디오 로딩 실패, 백업 경로 시도 중...: $error");
_videoController = VideoPlayerController.asset('assets/videos/basic.mp4');
_videoController.initialize().then((_) {
if (!mounted) return;
setState(() {
_isVideoInitialized = true;
});
_videoController.setLooping(true);
_videoController.setVolume(0.0);
_videoController.play();
}).catchError((retryError) {
if (!mounted) return;
setState(() {
_hasVideoError = true;
});
debugPrint("최종 비디오 초기화 에러: $retryError");
});
});
}
@override
void dispose() {
_videoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 🤖 1. 좌측 영역: 푸미 캐릭터 비디오 화면 (✨ flex: 60 으로 수정)
Expanded(
flex: 60,
child: GestureDetector(
onTap: widget.onCharacterTap,
child: _buildCharacterCard(),
),
),
const SizedBox(width: 20),
// 📊 2. 우측 영역: 센서 데이터 모음 (✨ flex: 40 으로 수정)
Expanded(
flex: 40,
child: Column(
children: [
Expanded(
flex: 10,
child: Row(
children: [
Expanded(child: _buildSensorCard('온도', '24.5°C', Icons.thermostat, Colors.orangeAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSensorCard('습도', '65%', Icons.water_drop, Colors.blueAccent)),
],
),
),
const SizedBox(height: 16),
Expanded(
flex: 10,
child: Row(
children: [
Expanded(child: _buildSensorCard('이산화탄소', '420 ppm', Icons.cloud, Colors.greenAccent)),
const SizedBox(width: 16),
Expanded(child: _buildSensorCard('TVOC', '0.15 mg/m³', Icons.biotech, Colors.tealAccent)),
],
),
),
const SizedBox(height: 16),
Expanded(
flex: 11,
child: _buildDustCard(),
),
],
),
),
],
);
}
// 🤖 [좌측] 캐릭터 전용 카드 빌더
Widget _buildCharacterCard() {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF1F242C),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white10),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildMiniInfo(Icons.eco_outlined, '학습된 식물: 51종'),
const SizedBox(width: 20),
_buildMiniInfo(Icons.trending_up, '예측 정확도: 98.7%'),
],
),
// 🎬 중앙 로봇 에셋 비디오 플레이어
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_hasVideoError)
const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.video_library_outlined, size: 80, color: Colors.white24),
SizedBox(height: 12),
Text('비디오 에셋을 로드할 수 없습니다.', style: TextStyle(color: Colors.white38, fontSize: 13)),
],
)
else if (_isVideoInitialized)
SizedBox(
width: 600,
child: AspectRatio(
aspectRatio: _videoController.value.aspectRatio,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: VideoPlayer(_videoController),
),
),
)
else
const SizedBox(
height: 280,
child: Center(child: CircularProgressIndicator(color: Color(0xFFB4E49C))),
),
const SizedBox(height: 16),
// 안내 뱃지 문구
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFB4E49C).withOpacity(0.06),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFB4E49C).withOpacity(0.2)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.touch_app_outlined, size: 14, color: Color(0xFFB4E49C)),
SizedBox(width: 6),
Text(
'여기를 클릭하면 AI 채팅창으로 이동합니다.',
style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
),
],
),
),
],
),
),
),
const Divider(color: Colors.white10, height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('푸미', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white)),
const SizedBox(height: 4),
Row(
children: [
const Text('상태: ', style: TextStyle(color: Colors.white54, fontSize: 13)),
Text('최상 - 모니터링 중', style: TextStyle(color: const Color(0xFFB4E49C), fontSize: 13, fontWeight: FontWeight.bold)),
],
),
],
),
],
),
],
),
);
}
// 📊 센서 카드 빌더
Widget _buildSensorCard(String title, String value, IconData icon, Color accentColor) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF1F242C),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white10),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: const TextStyle(fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w500)),
Icon(icon, color: accentColor.withOpacity(0.8), size: 22),
],
),
Text(value, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)),
Container(
height: 4,
width: double.infinity,
decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(2)),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: 0.65,
child: Container(decoration: BoxDecoration(color: accentColor, borderRadius: BorderRadius.circular(2))),
),
)
],
),
);
}
// 💨 미세먼지 카드 빌더
Widget _buildDustCard() {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFF1F242C),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white10),
),
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('미세먼지', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Row(
children: [
Icon(Icons.air, color: Colors.white54, size: 36),
SizedBox(width: 16),
Text('15', style: TextStyle(fontSize: 52, fontWeight: FontWeight.bold, color: Colors.white)),
SizedBox(width: 4),
Text('µg/m³', style: TextStyle(fontSize: 14, color: Colors.white38)),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.03),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
child: const Row(
children: [
Icon(Icons.check_circle, color: Colors.greenAccent, size: 28),
SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('PM2.5 : 1.50', style: TextStyle(color: Colors.white70, fontSize: 13)),
SizedBox(height: 2),
Text('PM10 : 1.30', style: TextStyle(color: Colors.white70, fontSize: 13)),
],
)
],
),
)
],
),
const SizedBox(height: 2),
],
),
);
}
Widget _buildMiniInfo(IconData icon, String text) {
return Row(
children: [
Icon(icon, size: 14, color: const Color(0xFFB4E49C)),
const SizedBox(width: 6),
Text(text, style: const TextStyle(color: Colors.white54, fontSize: 12)),
],
);
}
}

View File

@@ -1,177 +0,0 @@
import 'package:flutter/material.dart';
class Sidebar extends StatelessWidget {
final bool isSidebarOpen;
final String selectedPage;
final Function(String) onPageChanged;
const Sidebar({
super.key,
required this.isSidebarOpen,
required this.selectedPage,
required this.onPageChanged,
});
@override
Widget build(BuildContext context) {
return Container(
width: 260,
color: const Color(0xFF181C22), // 어두운 테마 배경색 유지
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 8.0),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
curve: isSidebarOpen ? const Interval(0.6, 1.0, curve: Curves.easeIn) : Curves.easeOut,
opacity: isSidebarOpen ? 1.0 : 0.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 최상단 앱 타이틀
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
'Smart Garden',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFFB4E49C), // 페이모바일 포인트 컬러 유지
),
),
),
const SizedBox(height: 32),
// 📜 대분류 및 하위 메뉴 영역
Expanded(
child: ListView(
physics: const BouncingScrollPhysics(),
children: [
// 1. Dashboard 그룹
_buildExpansionMenu(
icon: Icons.dashboard_outlined,
title: 'Dashboard',
children: [
_buildSubMenuItem('스마트가든 챗봇'),
],
),
// 2. 데이터 조회 그룹
_buildExpansionMenu(
icon: Icons.analytics_outlined,
title: '데이터 조회',
initiallyExpanded: true, // 자주 보는 데이터 조회를 기본으로 열어둡니다.
children: [
_buildSubMenuItem('온도 데이터'),
_buildSubMenuItem('습도 데이터'),
_buildSubMenuItem('이산화탄소 데이터'),
_buildSubMenuItem('TVOC 데이터'),
_buildSubMenuItem('미세먼지 데이터'),
],
),
// 3. 장비 관리 (하위 메뉴가 없는 단일 대분류 버튼)
_buildSingleMenu(
icon: Icons.widgets_outlined,
title: '장비 관리',
),
],
),
),
// 하단 로그아웃 버튼
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFB4E49C),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Log Out', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
),
],
),
),
);
}
// ✨ 그룹형 (대분류 > 소분류) 메뉴를 만드는 빌더
Widget _buildExpansionMenu({
required IconData icon,
required String title,
required List<Widget> children,
bool initiallyExpanded = false,
}) {
return Theme(
// ExpansionTile 고유의 상하단 구분선(Border)을 제거하는 테마 트릭
data: ThemeData.dark().copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: initiallyExpanded,
leading: Icon(icon, color: Colors.white70, size: 22),
title: Text(
title,
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold),
),
trailing: const Icon(Icons.keyboard_arrow_down, color: Colors.white38, size: 18),
childrenPadding: const EdgeInsets.only(left: 16.0), // 하위 메뉴 들여쓰기 구조 설정
children: children,
),
);
}
// ✨ 하위(소분류) 메뉴 아이템 빌더
Widget _buildSubMenuItem(String title) {
bool isSelected = (title == selectedPage);
return Container(
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFB4E49C).withOpacity(0.15) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
title: Text(
title,
style: TextStyle(
color: isSelected ? const Color(0xFFB4E49C) : Colors.white60,
fontSize: 14,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
dense: true,
visualDensity: const VisualDensity(vertical: -2), // 메뉴 간격을 좀 더 콤팩트하게 조절
onTap: () => onPageChanged(title),
),
);
}
// ✨ 하위 메뉴가 없는 단독 대분류 메뉴 빌더 (ex: 장비 관리)
Widget _buildSingleMenu({required IconData icon, required String title}) {
bool isSelected = (title == selectedPage);
return Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFB4E49C).withOpacity(0.15) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: Icon(icon, color: isSelected ? const Color(0xFFB4E49C) : Colors.white70, size: 22),
title: Text(
title,
style: TextStyle(
color: isSelected ? const Color(0xFFB4E49C) : Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
dense: true,
onTap: () => onPageChanged(title),
),
);
}
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class TempDataPage extends StatelessWidget {
const TempDataPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'🌡️ 실시간 온도 데이터 수집 분석 현황',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60),
),
);
}
}

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class TvocDataPage extends StatelessWidget {
const TvocDataPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'🧪 실시간 총휘발성유기화합물(TVOC) 측정값',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60),
),
);
}
}

View File

@@ -62,21 +62,13 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/videos/angry.mp4 - assets/videos/default.mp4
- assets/videos/basic.mp4
- assets/videos/cold.mp4
- assets/videos/joy.mp4 - assets/videos/joy.mp4
- assets/videos/know.mp4
- assets/videos/thinking.mp4 - assets/videos/thinking.mp4
- assets/videos/realization.mp4
- assets/videos/angry.mp4
- assets/videos/thirst.mp4 - assets/videos/thirst.mp4
- assets/videos/cold.mp4
- assets/videos/angry_img.mp4
- assets/videos/basic_img.mp4
- assets/videos/cold_img.mp4
- assets/videos/joy_img.mp4
- assets/videos/know_img.mp4
- assets/videos/thinking_img.mp4
- assets/videos/thirst_img.mp4
- assets/images/profile.png - assets/images/profile.png
- assets/images/background.png - assets/images/background.png