794 lines
26 KiB
Dart
794 lines
26 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'dart:ui' as ui;
|
|
import 'package:smarthelmet_app/widgets/custom_header.dart';
|
|
|
|
//25.12.03 지은 추가
|
|
import 'package:smarthelmet_app/services/locker_api.dart';
|
|
|
|
class HomeScreenContent extends StatefulWidget {
|
|
const HomeScreenContent({super.key});
|
|
|
|
@override
|
|
State<HomeScreenContent> createState() => _HomeScreenContentState();
|
|
}
|
|
|
|
class _HomeScreenContentState extends State<HomeScreenContent> {
|
|
static const double _uniformGap = 16.0;
|
|
|
|
final Color _mainBlueColor = const Color(0xFF002FA7);
|
|
final Color _mainTextColor = Colors.black;
|
|
final Color _subTextColor = const Color(0xFF6A717B);
|
|
final Color _pageBackgroundColor = const Color(0xFFF5F7F9);
|
|
final Color _cardBackgroundColor = Colors.white;
|
|
final Color _accentContainerColor = const Color(0xFFF0F2F5);
|
|
final Color _toggleOffTrackColor = const Color(0xFFE0E0E0);
|
|
final Color _toggleOffKnobBorderColor = const Color(0xFFE0E0E0);
|
|
|
|
// 25.12.03 지은 추가 시작
|
|
final LockerApi _api = LockerApi();
|
|
bool _isLoading = false;
|
|
|
|
Future<void> _runLockerAction(String name, Future<bool> Function() action) async {
|
|
if (_isLoading) return;
|
|
|
|
setState(() => _isLoading = true);
|
|
|
|
// 실제 명령 전송
|
|
final success = await action();
|
|
|
|
if (!mounted) return;
|
|
|
|
setState(() => _isLoading = false);
|
|
}
|
|
// 25.12.03 지은 추가 끝
|
|
|
|
void test() async {
|
|
final data = await _api.getTimeseries(target: "*", limit: 200);
|
|
|
|
if (data != null) {
|
|
print("받은 데이터 길이: ${data['samples'].length}");
|
|
print(data['samples'][0]); // 첫 번째 데이터 출력
|
|
}
|
|
}
|
|
|
|
int _selectedImageIndex = 0;
|
|
|
|
static const BoxShadow _cleanShadow = BoxShadow(
|
|
color: Color.fromRGBO(0, 0, 0, 0.07),
|
|
blurRadius: 8,
|
|
offset: Offset(0, 4),
|
|
spreadRadius: 0,
|
|
);
|
|
|
|
final Map<String, bool> _controlToggles = {
|
|
'UV LED': false,
|
|
'CHARGING': true,
|
|
'HELMET': true,
|
|
'FAN': false,
|
|
};
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
test();
|
|
return Container(
|
|
color: _pageBackgroundColor,
|
|
child: Column(
|
|
children: [
|
|
const CustomHeader(),
|
|
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOverviewSection() {
|
|
return Container(
|
|
margin: const EdgeInsets.only(top: 5),
|
|
child: DashboardCard(
|
|
shadow: _cleanShadow,
|
|
cardColor: _cardBackgroundColor,
|
|
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: [
|
|
Text('장비 상태 / 사용자 정보', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
const Spacer(),
|
|
Icon(Icons.search, color: _subTextColor, size: 20),
|
|
const SizedBox(width: 8),
|
|
Icon(Icons.notifications_outlined, color: _subTextColor, size: 20),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildImageCard() {
|
|
return AspectRatio(
|
|
aspectRatio: 1.0,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: _accentContainerColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: _cardBackgroundColor,
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 20.0),
|
|
child: Image.asset(
|
|
_selectedImageIndex == 0
|
|
? 'assets/images/storage.png'
|
|
: 'assets/images/top.png',
|
|
width: 90,
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 12,
|
|
child: Row(
|
|
children: [
|
|
_buildSelectableIndicator(0),
|
|
const SizedBox(width: 8),
|
|
_buildSelectableIndicator(1),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSelectableIndicator(int index) {
|
|
final bool isSelected = _selectedImageIndex == index;
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedImageIndex = index;
|
|
});
|
|
},
|
|
child: Container(
|
|
width: 30,
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? _mainTextColor : Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoCard() {
|
|
return SizedBox(
|
|
height: 164,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_buildInfoRow('ID', Icon(Icons.person, color: _mainTextColor, size: 20), 'USER', '001',
|
|
value1Color: _mainTextColor, value2Color: _mainTextColor),
|
|
const SizedBox(height: 8),
|
|
_buildInfoRow('STATUS', null, '잠금 해제', '● 활성',
|
|
value1Color: _mainTextColor, value2Color: _mainTextColor),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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: _accentContainerColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(title,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: _subTextColor, fontSize: 11, height: 1.4)),
|
|
),
|
|
VerticalDivider(color: _subTextColor.withOpacity(0.5), 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 ?? _mainTextColor, fontWeight: FontWeight.bold, fontSize: 12)),
|
|
const SizedBox(height: 2),
|
|
Text(value2, style: TextStyle(color: value2Color ?? _mainTextColor, fontWeight: FontWeight.w500, fontSize: 12)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBatteryStatusCard() {
|
|
return DashboardCard(
|
|
shadow: _cleanShadow,
|
|
cardColor: _cardBackgroundColor,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(_uniformGap),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('배터리 상태 (%)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 70,
|
|
height: 70,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
SizedBox.expand(
|
|
child: CustomPaint(
|
|
painter: _BatteryArcPainter(
|
|
backgroundColor: _accentContainerColor,
|
|
color: _mainBlueColor,
|
|
percentage: 1.0,
|
|
),
|
|
),
|
|
),
|
|
Text('86', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: _mainTextColor)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('현재', style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.bold)),
|
|
Text('사용 중', style: TextStyle(color: _mainBlueColor, fontSize: 14, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
Divider(color: _subTextColor.withOpacity(0.5), height: 20, thickness: 1),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 4,
|
|
child: Text('태양광 패널', style: TextStyle(color: _mainTextColor, fontSize: 14)),
|
|
),
|
|
Expanded(
|
|
flex: 5,
|
|
child: Row(
|
|
children: [
|
|
Expanded(child: Text('전압: 00', style: TextStyle(color: _subTextColor, fontSize: 14))),
|
|
SizedBox(
|
|
height: 20,
|
|
child: VerticalDivider(color: _subTextColor.withOpacity(0.5), thickness: 1),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 8.0),
|
|
child: Text('전류: 00', style: TextStyle(color: _subTextColor, fontSize: 14)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildControlCard() {
|
|
return DashboardCard(
|
|
shadow: _cleanShadow,
|
|
cardColor: _cardBackgroundColor,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(_uniformGap),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('보관함 원격 제어', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
|
|
const SizedBox(height: 16),
|
|
SizedBox(
|
|
height: 70,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStyledToggleSwitch(
|
|
'UV LED',
|
|
_controlToggles['UV LED']!,
|
|
// 25.12.03 지은 수정 시작
|
|
(val) {
|
|
_runLockerAction("UV 제어", () async {
|
|
bool success = await _api.setUV(val);
|
|
|
|
if (success) {
|
|
setState(() => _controlToggles['UV LED'] = val);
|
|
}
|
|
return success;
|
|
}
|
|
);
|
|
},
|
|
),
|
|
),
|
|
// 25.12.03 지은 수정 끝
|
|
VerticalDivider(color: _mainBlueColor.withOpacity(0.5), indent: 10, endIndent: 10),
|
|
Expanded(
|
|
child: _buildStyledToggleSwitch(
|
|
'CHARGING',
|
|
_controlToggles['CHARGING']!,
|
|
(val) => setState(() => _controlToggles['CHARGING'] = val))),
|
|
VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10),
|
|
Expanded(
|
|
child: _buildStyledToggleSwitch(
|
|
'HELMET',
|
|
_controlToggles['HELMET']!,
|
|
(val) => setState(() => _controlToggles['HELMET'] = val))),
|
|
VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10),
|
|
Expanded(
|
|
child: _buildStyledToggleSwitch('FAN',
|
|
_controlToggles['FAN']!,
|
|
// 25.12.03 지은 수정 시작
|
|
(val) {
|
|
print("👉 [디버깅] fan 눌림! 값: $val");
|
|
_runLockerAction("FAN 제어", () async {
|
|
print("👉 [디버깅] API 요청 시작...");
|
|
bool success = await _api.setFan(val);
|
|
|
|
if (success) {
|
|
setState(() => _controlToggles['FAN'] = val);
|
|
}
|
|
return success;
|
|
});
|
|
},
|
|
// 25.12.03 지은 수정 끝
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStyledToggleSwitch(String title, bool value, ValueChanged<bool> onChanged) {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(title, style: TextStyle(color: _mainTextColor, 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 ? _mainBlueColor : _toggleOffTrackColor,
|
|
),
|
|
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: Colors.white,
|
|
border: Border.all(
|
|
color: value ? _mainBlueColor : _toggleOffKnobBorderColor,
|
|
width: 1.5),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Center(
|
|
child: Text('ON',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: value ? Colors.white : Colors.transparent)))),
|
|
Expanded(
|
|
child: Center(
|
|
child: Text('OFF',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: value ? Colors.transparent : _mainTextColor)))),
|
|
],
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildEnvironmentSensorsCard() {
|
|
return DashboardCard(
|
|
shadow: _cleanShadow,
|
|
cardColor: _cardBackgroundColor,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(_uniformGap),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text('환경 모니터링', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
const Spacer(),
|
|
InkWell(
|
|
onTap: () {},
|
|
child: Row(
|
|
children: [
|
|
Text('더보기', style: TextStyle(color: _mainTextColor, fontSize: 9)),
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.arrow_forward_ios, size: 10, color: _subTextColor),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSensorInfoRow(Icons.water_drop_outlined, '습도: 60%', '습도'),
|
|
const SizedBox(height: 24),
|
|
_buildSensorInfoRow(Icons.thermostat, '온도: 24.5℃', '온도'),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
flex: 1,
|
|
child: SizedBox(
|
|
height: 60,
|
|
child: _LineChartPlaceholder(lineColor: _mainTextColor, subLabelColor: _subTextColor))),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSensorInfoRow(IconData icon, String text, String type) {
|
|
return Row(children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: _accentContainerColor,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(icon, color: _mainTextColor, size: 24),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(text, style: TextStyle(fontSize: 14, color: _mainTextColor, fontWeight: FontWeight.w600))
|
|
]);
|
|
}
|
|
|
|
Widget _buildMyLocationCard() {
|
|
const LatLng exampleLocation = LatLng(37.5665, 126.9780);
|
|
return DashboardCard(
|
|
shadow: _cleanShadow,
|
|
cardColor: _cardBackgroundColor,
|
|
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: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('현위치', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
const SizedBox(height: 4),
|
|
Text('주소: 남구 효덕로 277', style: TextStyle(fontSize: 11, color: _mainTextColor)),
|
|
],
|
|
),
|
|
InkWell(
|
|
onTap: () {},
|
|
child: Row(
|
|
children: [
|
|
Text('더보기', style: TextStyle(color: _mainTextColor, fontSize: 9)),
|
|
const SizedBox(width: 4),
|
|
Icon(Icons.arrow_forward_ios, size: 10, color: _subTextColor),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: FlutterMap(
|
|
options: const MapOptions(
|
|
initialCenter: exampleLocation,
|
|
initialZoom: 15.0,
|
|
interactionOptions: InteractionOptions(flags: InteractiveFlag.none),
|
|
),
|
|
children: [
|
|
TileLayer(
|
|
urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
|
userAgentPackageName: 'com.example.app',
|
|
subdomains: const ['a', 'b', 'c', 'd'],
|
|
retinaMode: true,
|
|
),
|
|
MarkerLayer(
|
|
markers: [
|
|
Marker(
|
|
point: exampleLocation,
|
|
width: 80,
|
|
height: 80,
|
|
child: Icon(Icons.location_pin, size: 40, color: _mainTextColor),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActivityCard() {
|
|
return DashboardCard(
|
|
shadow: _cleanShadow,
|
|
cardColor: _cardBackgroundColor,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(_uniformGap),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('최근 활동', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_activityText('08:15 AM - 배터리 충전 완료'),
|
|
const SizedBox(height: 8),
|
|
_activityText('9:30 AM - UV LED 활성화 됨'),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_activityText('10:45 AM - 안전모 잠금 해제'),
|
|
const SizedBox(height: 8),
|
|
_activityText('11:00 AM - 안전모 착용 해제'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _activityText(String text) {
|
|
return Text(text,
|
|
style: TextStyle(fontSize: 11, color: _mainTextColor));
|
|
}
|
|
}
|
|
|
|
class _LineChartPlaceholder extends StatelessWidget {
|
|
final Color lineColor;
|
|
final Color subLabelColor;
|
|
|
|
const _LineChartPlaceholder({
|
|
super.key,
|
|
required this.lineColor,
|
|
required this.subLabelColor
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(children: [
|
|
Expanded(
|
|
child:
|
|
CustomPaint(painter: _LineChartPainter(color: lineColor), size: Size.infinite)),
|
|
const SizedBox(height: 4),
|
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
|
Text('24 시간 전', style: TextStyle(fontSize: 8, color: subLabelColor)),
|
|
Text('12시간 전', style: TextStyle(fontSize: 8, color: subLabelColor)),
|
|
Text('현재', style: TextStyle(fontSize: 8, color: subLabelColor))
|
|
])
|
|
]);
|
|
}
|
|
}
|
|
|
|
class _LineChartPainter extends CustomPainter {
|
|
final Color color;
|
|
_LineChartPainter({required this.color});
|
|
|
|
@override
|
|
void paint(ui.Canvas canvas, ui.Size size) {
|
|
final paint = Paint()
|
|
..color = color
|
|
..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 DashboardCard extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsetsGeometry? padding;
|
|
final Clip clipBehavior;
|
|
final Color? borderColor;
|
|
final Color? cardColor;
|
|
final BoxShadow? shadow;
|
|
|
|
const DashboardCard({
|
|
super.key,
|
|
required this.child,
|
|
this.padding,
|
|
this.clipBehavior = Clip.none,
|
|
this.borderColor,
|
|
this.cardColor,
|
|
this.shadow,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
clipBehavior: clipBehavior,
|
|
decoration: BoxDecoration(
|
|
color: cardColor ?? Colors.white,
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: shadow != null ? [shadow!] : null,
|
|
),
|
|
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;
|
|
}
|
|
} |