Compare commits
10 Commits
f418338fa1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b871ea03b0 | |||
| 1140553c00 | |||
| c0fb4d02d5 | |||
| 8ea5c911e7 | |||
| 756453d144 | |||
| 68db5b7b45 | |||
| 44100e387b | |||
|
|
8465be1d06 | ||
| 989910a3ff | |||
| f899d52f66 |
19
README.md
@@ -1,16 +1,5 @@
|
|||||||
# smartgarden_chat
|
# 스마트가든 챗봇
|
||||||
|
|
||||||
A new Flutter project.
|
- 플러터 서버 실행 코드
|
||||||
|
- flutter run -d web-server --web-hostname 0.0.0.0 --web-port 8080
|
||||||
## Getting Started
|
- 예시) flutter run -d web-server --web-hostname 49.238.167.72 --web-port 8080
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
|
||||||
|
|
||||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
|
||||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 548 KiB |
|
Before Width: | Height: | Size: 958 KiB |
|
Before Width: | Height: | Size: 814 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
BIN
assets/images/chat_img.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/layer_img.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
assets/images/send.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
760
lib/main.dart
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'screens/chat_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const SmartGardenApp());
|
runApp(const SmartGardenApp());
|
||||||
@@ -12,760 +12,12 @@ class SmartGardenApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Smart Garden',
|
title: 'Smart Garden',
|
||||||
theme: ThemeData(primarySwatch: Colors.green, useMaterial3: true),
|
theme: ThemeData(
|
||||||
home: const SmartGardenScreen(),
|
primarySwatch: Colors.green,
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: const ChatScreen(),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessage {
|
|
||||||
final String text;
|
|
||||||
final bool isUser;
|
|
||||||
final DateTime timestamp;
|
|
||||||
|
|
||||||
ChatMessage({
|
|
||||||
required this.text,
|
|
||||||
required this.isUser,
|
|
||||||
required this.timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class SmartGardenScreen extends StatefulWidget {
|
|
||||||
const SmartGardenScreen({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<SmartGardenScreen> createState() => _SmartGardenScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SmartGardenScreenState extends State<SmartGardenScreen> {
|
|
||||||
final TextEditingController _textController = TextEditingController();
|
|
||||||
final List<ChatMessage> _messages = [];
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
|
|
||||||
late VideoPlayerController _videoController;
|
|
||||||
bool _isVideoInitialized = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_initializeVideo();
|
|
||||||
_addInitialMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeVideo() {
|
|
||||||
_videoController =
|
|
||||||
VideoPlayerController.asset('assets/videos/basic_img.mp4')
|
|
||||||
..initialize()
|
|
||||||
.then((_) {
|
|
||||||
_videoController.setLooping(true);
|
|
||||||
_videoController.setVolume(0.0);
|
|
||||||
_videoController.play();
|
|
||||||
setState(() {
|
|
||||||
_isVideoInitialized = true;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catchError((error) {
|
|
||||||
print('비디오 로드 오류: $error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addInitialMessage() {
|
|
||||||
setState(() {
|
|
||||||
_messages.add(
|
|
||||||
ChatMessage(
|
|
||||||
text: '상태 분석이 완료되었습니다.\n무엇을 도와드릴까요?',
|
|
||||||
isUser: false,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendMessage(String text) {
|
|
||||||
if (text.isEmpty) return;
|
|
||||||
|
|
||||||
// 사용자 메시지 추가
|
|
||||||
setState(() {
|
|
||||||
_messages.add(
|
|
||||||
ChatMessage(text: text, isUser: true, timestamp: DateTime.now()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
_textController.clear();
|
|
||||||
_scrollToBottom();
|
|
||||||
|
|
||||||
// 봇 응답 (0.5초 딜레이)
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
|
||||||
setState(() {
|
|
||||||
_messages.add(
|
|
||||||
ChatMessage(
|
|
||||||
text: '안녕하세요 스마트가든 AI 가이드 푸미입니다',
|
|
||||||
isUser: false,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
_scrollToBottom();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scrollToBottom() {
|
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
|
||||||
_scrollController.animateTo(
|
|
||||||
_scrollController.position.maxScrollExtent,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sendQuickQuestion(String question) {
|
|
||||||
_textController.text = question;
|
|
||||||
_sendMessage(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_textController.dispose();
|
|
||||||
_scrollController.dispose();
|
|
||||||
_videoController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
body: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage('assets/images/background1.png'),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.all(40),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: Color(0xFFE0E0E0), width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 좌측 캐릭터 영역 (60%)
|
|
||||||
Expanded(
|
|
||||||
flex: 6,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: Color(0xFFE0E0E0), width: 1),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 헤더
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(30),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFC8E6C9),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Text(
|
|
||||||
'🌱',
|
|
||||||
style: TextStyle(fontSize: 28),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Smart Garden',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF212121),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'스마트가든 챗봇',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Color(0xFF757575),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 캐릭터 영역
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFF5F5F5),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: _isVideoInitialized
|
|
||||||
? ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio:
|
|
||||||
_videoController.value.aspectRatio,
|
|
||||||
child: VideoPlayer(_videoController),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 180,
|
|
||||||
height: 180,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFF1F8E9),
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
12,
|
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFFC8E6C9),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color: Color(0xFF81C784),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
'비디오 로딩 중...',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Color(0xFF757575),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 40),
|
|
||||||
// 캐릭터 정보 + 설명 문구
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: 30,
|
|
||||||
bottom: 30,
|
|
||||||
right: 30,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 푸미 정보
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFC8E6C9),
|
|
||||||
// ← 연한 초록 배경 추가
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/images/profile.png',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'푸미',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF212121),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'스마트가든 AI 가이드',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Color(0xFF757575),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// 설명 문구 (반응형)
|
|
||||||
Container(
|
|
||||||
width: double.infinity, // ← 추가: 양쪽 꽉차게
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFF1F8E9),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFFC8E6C9),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'안녕하세요!\n스마트가든의 식물들이 건강하게 자랄 수 있도록 도와드릴게요.\n궁금한 점을 언제든 물어보세요!',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF558B2F),
|
|
||||||
height: 1.6,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 우측 채팅 영역 (40%)
|
|
||||||
Expanded(
|
|
||||||
flex: 4,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFFAFAFA),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: Color(0xFFE0E0E0), width: 1),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 헤더 (왼쪽정렬)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(16),
|
|
||||||
topRight: Radius.circular(16),
|
|
||||||
),
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Color(0xFFE0E0E0),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'푸미와 대화하기',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 22,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF212121),
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.left, // ← 왼쪽정렬
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 채팅 메시지 영역
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFFE0E0E0),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: _scrollController,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: _messages.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final message = _messages[index];
|
|
||||||
return _ChatBubble(message: message);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 자주하는 질문 버튼들 (채팅 아래, 입력창 위)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 온도 정보
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFBBDEFB),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFF1976D2).withOpacity(0.2),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () =>
|
|
||||||
_sendQuickQuestion('현재 온도는?'),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'🌡️',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'온도 정보',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF1976D2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
// 습도 정보
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFC8E6C9),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFF388E3C).withOpacity(0.2),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () =>
|
|
||||||
_sendQuickQuestion('현재 습도는?'),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'💧',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'습도 정보',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF388E3C),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
// 물 주기
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFFFE0B2),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFFFFA000).withOpacity(0.2),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _sendQuickQuestion('물을 주세요'),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'💦',
|
|
||||||
style: TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'물 주기',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFFFFA000),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 입력 필드 + 전송 버튼 (같은 줄)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 입력 필드
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Color(0xFFE0E0E0),
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
controller: _textController,
|
|
||||||
onSubmitted: (value) {
|
|
||||||
_sendMessage(
|
|
||||||
_textController.text,
|
|
||||||
); // 엔터로 전송
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: '메시지를 입력하세요...',
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: Color(0xFF9E9E9E),
|
|
||||||
),
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding:
|
|
||||||
const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
style: const TextStyle(fontSize: 20),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
// 전송 버튼
|
|
||||||
SizedBox(
|
|
||||||
height: 48,
|
|
||||||
width: 60,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
_sendMessage(_textController.text);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Color(0xFF81C784),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'전송',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatBubble extends StatelessWidget {
|
|
||||||
final ChatMessage message;
|
|
||||||
|
|
||||||
const _ChatBubble({required this.message});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: message.isUser
|
|
||||||
? MainAxisAlignment.end
|
|
||||||
: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (!message.isUser) ...[
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFFC8E6C9), // ← 연한 초록 배경
|
|
||||||
borderRadius: BorderRadius.circular(20), // ← 원형 (8 → 20)
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(20), // ← 원형 (8 → 20)
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/images/profile.png',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
],
|
|
||||||
Flexible(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: message.isUser
|
|
||||||
? Color(0xFFE8F5E9) // ← 연한 녹색
|
|
||||||
: Colors.white, // ← 하얀색
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.1), // ← 연한 그림자
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: SelectableText(
|
|
||||||
message.text,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: message.isUser
|
|
||||||
? Color(0xFF000000) // ← 진한 초록 텍스트
|
|
||||||
: Color(0xFF424242),
|
|
||||||
height: 1.4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (message.isUser) const SizedBox(width: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _QuickButton extends StatelessWidget {
|
|
||||||
final String emoji;
|
|
||||||
final String title;
|
|
||||||
final String subtitle;
|
|
||||||
final Color bgColor;
|
|
||||||
final Color textColor;
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
|
|
||||||
const _QuickButton({
|
|
||||||
required this.emoji,
|
|
||||||
required this.title,
|
|
||||||
required this.subtitle,
|
|
||||||
required this.bgColor,
|
|
||||||
required this.textColor,
|
|
||||||
required this.onPressed,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: onPressed,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: bgColor,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: textColor.withOpacity(0.2), width: 0.5),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(emoji, style: const TextStyle(fontSize: 18)),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: textColor,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
if (subtitle.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
subtitle,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: textColor.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
33
lib/models/chat_message.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
class ChatMessage {
|
||||||
|
final String text;
|
||||||
|
final bool isUser;
|
||||||
|
final DateTime timestamp;
|
||||||
|
final int? emotionCategory; // ← 새로 추가 (봇 메시지의 감정)
|
||||||
|
|
||||||
|
ChatMessage({
|
||||||
|
required this.text,
|
||||||
|
required this.isUser,
|
||||||
|
required this.timestamp,
|
||||||
|
this.emotionCategory,
|
||||||
|
});
|
||||||
|
|
||||||
|
// JSON에서 ChatMessage로 변환
|
||||||
|
factory ChatMessage.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ChatMessage(
|
||||||
|
text: json['text'] as String,
|
||||||
|
isUser: json['isUser'] as bool,
|
||||||
|
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||||
|
emotionCategory: json['emotionCategory'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatMessage를 JSON으로 변환
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'text': text,
|
||||||
|
'isUser': isUser,
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
'emotionCategory': emotionCategory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
1122
lib/screens/chat_screen.dart
Normal file
167
lib/services/chat_service.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import '../models/chat_message.dart';
|
||||||
|
|
||||||
|
class ChatService {
|
||||||
|
// 서버 URL
|
||||||
|
static const String serverUrl = 'http://49.238.167.122:8000/chat/';
|
||||||
|
|
||||||
|
// 타임아웃 설정 (초)
|
||||||
|
static const int timeoutSeconds = 30;
|
||||||
|
|
||||||
|
/// 사용자 메시지를 서버로 전송하고 응답을 받는 메서드
|
||||||
|
/// 반환값: (메시지, emotion_category) 튜플
|
||||||
|
static Future<(String, int)> sendMessage(String userMessage) async {
|
||||||
|
try {
|
||||||
|
// 요청 생성
|
||||||
|
final requestBody = {
|
||||||
|
'device_id': 1,
|
||||||
|
'locale': 'ko-KR',
|
||||||
|
'message': userMessage,
|
||||||
|
'session_id': 'session-001',
|
||||||
|
'user_id': 'user-123',
|
||||||
|
};
|
||||||
|
|
||||||
|
print('서버로 요청 전송: $requestBody');
|
||||||
|
|
||||||
|
// POST 요청 전송
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse(serverUrl),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: jsonEncode(requestBody),
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: timeoutSeconds),
|
||||||
|
onTimeout: () => throw TimeoutException('서버 응답 시간 초과'),
|
||||||
|
);
|
||||||
|
|
||||||
|
print('서버 응답 상태코드: ${response.statusCode}');
|
||||||
|
print('서버 응답 본문: ${response.body}');
|
||||||
|
|
||||||
|
// 상태 코드 확인
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
// 성공
|
||||||
|
final jsonResponse = jsonDecode(response.body);
|
||||||
|
|
||||||
|
// 응답에서 'message' 필드 추출
|
||||||
|
final responseMessage = jsonResponse['message'] as String?;
|
||||||
|
|
||||||
|
// 응답에서 'emotion_category' 필드 추출 (기본값: 1)
|
||||||
|
final emotionCategory = jsonResponse['emotion_category'] as int? ?? 1;
|
||||||
|
|
||||||
|
if (responseMessage != null && responseMessage.isNotEmpty) {
|
||||||
|
print('응답 메시지: $responseMessage');
|
||||||
|
print('감정 카테고리: $emotionCategory');
|
||||||
|
return (responseMessage, emotionCategory);
|
||||||
|
} else {
|
||||||
|
print('응답이 비어있습니다');
|
||||||
|
return ('응답을 받지 못했습니다', 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 실패
|
||||||
|
print('서버 오류: ${response.statusCode}');
|
||||||
|
return ('서버 오류 (${response.statusCode})', 1);
|
||||||
|
}
|
||||||
|
} on TimeoutException catch (e) {
|
||||||
|
print('타임아웃: $e');
|
||||||
|
return ('서버 응답이 없습니다. 시간 초과', 1);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
print('JSON 파싱 오류: $e');
|
||||||
|
return ('JSON 형식 오류', 1);
|
||||||
|
} catch (e) {
|
||||||
|
print('오류 발생: $e');
|
||||||
|
return ('오류가 발생했습니다: $e', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 서버 연결 테스트
|
||||||
|
static Future<bool> testConnection() async {
|
||||||
|
try {
|
||||||
|
print('서버 연결 테스트 중...');
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(serverUrl),
|
||||||
|
).timeout(
|
||||||
|
const Duration(seconds: timeoutSeconds),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 405) {
|
||||||
|
print('서버 연결 성공');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
print('서버 연결 실패: ${response.statusCode}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('서버 연결 오류: $e');
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 타임아웃 예외
|
||||||
|
class TimeoutException implements Exception {
|
||||||
|
final String message;
|
||||||
|
TimeoutException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
@@ -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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
pubspec.lock
@@ -112,6 +112,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.6"
|
version: "0.15.6"
|
||||||
|
http:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -237,6 +253,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.6"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
25
pubspec.yaml
@@ -28,6 +28,8 @@ environment:
|
|||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
# versions available, run `flutter pub outdated`.
|
# versions available, run `flutter pub outdated`.
|
||||||
dependencies:
|
dependencies:
|
||||||
|
http: ^1.1.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
@@ -60,27 +62,20 @@ 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/shy.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
|
||||||
- assets/images/background1.png
|
- assets/images/layer_img.png
|
||||||
- assets/images/background2.png
|
- assets/images/chat_img.png
|
||||||
- assets/images/background3.png
|
- assets/images/send.png
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
|
|||||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 917 B |
BIN
web/icons/favicon.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
@@ -27,7 +27,7 @@
|
|||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="icons/favicon.png"/>
|
||||||
|
|
||||||
<title>smartgarden_chat</title>
|
<title>smartgarden_chat</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|||||||