Files
smarthelmet_app/lib/main.dart
2025-10-02 18:35:35 +09:00

833 lines
27 KiB
Dart

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<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
static const double _uniformGap = 16.0;
final Map<String, bool> _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<bool> 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;
}
}