893 lines
32 KiB
Dart
893 lines
32 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:table_calendar/table_calendar.dart';
|
|
|
|
import '../../../common/extension/extension.dart';
|
|
import '../../../common/theme/theme.dart';
|
|
import '../../components/appbar/appbar.dart';
|
|
|
|
// Schedule model dengan optimasi
|
|
class Schedule {
|
|
final DateTime date;
|
|
final String title;
|
|
final String? subtitle;
|
|
final String? time;
|
|
final Color color;
|
|
final IconData? icon;
|
|
final String? avatarUrl;
|
|
final bool hasDetails;
|
|
final String? location;
|
|
final String? instructions;
|
|
|
|
const Schedule({
|
|
required this.date,
|
|
required this.title,
|
|
this.subtitle,
|
|
this.time,
|
|
required this.color,
|
|
this.icon,
|
|
this.avatarUrl,
|
|
this.hasDetails = false,
|
|
this.location,
|
|
this.instructions,
|
|
});
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is Schedule &&
|
|
runtimeType == other.runtimeType &&
|
|
date == other.date;
|
|
|
|
@override
|
|
int get hashCode => date.hashCode;
|
|
}
|
|
|
|
@RoutePage()
|
|
class SchedulePage extends StatefulWidget {
|
|
const SchedulePage({super.key});
|
|
|
|
@override
|
|
State<SchedulePage> createState() => _SchedulePageState();
|
|
}
|
|
|
|
class _SchedulePageState extends State<SchedulePage>
|
|
with TickerProviderStateMixin {
|
|
late DateTime _selectedDay;
|
|
late DateTime _focusedDay;
|
|
late PageController _pageController;
|
|
late AnimationController _fadeController;
|
|
late AnimationController _slideController;
|
|
late AnimationController _rotationController;
|
|
late Animation<double> _fadeAnimation;
|
|
late Animation<Offset> _slideAnimation;
|
|
late Animation<double> rotationAnimation;
|
|
|
|
final CalendarFormat _calendarFormat = CalendarFormat.week;
|
|
|
|
// Optimized schedules with const constructor
|
|
static final List<Schedule> _schedules = [
|
|
Schedule(
|
|
date: DateTime.now(),
|
|
title: 'Morning Shift',
|
|
subtitle: '8am - 1pm (5h)',
|
|
time: '08:00',
|
|
color: AppColor.primary,
|
|
icon: Icons.work,
|
|
hasDetails: true,
|
|
location: 'Office Building A',
|
|
instructions: 'Check all equipment before starting',
|
|
),
|
|
Schedule(
|
|
date: DateTime.now().add(Duration(days: 1)),
|
|
title: 'Night Shift',
|
|
subtitle: '6pm - 11pm (5h)',
|
|
time: '18:00',
|
|
color: AppColor.warning,
|
|
icon: Icons.nights_stay,
|
|
hasDetails: true,
|
|
location: 'Warehouse B',
|
|
instructions: 'Close all the lights when you exit',
|
|
),
|
|
Schedule(
|
|
date: DateTime.now().add(Duration(days: 2)),
|
|
title: 'Stand up Meeting',
|
|
subtitle: '30 minutes',
|
|
time: '09:00',
|
|
color: AppColor.info,
|
|
icon: Icons.group,
|
|
hasDetails: true,
|
|
location: 'Conference Room',
|
|
instructions: 'Prepare weekly report',
|
|
),
|
|
Schedule(
|
|
date: DateTime.now().add(Duration(days: 3)),
|
|
title: 'Training Session',
|
|
subtitle: '2 hours',
|
|
time: '14:00',
|
|
color: AppColor.success,
|
|
icon: Icons.school,
|
|
hasDetails: true,
|
|
location: 'Training Center',
|
|
instructions: 'Bring notebook and pen',
|
|
),
|
|
Schedule(
|
|
date: DateTime.now().add(Duration(days: 4)),
|
|
title: 'Client Meeting',
|
|
subtitle: '1 hour',
|
|
time: '10:00',
|
|
color: Color(0xFF9C27B0),
|
|
icon: Icons.business,
|
|
hasDetails: true,
|
|
location: 'Client Office',
|
|
instructions: 'Prepare presentation slides',
|
|
),
|
|
];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedDay = DateTime.now();
|
|
_focusedDay = DateTime.now();
|
|
_pageController = PageController();
|
|
|
|
// Animation controllers
|
|
_fadeController = AnimationController(
|
|
duration: const Duration(milliseconds: 300),
|
|
vsync: this,
|
|
);
|
|
|
|
_slideController = AnimationController(
|
|
duration: const Duration(milliseconds: 400),
|
|
vsync: this,
|
|
);
|
|
|
|
_rotationController = AnimationController(
|
|
duration: const Duration(seconds: 20),
|
|
vsync: this,
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
|
CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut),
|
|
);
|
|
|
|
_slideAnimation =
|
|
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate(
|
|
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
|
|
);
|
|
|
|
rotationAnimation =
|
|
Tween<double>(
|
|
begin: 0.0,
|
|
end: 2 * 3.14159, // 360 degrees in radians
|
|
).animate(
|
|
CurvedAnimation(parent: _rotationController, curve: Curves.linear),
|
|
);
|
|
|
|
// Start animations
|
|
_fadeController.forward();
|
|
_slideController.forward();
|
|
_rotationController.repeat(); // Infinite rotation
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pageController.dispose();
|
|
_fadeController.dispose();
|
|
_slideController.dispose();
|
|
_rotationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
List<Schedule> _getEventsForDay(DateTime day) {
|
|
return _schedules.where((schedule) {
|
|
return isSameDay(schedule.date, day);
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppColor.background,
|
|
body: AnimatedBuilder(
|
|
animation: _fadeAnimation,
|
|
builder: (context, child) {
|
|
return Opacity(
|
|
opacity: _fadeAnimation.value,
|
|
child: CustomScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
slivers: [
|
|
_buildSliverAppBar(),
|
|
_buildSliverCalendar(),
|
|
_buildSliverScheduleList(),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSliverAppBar() {
|
|
return SliverAppBar(
|
|
expandedHeight: 120.0,
|
|
floating: false,
|
|
pinned: true,
|
|
backgroundColor: AppColor.primary,
|
|
flexibleSpace: CustomAppBar(title: context.lang.schedule),
|
|
);
|
|
}
|
|
|
|
Widget _buildSliverCalendar() {
|
|
return SliverToBoxAdapter(
|
|
child: SlideTransition(
|
|
position: _slideAnimation,
|
|
child: Container(
|
|
margin: const EdgeInsets.all(20),
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white,
|
|
borderRadius: BorderRadius.circular(24),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: TableCalendar<Schedule>(
|
|
firstDay: DateTime.utc(2023, 1, 1),
|
|
lastDay: DateTime.utc(2025, 12, 31),
|
|
focusedDay: _focusedDay,
|
|
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
|
|
calendarFormat: _calendarFormat,
|
|
eventLoader: _getEventsForDay,
|
|
availableCalendarFormats: const {CalendarFormat.week: 'Week'},
|
|
headerStyle: HeaderStyle(
|
|
formatButtonVisible: false,
|
|
titleCentered: true,
|
|
leftChevronVisible: true,
|
|
rightChevronVisible: true,
|
|
headerPadding: const EdgeInsets.symmetric(vertical: 16),
|
|
titleTextStyle: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
leftChevronIcon: Icon(
|
|
Icons.chevron_left,
|
|
color: AppColor.textSecondary,
|
|
),
|
|
rightChevronIcon: Icon(
|
|
Icons.chevron_right,
|
|
color: AppColor.textSecondary,
|
|
),
|
|
),
|
|
calendarStyle: CalendarStyle(
|
|
outsideDaysVisible: false,
|
|
weekendTextStyle: TextStyle(
|
|
color: AppColor.textLight,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
defaultTextStyle: TextStyle(
|
|
color: AppColor.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
selectedTextStyle: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
selectedDecoration: BoxDecoration(
|
|
color: AppColor.primary,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColor.primary.withOpacity(0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
todayDecoration: BoxDecoration(
|
|
color: AppColor.primary.withOpacity(0.2),
|
|
shape: BoxShape.circle,
|
|
),
|
|
todayTextStyle: TextStyle(
|
|
color: AppColor.primary,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
markerDecoration: BoxDecoration(
|
|
color: AppColor.warning,
|
|
shape: BoxShape.circle,
|
|
),
|
|
markersMaxCount: 1,
|
|
markerMargin: const EdgeInsets.only(top: 5),
|
|
markerSize: 6,
|
|
),
|
|
onDaySelected: (selectedDay, focusedDay) {
|
|
if (!isSameDay(_selectedDay, selectedDay)) {
|
|
setState(() {
|
|
_selectedDay = selectedDay;
|
|
_focusedDay = focusedDay;
|
|
});
|
|
_animateScheduleChange();
|
|
}
|
|
},
|
|
onPageChanged: (focusedDay) {
|
|
_focusedDay = focusedDay;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSliverScheduleList() {
|
|
final selectedEvents = _getEventsForDay(_selectedDay);
|
|
|
|
return selectedEvents.isEmpty
|
|
? SliverFillRemaining(
|
|
child: SlideTransition(
|
|
position: _slideAnimation,
|
|
child: _buildEmptyState(),
|
|
),
|
|
)
|
|
: SliverPadding(
|
|
padding: const EdgeInsets.only(
|
|
top: 8,
|
|
left: 20,
|
|
right: 20,
|
|
bottom: 20,
|
|
),
|
|
sliver: SliverList(
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
|
return SlideTransition(
|
|
position: _slideAnimation,
|
|
child: AnimatedContainer(
|
|
duration: Duration(milliseconds: 200 + (index * 100)),
|
|
curve: Curves.easeOutCubic,
|
|
child: _buildModernScheduleItem(
|
|
selectedEvents[index],
|
|
index,
|
|
),
|
|
),
|
|
);
|
|
}, childCount: selectedEvents.length),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(32),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.primary.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.event_available,
|
|
size: 64,
|
|
color: AppColor.primary.withOpacity(0.6),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'No events today',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Enjoy your free time!',
|
|
style: TextStyle(fontSize: 16, color: AppColor.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildModernScheduleItem(Schedule schedule, int index) {
|
|
return TweenAnimationBuilder<double>(
|
|
duration: Duration(milliseconds: 300 + (index * 100)),
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
curve: Curves.easeOutCubic,
|
|
builder: (context, value, child) {
|
|
return Transform.translate(
|
|
offset: Offset(0, 20 * (1 - value)),
|
|
child: Opacity(
|
|
opacity: value,
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: schedule.hasDetails
|
|
? () => _showModernScheduleDetails(schedule)
|
|
: null,
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: schedule.color.withOpacity(0.2),
|
|
width: 2,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: schedule.color.withOpacity(0.1),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Time indicator
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: schedule.color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
schedule.time ?? 'All day',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: schedule.color,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
|
|
// Content
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
schedule.title,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
if (schedule.subtitle != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
schedule.subtitle!,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: AppColor.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
if (schedule.location != null) ...[
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.location_on,
|
|
size: 14,
|
|
color: AppColor.textLight,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
schedule.location!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColor.textLight,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
|
|
// Icon and arrow
|
|
if (schedule.icon != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: schedule.color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
schedule.icon,
|
|
color: schedule.color,
|
|
size: 24,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
],
|
|
|
|
if (schedule.hasDetails)
|
|
Icon(
|
|
Icons.arrow_forward_ios,
|
|
size: 16,
|
|
color: AppColor.textLight,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _animateScheduleChange() {
|
|
_slideController.reset();
|
|
_slideController.forward();
|
|
}
|
|
|
|
void _showModernScheduleDetails(Schedule schedule) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
isScrollControlled: true,
|
|
builder: (context) => _buildModernScheduleDetailsModal(schedule),
|
|
);
|
|
}
|
|
|
|
Widget _buildModernScheduleDetailsModal(Schedule schedule) {
|
|
return TweenAnimationBuilder<double>(
|
|
duration: const Duration(milliseconds: 400),
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
curve: Curves.easeOutCubic,
|
|
builder: (context, value, child) {
|
|
return Transform.translate(
|
|
offset: Offset(0, 50 * (1 - value)),
|
|
child: Opacity(
|
|
opacity: value,
|
|
child: Container(
|
|
height: MediaQuery.of(context).size.height * 0.75,
|
|
decoration: BoxDecoration(
|
|
color: AppColor.white,
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(24),
|
|
topRight: Radius.circular(24),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Handle bar
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 12),
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: AppColor.border,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
|
|
// Content
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header with color indicator
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 4,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: schedule.color,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
schedule.title,
|
|
style: TextStyle(
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
if (schedule.time != null)
|
|
Text(
|
|
schedule.time!,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: schedule.color,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (schedule.icon != null)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: schedule.color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Icon(
|
|
schedule.icon,
|
|
color: schedule.color,
|
|
size: 32,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Duration and location info
|
|
if (schedule.subtitle != null ||
|
|
schedule.location != null) ...[
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.background,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
if (schedule.subtitle != null) ...[
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.access_time,
|
|
color: AppColor.textSecondary,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
schedule.subtitle!,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (schedule.location != null)
|
|
const SizedBox(height: 16),
|
|
],
|
|
if (schedule.location != null)
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.location_on,
|
|
color: AppColor.textSecondary,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
schedule.location!,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
|
|
// Map placeholder
|
|
Container(
|
|
height: 180,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
schedule.color.withOpacity(0.1),
|
|
schedule.color.withOpacity(0.05),
|
|
],
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// Map grid pattern
|
|
CustomPaint(
|
|
size: const Size(double.infinity, 180),
|
|
painter: MapGridPainter(
|
|
color: schedule.color.withOpacity(0.1),
|
|
),
|
|
),
|
|
// Location pin
|
|
Positioned(
|
|
right: 40,
|
|
top: 60,
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: schedule.color,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: schedule.color.withOpacity(
|
|
0.3,
|
|
),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.location_on,
|
|
color: Colors.white,
|
|
size: 24,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Instructions
|
|
if (schedule.instructions != null) ...[
|
|
Text(
|
|
'Instructions',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.background,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: schedule.color.withOpacity(0.2),
|
|
),
|
|
),
|
|
child: Text(
|
|
schedule.instructions!,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColor.textPrimary,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
],
|
|
|
|
// PDF attachment
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColor.background,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: AppColor.border),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: const Icon(
|
|
Icons.picture_as_pdf,
|
|
color: Colors.red,
|
|
size: 24,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Instructions.pdf',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColor.textPrimary,
|
|
),
|
|
),
|
|
Text(
|
|
'2.4 MB',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: AppColor.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: schedule.color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
Icons.download,
|
|
color: schedule.color,
|
|
size: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Custom painter for map grid
|
|
class MapGridPainter extends CustomPainter {
|
|
final Color color;
|
|
|
|
MapGridPainter({required this.color});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint = Paint()
|
|
..color = color
|
|
..strokeWidth = 1;
|
|
|
|
// Draw vertical lines
|
|
for (int i = 0; i <= 8; i++) {
|
|
final x = (size.width / 8) * i;
|
|
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
|
|
}
|
|
|
|
// Draw horizontal lines
|
|
for (int i = 0; i <= 6; i++) {
|
|
final y = (size.height / 6) * i;
|
|
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|