Compare commits

...

14 Commits

Author SHA1 Message Date
b871ea03b0 수정 2026-06-29 09:06:04 +09:00
1140553c00 영상 크기 조절 2026-06-26 15:40:04 +09:00
c0fb4d02d5 크기 1차 수정 2026-06-26 15:14:58 +09:00
8ea5c911e7 개발서버 주소 변경 2026-06-25 17:51:36 +09:00
756453d144 readme.md 파일 수정 2026-06-25 11:39:34 +09:00
68db5b7b45 ã…감정표현 2회 재생 후 기본값으로 ë³돌 변경 2026-06-24 17:55:03 +09:00
44100e387b 감정표현, 채팅응답대기 추가 2026-06-24 15:31:06 +09:00
KIMGYEONGRAN
8465be1d06 디자인 2차 초안 2026-06-23 11:14:41 +09:00
989910a3ff 서버 연결 ã…코드 작성ì중 2026-06-19 15:39:15 +09:00
f899d52f66 디자인 1차 초안 완성 2026-06-19 11:45:24 +09:00
f418338fa1 4차 수정 2026-06-18 17:57:08 +09:00
7325b47d81 3차 수정 2026-06-18 17:11:11 +09:00
7a7d25fc9b 2차 수정 2026-06-18 15:55:33 +09:00
ac2ba4dbc2 1차 레이아웃 수정 2026-06-18 14:37:05 +09:00
36 changed files with 1377 additions and 1319 deletions

View File

@@ -1,16 +1,5 @@
# smartgarden_chat
# 스마트가든 챗봇
A new Flutter project.
## Getting Started
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.
- 플러터 서버 실행 코드
- flutter run -d web-server --web-hostname 0.0.0.0 --web-port 8080
- 예시) flutter run -d web-server --web-hostname 49.238.167.72 --web-port 8080

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

BIN
assets/images/chat_img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
assets/images/layer_img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
assets/images/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
assets/images/send.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

BIN
assets/videos/default.mp4 Normal file

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

@@ -1,109 +1,23 @@
import 'package:flutter/material.dart';
import 'package:smartgarden_chat/widgets/sidebar.dart';
import 'package:smartgarden_chat/widgets/header.dart';
import 'package:smartgarden_chat/widgets/main_dashboard.dart';
import 'screens/chat_screen.dart';
import 'package:smartgarden_chat/widgets/temp_data_page.dart';
import 'package:smartgarden_chat/widgets/humid_data_page.dart';
import 'package:smartgarden_chat/widgets/co2_data_page.dart';
import 'package:smartgarden_chat/widgets/tvoc_data_page.dart';
import 'package:smartgarden_chat/widgets/dust_data_page.dart';
import 'package:smartgarden_chat/widgets/device_manage_page.dart';
import 'package:smartgarden_chat/widgets/chatbot_detail_page.dart';
void main() => runApp(const SmartGardenApp());
void main() {
runApp(const SmartGardenApp());
}
class SmartGardenApp extends StatelessWidget {
const SmartGardenApp({super.key});
const SmartGardenApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Smart Garden',
theme: ThemeData(
primarySwatch: Colors.green,
useMaterial3: true,
),
home: const ChatScreen(),
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: const Color(0xFF0F1115)),
home: const MainDashboardScreen(),
);
}
}
class MainDashboardScreen extends StatefulWidget {
const MainDashboardScreen({super.key});
@override
State<MainDashboardScreen> createState() => _MainDashboardScreenState();
}
class _MainDashboardScreenState extends State<MainDashboardScreen> {
// ✨ 초기값을 false로 설정하여 앱 시작 시 사이드바가 숨겨진 상태로 만듭니다.
bool isSidebarOpen = false;
String selectedPage = '스마트가든 챗봇';
bool isChatbotDetail = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
width: isSidebarOpen ? 260 : 0,
child: Sidebar(
isSidebarOpen: isSidebarOpen,
selectedPage: selectedPage,
onPageChanged: (page) {
setState(() {
selectedPage = page;
isChatbotDetail = false; // 다른 메뉴 클릭 시 상세 뷰 초기화
});
},
),
),
Expanded(
child: Column(
children: [
Header(
title: isChatbotDetail ? '$selectedPage - 상세' : selectedPage,
isSidebarOpen: isSidebarOpen,
onToggleSidebar: () => setState(() => isSidebarOpen = !isSidebarOpen),
),
Expanded(
child: Container(
padding: const EdgeInsets.all(24.0),
color: const Color(0xFF13161A),
child: _buildBody(), // ✨ 개인 위젯 호출 통로
),
),
],
),
),
],
),
);
}
// ✨ 질문자님이 정의하신 아코디언 메뉴명과 개인 위젯 매핑 분기점
Widget _buildBody() {
switch (selectedPage) {
case '스마트가든 챗봇':
return isChatbotDetail
? ChatbotDetailPage(onBack: () => setState(() => isChatbotDetail = false))
: DashboardGrid(onCharacterTap: () => setState(() => isChatbotDetail = true));
case '온도 데이터':
return const TempDataPage();
case '습도 데이터':
return const HumidDataPage();
case '이산화탄소 데이터':
return const Co2DataPage();
case 'TVOC 데이터':
return const TvocDataPage();
case '미세먼지 데이터':
return const DustDataPage();
case '장비 관리':
return const DeviceManagePage();
default:
return isChatbotDetail
? ChatbotDetailPage(onBack: () => setState(() => isChatbotDetail = false))
: DashboardGrid(onCharacterTap: () => setState(() => isChatbotDetail = true));
}
}
}

View 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

File diff suppressed because it is too large Load Diff

View 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;
}

View File

@@ -1,107 +0,0 @@
import 'dart:async';
import 'dart:math';
/// 📌 [독립 함수] 지난 24시간 치의 가상(더미) 온도 데이터 리스트를 생성한다.
///
/// 실제 DB 연동 전, 개발 단계에서 차트/리스트 UI를 채우기 위한 용도입니다.
/// 값이 들쭉날쭉 튀지 않고 자연스럽게 보이도록, 직전 값에서 조금씩만
/// 변하는 "랜덤 워크(random walk)" 방식으로 만듭니다.
///
/// - [count] : 만들 데이터 개수 (기본 24개 = 1시간 간격 24시간)
/// - [minTemp] : 가상 온도 하한
/// - [maxTemp] : 가상 온도 상한
List<double> generateDummyTemperatures({
int count = 24,
double minTemp = 18.0,
double maxTemp = 28.0,
}) {
final random = Random();
final list = <double>[];
// 시작 온도는 범위 내 임의 값
double last = minTemp + random.nextDouble() * (maxTemp - minTemp);
for (int i = 0; i < count; i++) {
// 직전 값에서 -0.5 ~ +0.5°C 내외로 흔들기 (clamp는 num 반환 → toDouble)
last = (last + (random.nextDouble() - 0.5)).clamp(minTemp, maxTemp).toDouble();
list.add(double.parse(last.toStringAsFixed(1))); // 소수점 1자리로 정리
}
return list;
}
/// 📡 [스트림 서비스] 앱이 켜져 있는 동안 일정 주기마다
/// 새로운 가상 온도 데이터를 계속 추가하며 갱신된 리스트를 흘려보낸다.
///
/// 사용 흐름:
/// final service = TempDataService()..start();
/// service.stream.listen((temps) { ... }); // StreamBuilder로 구독
/// service.dispose(); // 화면이 사라질 때 반드시 호출
///
/// 👉 나중에 실제 DB 연동 시에는 [_generateNextTemperature] 내부만
/// "DB에서 최신 온도 1건 조회" 로직으로 교체하면 됩니다.
class TempDataService {
TempDataService({
this.minTemp = 18.0,
this.maxTemp = 28.0,
this.historyCount = 24,
this.interval = const Duration(seconds: 3), // ⏱️ 테스트용 3초
this.maxLength = 200, // 메모리 무한 증가 방지용 보관 한도
});
final double minTemp; // 최저 가상 온도
final double maxTemp; // 최고 가상 온도
final int historyCount; // 시작 시 채워둘 24시간 데이터 개수
final Duration interval; // 새 데이터 추가 주기
final int maxLength; // 리스트에 보관할 최대 개수
final _random = Random();
final List<double> _temperatures = [];
Timer? _timer;
// broadcast: 여러 위젯이 동시에 구독할 수 있게 함
final _controller = StreamController<List<double>>.broadcast();
/// 갱신될 때마다 "전체 온도 리스트"가 흘러나오는 스트림
Stream<List<double>> get stream => _controller.stream;
/// 현재까지 쌓인 데이터 (읽기 전용 복사본)
List<double> get current => List.unmodifiable(_temperatures);
/// 스트림 시작: 24시간 더미 데이터로 시드한 뒤, [interval]마다 새 값 추가
void start() {
_temperatures
..clear()
..addAll(generateDummyTemperatures(
count: historyCount,
minTemp: minTemp,
maxTemp: maxTemp,
));
_controller.add(current); // 초기 데이터를 즉시 한 번 전달
_timer?.cancel();
_timer = Timer.periodic(interval, (_) {
_temperatures.add(_generateNextTemperature());
// 오래된 데이터부터 버려서 길이 유지
while (_temperatures.length > maxLength) {
_temperatures.removeAt(0);
}
_controller.add(current);
});
}
/// 다음(새) 가상 온도 한 건을 생성한다. 직전 값 기준 랜덤 워크.
double _generateNextTemperature() {
final last = _temperatures.isEmpty
? minTemp + _random.nextDouble() * (maxTemp - minTemp)
: _temperatures.last;
final next = (last + (_random.nextDouble() - 0.5)).clamp(minTemp, maxTemp);
return double.parse(next.toStringAsFixed(1));
}
/// 타이머 정지 + 스트림 정리. 위젯 dispose() 에서 꼭 호출할 것.
void dispose() {
_timer?.cancel();
_controller.close();
}
}

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

@@ -112,6 +112,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -237,6 +253,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

@@ -28,6 +28,8 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
http: ^1.1.0
flutter:
sdk: flutter
@@ -60,13 +62,20 @@ flutter:
uses-material-design: true
assets:
- assets/videos/angry.mp4
- assets/videos/basic.mp4
- assets/videos/cold.mp4
- assets/videos/default.mp4
- assets/videos/joy.mp4
- assets/videos/know.mp4
- assets/videos/thinking.mp4
- assets/videos/realization.mp4
- assets/videos/angry.mp4
- assets/videos/thirst.mp4
- assets/videos/cold.mp4
- assets/videos/shy.mp4
- assets/images/profile.png
- assets/images/background.png
- assets/images/layer_img.png
- assets/images/chat_img.png
- assets/images/send.png
# To add assets to your application, add an assets section, like this:
# assets:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

BIN
web/icons/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -27,7 +27,7 @@
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="icon" type="image/png" href="icons/favicon.png"/>
<title>smartgarden_chat</title>
<link rel="manifest" href="manifest.json">