import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'dart:ui' as ui; import 'dart:math' as math; void main() { runApp(const SmartHelmetApp()); } class SmartHelmetApp extends StatelessWidget { const SmartHelmetApp({super.key}); @override Widget build(BuildContext context) { SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, )); return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( brightness: Brightness.dark, scaffoldBackgroundColor: const Color(0xFF27292B), primaryColor: const Color(0xFF30343B), fontFamily: 'Pretendard', textTheme: const TextTheme( bodyLarge: TextStyle(color: Colors.white, fontWeight: FontWeight.w500), bodyMedium: TextStyle(color: Colors.white70, fontWeight: FontWeight.w400), ), ), home: const HomeScreen(), ); } } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { int _selectedIndex = 0; static const double _uniformGap = 16.0; final Map _controlToggles = { 'UV LED': false, 'CHARGING': true, 'HELMET': true, 'FAN': false, }; @override Widget build(BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear(1.0), ), child: Scaffold( body: SafeArea( bottom: false, child: Column( children: [ _buildCustomHeader(), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: _uniformGap), child: Column( children: [ const SizedBox(height: _uniformGap), _buildOverviewSection(), const SizedBox(height: _uniformGap), _buildBatteryStatusCard(), const SizedBox(height: _uniformGap), _buildControlCard(), const SizedBox(height: _uniformGap), _buildEnvironmentSensorsCard(), const SizedBox(height: _uniformGap), _buildMyLocationCard(), const SizedBox(height: _uniformGap), _buildActivityCard(), const SizedBox(height: _uniformGap * 2), ], ), ), ), ], ), ), bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, onTap: (index) { setState(() { _selectedIndex = index; }); }, type: BottomNavigationBarType.fixed, backgroundColor: const Color(0xFF1C1C1E), elevation: 0, selectedItemColor: Colors.white, unselectedItemColor: Colors.grey[600], showUnselectedLabels: true, selectedFontSize: 12, unselectedFontSize: 12, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'HOME'), BottomNavigationBarItem( icon: Icon(Icons.tune), label: 'CONTROL'), BottomNavigationBarItem( icon: Icon(Icons.location_on), label: 'LOCATION'), BottomNavigationBarItem( icon: Icon(Icons.history), label: 'HISTORY'), ], ), ), ); } Widget _buildCustomHeader() { return Container( height: 60.0, color: Theme.of(context).scaffoldBackgroundColor, padding: const EdgeInsets.symmetric(horizontal: _uniformGap), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ Row(children: [ Icon(Icons.settings_input_component, color: Colors.grey[400]), const SizedBox(width: 12), const Text('SMART HELMET SYSTEMS', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Colors.white)) ]), Text('2025/09/26 - 10:44 AM', style: TextStyle(color: Colors.grey[400], fontSize: 11)) ])); } Widget _buildOverviewSection() { return Card( child: Column( children: [ _buildOverviewHeader(), Padding( padding: const EdgeInsets.all(12.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _buildImageCard()), const SizedBox(width: 12), Expanded(child: _buildInfoCard()), ], ), ), ], ), ); } Widget _buildOverviewHeader() { return Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 0), child: Row( children: [ const Text('SYSTEM OVERVIEW', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)), const Spacer(), Icon(Icons.search, color: Colors.grey[400], size: 20), const SizedBox(width: 8), Icon(Icons.notifications_outlined, color: Colors.grey[400], size: 20), ], ), ); } Widget _buildImageCard() { return AspectRatio( aspectRatio: 1.0, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.3), borderRadius: BorderRadius.circular(10), ), child: Stack( alignment: Alignment.center, children: [ Container( decoration: BoxDecoration( color: Colors.grey[850], borderRadius: BorderRadius.circular(5) ), ), Padding( padding: const EdgeInsets.only(bottom: 20.0), child: Image.asset( 'assets/images/helmet.png', width: 100, ), ), Positioned( bottom: 12, child: Row( children: [ _buildLedIndicator(Colors.grey.shade700), const SizedBox(width: 4), _buildLedIndicator(Colors.grey.shade700), const SizedBox(width: 4), _buildLedIndicator(Colors.grey.shade700), const SizedBox(width: 4), _buildLedIndicator(Colors.white), ], ), ) ], ), ), ); } Widget _buildLedIndicator(Color color) { return Container( width: 18, height: 6, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(3), ), ); } Widget _buildInfoCard() { const Color accentColor = Color(0xFFFF9500); return SizedBox( height: 160, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildInfoRow('Name / \nNumber', const Icon(Icons.person, color: Colors.white, size: 20), 'USER', '001'), const SizedBox(height: 8), _buildInfoRow('STATUS', null, 'UNLOCKED', '● ACTIVE', value1Color: accentColor, value2Color: accentColor), ], ), ); } Widget _buildInfoRow(String title, Widget? icon, String value1, String value2, {Color? value1Color, Color? value2Color}) { return Expanded( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.3), borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Expanded( flex: 2, child: Text(title, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey[400], fontSize: 11, height: 1.4)), ), VerticalDivider(color: Colors.grey[700], indent: 10, endIndent: 10), Expanded( flex: 3, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[icon, const SizedBox(height: 4)], Text(value1, style: TextStyle(color: value1Color ?? Colors.white, fontWeight: FontWeight.bold, fontSize: 12)), const SizedBox(height: 2), Text(value2, style: TextStyle(color: value2Color ?? Colors.white, fontWeight: FontWeight.w500, fontSize: 12)), ], ), ), ], ), ), ); } Widget _buildBatteryStatusCard() { const Color accentColor = Color(0xFFFF9500); return Card( child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('BATTERY STATUS (%)', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)), const SizedBox(height: 16), Row( children: [ SizedBox( width: 80, height: 80, child: Stack( alignment: Alignment.center, children: [ SizedBox.expand( child: CustomPaint( painter: _BatteryArcPainter( backgroundColor: Colors.grey.shade800, color: Colors.white, percentage: 1.0, ), ), ), const Text('86', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600)), ], ), ), const SizedBox(width: 20), Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('NOW', style: TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)), const Text('사용 중', style: TextStyle( color: accentColor, fontSize: 14, fontWeight: FontWeight.bold)), ], ), const Divider( color: Color(0xFF555555), height: 20, thickness: 1, ), Row( children: [ const Expanded( flex: 4, child: Text('Solar Panel', style: TextStyle( color: Colors.white, fontSize: 14)), ), Expanded( flex: 5, child: Row( children: [ const Expanded( child: Text('전압: 00', style: TextStyle( color: Colors.white70, fontSize: 14)), ), SizedBox( height: 20, child: VerticalDivider( color: Colors.grey[700], thickness: 1, ), ), const Expanded( child: Padding( padding: EdgeInsets.only(left: 8.0), child: Text('전류: 00', style: TextStyle( color: Colors.white70, fontSize: 14)), ), ), ], ), ), ], ), ], ), ), ], ), ], ), ), ); } Widget _buildControlCard() { return Card( child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('CONTROL', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)), const SizedBox(height: 16), SizedBox( height: 70, child: Row( children: [ Expanded( child: _buildStyledToggleSwitch( 'UV LED', _controlToggles['UV LED']!, (val) => setState(() => _controlToggles['UV LED'] = val))), VerticalDivider( color: Colors.grey[700], indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( 'CHARGING', _controlToggles['CHARGING']!, (val) => setState(() => _controlToggles['CHARGING'] = val))), VerticalDivider( color: Colors.grey[700], indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( 'HELMET', _controlToggles['HELMET']!, (val) => setState(() => _controlToggles['HELMET'] = val))), VerticalDivider( color: Colors.grey[700], indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch('FAN', _controlToggles['FAN']!, (val) => setState(() => _controlToggles['FAN'] = val))), ], ), ) ], ), ), ); } Widget _buildStyledToggleSwitch( String title, bool value, ValueChanged onChanged) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)), const SizedBox(height: 12), GestureDetector( onTap: () => onChanged(!value), child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, width: 60, height: 30, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: value ? Colors.white : Colors.grey.shade700, ), child: Stack( children: [ AnimatedAlign( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, alignment: value ? Alignment.centerRight : Alignment.centerLeft, child: Padding( padding: const EdgeInsets.all(2.0), child: Container( width: 26, height: 26, decoration: BoxDecoration( shape: BoxShape.circle, color: value ? Theme.of(context).primaryColor : Colors.white, ), ), ), ), Row( children: [ Expanded( child: Center( child: Text('ON', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: value ? Theme.of(context).primaryColor : Colors.transparent)))), Expanded( child: Center( child: Text('OFF', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: value ? Colors.transparent : Colors.white)))), ], ) ], ), ), ), ], ); } Widget _buildEnvironmentSensorsCard() { return Card( child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('ENVIRONMENT SENSORS', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), const Spacer(), InkWell( onTap: () {}, child: Row( children: [ Text('VIEW HISTORY', style: TextStyle(color: Colors.grey[400], fontSize: 10)), const SizedBox(width: 4), Icon(Icons.arrow_forward_ios, size: 10, color: Colors.grey[400]), ], ), ), ], ), const SizedBox(height: 20), Row( children: [ Expanded( flex: 1, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSensorInfoRow(Icons.water_drop_outlined, 'HUMID: 60%'), const SizedBox(height: 24), _buildSensorInfoRow(Icons.thermostat, 'TEMP: 24.5℃'), ], ), ), const SizedBox(width: 16), Expanded( flex: 1, child: SizedBox( height: 60, child: const _LineChartPlaceholder())), ], ), ], ), ), ); } Widget _buildSensorInfoRow(IconData icon, String text) { return Row(children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: Colors.black, size: 24), ), const SizedBox(width: 12), Text(text, style: const TextStyle( fontSize: 14, color: Colors.white, fontWeight: FontWeight.w600)) ]); } Widget _buildMyLocationCard() { final LatLng exampleLocation = LatLng(37.5665, 126.9780); return Card( clipBehavior: Clip.antiAlias, child: SizedBox( height: 200.0, child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('My Location', style: TextStyle( fontSize: 13, color: Colors.white, fontWeight: FontWeight.bold)), SizedBox(height: 4), Text('주소: 남구 효덕로 277', style: TextStyle(fontSize: 11, color: Colors.white70)), ], ), InkWell( onTap: () {}, child: Row( children: [ Text('VIEW MORE', style: TextStyle(color: Colors.grey[400], fontSize: 9)), const SizedBox(width: 4), Icon(Icons.arrow_forward_ios, size: 10, color: Colors.grey[400]), ], ), ), ], ), ), Expanded( child: FlutterMap( options: MapOptions( initialCenter: exampleLocation, initialZoom: 15.0, interactionOptions: const InteractionOptions(flags: InteractiveFlag.none), ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], ), MarkerLayer( markers: [ Marker( point: exampleLocation, width: 80, height: 80, child: const Icon(Icons.location_pin, size: 40, color: Colors.white), ), ], ), ], ), ), ], ), ), ); } Widget _buildActivityCard() { return Card( child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Activity', style: TextStyle( fontSize: 13, color: Colors.white, fontWeight: FontWeight.bold)), const SizedBox(height: 12), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _activityText('08:15 AM - Battery fully Charged'), const SizedBox(height: 8), _activityText('9:30 AM - UV LED Actived'), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _activityText('10:45 AM - Helmet Unlocked'), const SizedBox(height: 8), _activityText('11:00 AM - Helmet Off'), ], ), ), ], ), ], ), ), ); } Widget _activityText(String text) { return Text(text, style: const TextStyle(fontSize: 11, color: Colors.white70)); } } class _LineChartPlaceholder extends StatelessWidget { const _LineChartPlaceholder(); @override Widget build(BuildContext context) { return Column(children: [ Expanded( child: CustomPaint(painter: _LineChartPainter(), size: Size.infinite)), const SizedBox(height: 4), const Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('24H AGO', style: TextStyle(fontSize: 8, color: Colors.white54)), Text('12H AGO', style: TextStyle(fontSize: 8, color: Colors.white54)), Text('NOW', style: TextStyle(fontSize: 8, color: Colors.white54)) ]) ]); } } class _LineChartPainter extends CustomPainter { @override void paint(ui.Canvas canvas, ui.Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.8) ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; final path = ui.Path(); path.moveTo(0, size.height * 0.6); path.cubicTo(size.width * 0.1, size.height * 0.8, size.width * 0.2, size.height * 0.4, size.width * 0.3, size.height * 0.6); path.cubicTo(size.width * 0.4, size.height * 0.8, size.width * 0.45, size.height * 0.2, size.width * 0.6, size.height * 0.5); path.cubicTo(size.width * 0.75, size.height * 0.8, size.width * 0.8, size.height * 0.3, size.width, size.height * 0.2); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } } class Card extends StatelessWidget { final Widget child; final EdgeInsetsGeometry? padding; final Clip clipBehavior; const Card({ super.key, required this.child, this.padding, this.clipBehavior = Clip.none, }); @override Widget build(BuildContext context) { return Container( clipBehavior: clipBehavior, decoration: BoxDecoration( color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(12), ), child: child, ); } } class _BatteryArcPainter extends CustomPainter { final Color backgroundColor; final Color color; final double percentage; _BatteryArcPainter({ required this.backgroundColor, required this.color, required this.percentage, }); @override void paint(Canvas canvas, Size size) { final Paint backgroundPaint = Paint() ..color = backgroundColor ..strokeWidth = 8 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final Paint foregroundPaint = Paint() ..color = color ..strokeWidth = 8 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final Rect rect = Rect.fromLTWH(0, 0, size.width, size.height); const double startAngle = -2.35; const double sweepAngle = 4.7; canvas.drawArc(rect, startAngle, sweepAngle, false, backgroundPaint); final double progressAngle = sweepAngle * percentage; canvas.drawArc(rect, startAngle, progressAngle, false, foregroundPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } }