Created
May 2, 2019 13:50
-
-
Save rodydavis/d11bd2082aed3afeb227fb4847d6766d to your computer and use it in GitHub Desktop.
Working version of a desktop calendar built with Flutter.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import '../../constants.dart'; | |
import '../../utils/index.dart'; | |
import 'index.dart'; | |
class FullCalendar extends StatefulWidget { | |
const FullCalendar({ | |
this.events, | |
this.onNextWeek, | |
this.onPreviousWeek, | |
}); | |
final List<CalendarEvent> events; | |
final ValueChanged<DateTime> onNextWeek, onPreviousWeek; | |
@override | |
_FullCalendarState createState() => _FullCalendarState(); | |
} | |
class _FullCalendarState extends State<FullCalendar> { | |
DateTime _week; | |
DateTime _currentTime = DateTime.now(); | |
ScrollController _controller; | |
Timer _timer; | |
List<CalendarEvent> _events; | |
@override | |
void initState() { | |
startTimer(); | |
changeDate(DateTime.now()); | |
loadEvents(); | |
super.initState(); | |
} | |
void loadEvents() { | |
if (mounted) | |
setState(() { | |
_events = widget?.events ?? []; | |
}); | |
} | |
void nextWeek() { | |
if (mounted) | |
setState(() { | |
_week = _week.add(Duration(days: 7)); | |
}); | |
if (widget?.onNextWeek != null) { | |
widget.onNextWeek(_week); | |
} | |
} | |
void previousWeek() { | |
if (mounted) | |
setState(() { | |
_week = _week.subtract(Duration(days: 7)); | |
}); | |
if (widget?.onPreviousWeek != null) { | |
widget.onPreviousWeek(_week); | |
} | |
} | |
void changeDate(DateTime value) { | |
final _now = value; | |
if (mounted) | |
setState(() { | |
_week = _now.subtract(Duration(days: _now.weekday)); | |
final _offset = _getHourOffset(_now); | |
_controller = ScrollController( | |
initialScrollOffset: | |
(_offset - 200).isNegative ? _offset : (_offset - 200)); | |
}); | |
} | |
double _getHourOffset(DateTime _now) { | |
final _duration = Duration(hours: _now.hour, minutes: _now.minute); | |
return _duration.inMinutes * (_cellHeight / 60); | |
} | |
double _getEndOffset(CalendarEvent event) { | |
var _start = event.startDate; | |
_start = _start.add(Duration(minutes: 30)); | |
final _end = event?.endDate ?? _start; | |
final _offset = _getHourOffset(_end); | |
final _hours = TimeOfDay.hoursPerDay * (60 * (_cellHeight / 60)); | |
return _hours - _offset; | |
} | |
bool _isSameDay(int day, DateTime value) { | |
final _now = value; | |
final _current = _week.add(Duration(days: day)); | |
if (_current.year == _now.year) { | |
if (_current.month == _now.month) { | |
if (_current.day == _now.day) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
void startTimer() { | |
_timer = new Timer.periodic(const Duration(seconds: 5), (timer) { | |
if (mounted) | |
setState(() { | |
_currentTime = DateTime.now(); | |
}); | |
}); | |
} | |
@override | |
void dispose() { | |
_timer.cancel(); | |
super.dispose(); | |
} | |
@override | |
void didUpdateWidget(FullCalendar oldWidget) { | |
if (oldWidget.events != widget.events) { | |
loadEvents(); | |
} | |
super.didUpdateWidget(oldWidget); | |
} | |
final _cellHeight = 100.0; | |
double _getWidth(BuildContext context) { | |
final _size = MediaQuery.of(context).size; | |
final _width = _size.width / 12.84; | |
return _width; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), | |
child: Column( | |
children: <Widget>[ | |
_buildHeader(), | |
_buildDateRange(context), | |
Expanded( | |
child: Container( | |
padding: EdgeInsets.only(top: 20.0), | |
child: SingleChildScrollView( | |
controller: _controller, | |
child: Stack( | |
children: <Widget>[ | |
_buildHours(context), | |
Positioned( | |
top: 0, | |
left: 0, | |
bottom: 0, | |
right: 0, | |
child: _buildEvents(), | |
), | |
_buildCurrentTime(context), | |
], | |
)), | |
), | |
), | |
], | |
), | |
); | |
} | |
Container _buildDateRange(BuildContext context) { | |
final _width = _getWidth(context); | |
return Container( | |
child: Stack( | |
children: <Widget>[ | |
Container( | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: <Widget>[ | |
Container( | |
width: 100, | |
child: IconButton( | |
icon: Icon(Icons.arrow_back), | |
onPressed: previousWeek, | |
), | |
), | |
for (var i = 0; i < DateTime.daysPerWeek; i++) ...[ | |
Container( | |
width: _width, | |
child: Container( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
Container( | |
padding: EdgeInsets.all(8.0), | |
decoration: !_isSameDay(i, DateTime.now()) | |
? null | |
: BoxDecoration( | |
color: Colors.redAccent, | |
borderRadius: BorderRadius.circular(12.0), | |
), | |
child: Column( | |
children: <Widget>[ | |
Text( | |
formatDateCustom(_week.add(Duration(days: i)), | |
format: 'EEE'), | |
style: TextStyle( | |
fontSize: 16, | |
fontWeight: FontWeight.w300, | |
color: textColor(context, | |
invert: _isSameDay(i, DateTime.now())), | |
), | |
), | |
Text( | |
_week.add(Duration(days: i)).day.toString(), | |
style: TextStyle( | |
fontSize: 24, | |
fontWeight: FontWeight.w500, | |
color: textColor(context, | |
invert: _isSameDay(i, DateTime.now())), | |
), | |
), | |
], | |
), | |
) | |
], | |
), | |
), | |
) | |
], | |
], | |
), | |
), | |
Positioned( | |
right: 0, | |
top: 0, | |
bottom: 0, | |
child: Container( | |
width: _width / 2, | |
child: IconButton( | |
icon: Icon(Icons.arrow_forward), | |
onPressed: nextWeek, | |
), | |
), | |
) | |
], | |
), | |
); | |
} | |
Container _buildHeader() { | |
return Container( | |
padding: EdgeInsets.all(8.0), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: <Widget>[ | |
RaisedButton.icon( | |
icon: Icon(Icons.event), | |
label: Text('Today'), | |
onPressed: () { | |
changeDate(DateTime.now()); | |
}, | |
), | |
Text( | |
formatDateCustom(_week, format: 'MMMM'), | |
style: TextStyle( | |
fontSize: 30, | |
fontWeight: FontWeight.w700, | |
), | |
), | |
RaisedButton.icon( | |
icon: Icon(Icons.refresh), | |
label: Text('Refresh'), | |
onPressed: loadEvents, | |
), | |
], | |
), | |
); | |
} | |
Container _buildEvents() { | |
final _width = _getWidth(context); | |
return Container( | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: <Widget>[ | |
Container(width: 100, height: _cellHeight), | |
for (var i = 0; i < DateTime.daysPerWeek; i++) ...[ | |
Container( | |
width: _width, | |
child: Stack( | |
children: <Widget>[ | |
if (_events != null) | |
for (var event in _events) ...[ | |
if (_isSameDay(i, event.startDate)) ...[ | |
Positioned( | |
top: _getHourOffset(event.startDate), | |
bottom: _getEndOffset(event), | |
child: Container( | |
width: _width, | |
child: _buildEvent( | |
event, | |
_getHourOffset(event.startDate), | |
_getEndOffset(event)), | |
), | |
) | |
] | |
] | |
], | |
), | |
), | |
], | |
], | |
), | |
); | |
} | |
Container _buildEvent(CalendarEvent event, double start, double end) { | |
final _width = _getWidth(context); | |
final _color = event?.color ?? Colors.lightBlueAccent; | |
return Container( | |
child: InkWell( | |
onTap: () { | |
showDialog( | |
context: context, | |
builder: (context) => PopUpSurface( | |
title: 'Event Info', | |
), | |
); | |
}, | |
child: Container( | |
width: _width, | |
child: Container( | |
decoration: BoxDecoration( | |
color: _color.withAlpha(50), | |
border: Border( | |
left: BorderSide( | |
color: _color, | |
width: 3.0, | |
), | |
)), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
Container( | |
padding: EdgeInsets.only(left: 4.0, top: 4.0), | |
child: Text( | |
TimeOfDay.fromDateTime(event.startDate).format(context), | |
style: TextStyle( | |
fontSize: 12, | |
fontWeight: FontWeight.w500, | |
color: _color, | |
), | |
), | |
), | |
Container( | |
padding: EdgeInsets.only(left: 4.0), | |
child: Text( | |
event.title, | |
style: TextStyle( | |
fontSize: 16, | |
fontWeight: FontWeight.w500, | |
), | |
), | |
), | |
if (event?.description != null) ...[ | |
Expanded( | |
child: Container( | |
padding: EdgeInsets.all(4.0), | |
child: Text( | |
event.description, | |
style: TextStyle( | |
fontSize: 16, | |
fontWeight: FontWeight.w500, | |
), | |
), | |
), | |
) | |
] | |
], | |
), | |
), | |
), | |
), | |
); | |
} | |
Column _buildHours(BuildContext context) { | |
final _width = _getWidth(context); | |
return Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
for (var i = 0; i < TimeOfDay.hoursPerDay; i++) ...[ | |
Container( | |
width: 100, | |
height: _cellHeight, | |
child: Container( | |
padding: EdgeInsets.all(8.0), | |
child: Text( | |
TimeOfDay(hour: i, minute: 0).format(context), | |
style: TextStyle( | |
fontSize: 20, | |
fontWeight: FontWeight.w500, | |
), | |
), | |
), | |
), | |
...[Divider(height: 0.0)], | |
], | |
], | |
); | |
} | |
AnimatedContainer _buildCurrentTime(BuildContext context) { | |
return AnimatedContainer( | |
padding: EdgeInsets.only(top: _getHourOffset(_currentTime)), | |
duration: Duration(seconds: 1), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: <Widget>[ | |
Material( | |
borderRadius: BorderRadius.circular(4.0), | |
elevation: 8.0, | |
child: Container( | |
padding: EdgeInsets.all(10.0), | |
child: Text( | |
TimeOfDay.fromDateTime(_currentTime).format(context), | |
style: TextStyle( | |
fontSize: 20, | |
fontWeight: FontWeight.w800, | |
color: Colors.blueAccent), | |
), | |
), | |
), | |
Expanded( | |
child: DashedLine( | |
color: Colors.blueAccent, | |
height: 1, | |
)) | |
], | |
), | |
); | |
} | |
} | |
class CalendarEvent { | |
const CalendarEvent({ | |
@required this.title, | |
@required this.startDate, | |
this.allDay = false, | |
this.endDate, | |
this.description, | |
this.onTap, | |
this.color, | |
}); | |
final DateTime startDate, endDate; | |
final bool allDay; | |
final String title; | |
final String description; | |
final VoidCallback onTap; | |
final Color color; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment