324 lines
12 KiB
Dart
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)),
|
|
],
|
|
);
|
|
}
|
|
} |