Files
smartgarden_chat/lib/widgets/main_dashboard.dart
2026-06-15 18:12:31 +09:00

324 lines
12 KiB
Dart

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)),
],
);
}
}