Created
May 29, 2020 22:44
-
-
Save rodydavis/5cd5f49eb0981133160f299564130d07 to your computer and use it in GitHub Desktop.
Flutter Gmail Clone
This file contains hidden or 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:convert'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'dart:math' as math; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
title: 'Flutter Gmail Clone', | |
theme: ThemeData(primaryColor: Colors.white, accentColor: Colors.black), | |
home: GmailApp(), | |
); | |
} | |
} | |
class GmailApp extends StatefulWidget { | |
const GmailApp({Key key}) : super(key: key); | |
@override | |
_GmailAppState createState() => _GmailAppState(); | |
} | |
class _GmailAppState extends State<GmailApp> { | |
final emailsController = ValueNotifier<List<Email>>(null); | |
final indexController = ValueNotifier<int>(0); | |
@override | |
void initState() { | |
super.initState(); | |
emailsController.value = []; | |
final rdm = math.Random(); | |
const total = 200; | |
List.generate(total, (index) { | |
final _current = emailsController.value; | |
_current.add(Email( | |
sender: (EmailContact() | |
..firstName = createWord() | |
..lastName = createWord()), | |
isRead: false, | |
dateReceived: DateTime.now().subtract(Duration(minutes: total - index)), | |
subject: createSentence(sentenceLength: 5), | |
body: createText( | |
numParagraphs: rdm.nextInt(5), | |
numSentences: rdm.nextInt(8), | |
), | |
)); | |
updateEmails(_current); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return ValueListenableBuilder<List<Email>>( | |
valueListenable: emailsController, | |
builder: (context, emails, child) => LayoutBuilder( | |
builder: (context, dimens) { | |
if (dimens.maxWidth >= kTabletBreakpoint) { | |
return Scaffold( | |
drawer: _Drawer(), | |
floatingActionButton: _FAB(), | |
body: Row( | |
children: [ | |
Builder( | |
builder: (context) => NavigationRail( | |
destinations: [ | |
NavigationRailDestination( | |
icon: Icon(Icons.menu), | |
label: Text('Menu'), | |
), | |
NavigationRailDestination( | |
icon: Icon(Icons.inbox), | |
label: Text('Inbox'), | |
), | |
NavigationRailDestination( | |
icon: Icon(Icons.people), | |
label: Text('Contacts'), | |
), | |
NavigationRailDestination( | |
icon: Icon(Icons.label), | |
label: Text('Tags'), | |
), | |
], | |
labelType: NavigationRailLabelType.none, | |
selectedIndex: 0, | |
onDestinationSelected: (val) { | |
if (val == 0) { | |
Scaffold.of(context).openDrawer(); | |
} | |
}, | |
), | |
), | |
SizedBox( | |
width: kListWidth, | |
child: buildBody(emails, false), | |
), | |
Expanded( | |
child: _Details( | |
emailsController: emailsController, | |
indexController: indexController, | |
), | |
), | |
], | |
), | |
); | |
} | |
return Scaffold( | |
floatingActionButton: _FAB(), | |
drawer: _Drawer(), | |
body: buildBody(emails, true), | |
); | |
}, | |
), | |
); | |
} | |
Widget buildBody(List<Email> emails, bool isMobile) { | |
return NestedScrollView( | |
headerSliverBuilder: (context, innerScrolled) { | |
return [ | |
SliverPadding( | |
padding: EdgeInsets.only(top: 8), | |
sliver: SliverFloatingBar( | |
floating: true, | |
snap: true, | |
leading: !isMobile | |
? null | |
: InkWell( | |
child: Icon(Icons.menu), | |
onTap: () { | |
Scaffold.of(context).openDrawer(); | |
}, | |
), | |
automaticallyImplyLeading: false, | |
title: TextField( | |
decoration: InputDecoration.collapsed(hintText: "Search mail"), | |
), | |
trailing: CircleAvatar( | |
child: Text("RD"), | |
), | |
), | |
), | |
]; | |
}, | |
body: Scrollbar( | |
child: buildEmailList(emails, isMobile), | |
), | |
); | |
} | |
Widget buildEmailList(List<Email> emails, bool isMobile) { | |
if (emails == null) { | |
return Center(child: CircularProgressIndicator()); | |
} | |
if (emails.isEmpty) { | |
return Center(child: Text('No Emails Found!')); | |
} | |
return ListView.builder( | |
itemCount: emails.length, | |
itemBuilder: (context, index) { | |
final item = emails[index]; | |
return GestureDetector( | |
behavior: HitTestBehavior.opaque, | |
onTap: () { | |
updateEmailAtIndex(item.copyWith(isRead: true), index); | |
emailsController.value = emails; | |
indexController.value = index; | |
if (isMobile) { | |
Navigator.of(context) | |
.push<String>(MaterialPageRoute( | |
builder: (context) => _Details( | |
emailsController: emailsController, | |
indexController: indexController, | |
), | |
)) | |
.then((message) { | |
if (message != null) { | |
Scaffold.of(context).showSnackBar( | |
SnackBar(content: Text(message)), | |
); | |
} | |
}); | |
return; | |
} | |
}, | |
child: ValueListenableBuilder<int>( | |
valueListenable: indexController, | |
builder: (context, itemIndex, child) => Container( | |
decoration: isMobile || index != itemIndex | |
? null | |
: BoxDecoration( | |
color: Colors.blueGrey[100], | |
border: Border( | |
left: BorderSide( | |
color: Colors.blueGrey, | |
width: 5, | |
), | |
)), | |
child: _EmailTile( | |
email: emails[index], | |
onChanged: (val) { | |
updateEmailAtIndex(val, index); | |
}, | |
), | |
), | |
), | |
); | |
}, | |
); | |
} | |
void updateEmails(List<Email> items) { | |
emailsController.value = items; | |
emailsController.notifyListeners(); | |
} | |
void updateEmailAtIndex(Email val, int index) { | |
final _current = emailsController.value; | |
_current[index] = val; | |
emailsController.value = _current; | |
emailsController.notifyListeners(); | |
} | |
} | |
class _Details extends StatelessWidget { | |
const _Details({ | |
Key key, | |
@required this.emailsController, | |
@required this.indexController, | |
}) : super(key: key); | |
final ValueNotifier<List<Email>> emailsController; | |
final ValueNotifier<int> indexController; | |
@override | |
Widget build(BuildContext context) { | |
return ValueListenableBuilder<List<Email>>( | |
valueListenable: emailsController, | |
builder: (context, emails, child) => ValueListenableBuilder<int>( | |
valueListenable: indexController, | |
builder: (context, index, child) { | |
final selectedEmail = emails[index]; | |
return Scaffold( | |
appBar: AppBar( | |
actions: [ | |
IconButton( | |
icon: Icon(Icons.archive), | |
onPressed: () { | |
final _current = emails; | |
_current.removeAt(index); | |
updateEmails(_current); | |
Navigator.of(context).maybePop('Message moved to Archive'); | |
}, | |
), | |
IconButton( | |
icon: Icon(Icons.delete_outline), | |
onPressed: () { | |
final _current = emails; | |
_current.removeAt(index); | |
updateEmails(_current); | |
Navigator.of(context).maybePop('Message Deleted'); | |
}, | |
), | |
IconButton( | |
icon: Icon(Icons.mail_outline), | |
onPressed: () { | |
Navigator.of(context).maybePop(); | |
}, | |
), | |
IconButton( | |
icon: Icon(Icons.more_horiz), | |
onPressed: () {}, | |
), | |
], | |
), | |
body: SingleChildScrollView( | |
child: Column( | |
children: [ | |
Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Row( | |
children: [ | |
Expanded( | |
child: Text( | |
selectedEmail?.subject ?? 'No Subject', | |
style: TextStyle( | |
fontSize: 20, | |
), | |
), | |
), | |
InkWell( | |
borderRadius: BorderRadius.circular(18), | |
child: selectedEmail.isFavorite | |
? Icon( | |
Icons.star, | |
color: Colors.amber, | |
) | |
: Icon( | |
Icons.star_border, | |
color: inactiveColor, | |
), | |
onTap: () { | |
updateEmailAtIndex( | |
selectedEmail.copyWith( | |
isFavorite: !selectedEmail.isFavorite), | |
index, | |
); | |
}, | |
), | |
], | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Text(selectedEmail?.body ?? 'No Message Content'), | |
), | |
], | |
), | |
), | |
); | |
}, | |
), | |
); | |
} | |
void updateEmails(List<Email> items) { | |
emailsController.value = items; | |
emailsController.notifyListeners(); | |
} | |
void updateEmailAtIndex(Email val, int index) { | |
final _current = emailsController.value; | |
_current[index] = val; | |
emailsController.value = _current; | |
emailsController.notifyListeners(); | |
} | |
} | |
class _Drawer extends StatelessWidget { | |
const _Drawer({ | |
Key key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Drawer( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.only(top: 40, left: 20, bottom: 10), | |
child: Text( | |
'Gmail', | |
style: TextStyle( | |
color: Colors.red, | |
fontSize: 22, | |
), | |
), | |
), | |
Divider(), | |
], | |
), | |
); | |
} | |
} | |
// -- Custom Widgets -- | |
class _EmailTile extends StatelessWidget { | |
const _EmailTile({ | |
Key key, | |
@required this.email, | |
@required this.onChanged, | |
}) : super(key: key); | |
final Email email; | |
final ValueChanged<Email> onChanged; | |
@override | |
Widget build(BuildContext context) { | |
const kFontSize = 14.0; | |
return Container( | |
margin: const EdgeInsets.symmetric( | |
horizontal: 12, | |
vertical: 8, | |
), | |
child: Row( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
CircleAvatar( | |
radius: 23, | |
child: email.sender.hasImage | |
? Image.network(email.sender.photoUrl) | |
: Text(email.sender.letter)), | |
const SizedBox(width: 15), | |
Expanded( | |
child: Column( | |
children: [ | |
Row( | |
children: [ | |
Text( | |
email.recipients == null || email.recipients.isEmpty | |
? email.sender.displayName | |
: 'Test', | |
style: isReadStyle.copyWith(fontSize: kFontSize), | |
), | |
Spacer(), | |
Text( | |
email.time, | |
style: isReadStyle.copyWith(fontSize: kFontSize), | |
), | |
], | |
), | |
Container( | |
height: 45, | |
padding: EdgeInsets.only(top: 6), | |
child: Row( | |
children: [ | |
Expanded( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text( | |
email?.subject ?? 'No Subject', | |
style: isReadStyle.copyWith(fontSize: kFontSize), | |
overflow: TextOverflow.ellipsis, | |
maxLines: 1, | |
), | |
Text( | |
email?.body ?? 'No Preview Available', | |
style: TextStyle(color: inactiveColor) | |
.copyWith(fontSize: kFontSize), | |
maxLines: 1, | |
overflow: TextOverflow.ellipsis, | |
), | |
], | |
), | |
), | |
Column( | |
mainAxisSize: MainAxisSize.max, | |
mainAxisAlignment: MainAxisAlignment.end, | |
crossAxisAlignment: CrossAxisAlignment.end, | |
children: [ | |
InkWell( | |
borderRadius: BorderRadius.circular(18), | |
child: email?.isFavorite ?? false | |
? Icon( | |
Icons.star, | |
color: Colors.amber, | |
) | |
: Icon( | |
Icons.star_border, | |
color: inactiveColor, | |
), | |
onTap: () { | |
onChanged( | |
email.copyWith(isFavorite: !email.isFavorite), | |
); | |
}, | |
), | |
], | |
), | |
], | |
), | |
), | |
], | |
)), | |
], | |
), | |
); | |
} | |
TextStyle get isReadStyle { | |
if (!email.isRead) { | |
return TextStyle( | |
fontWeight: FontWeight.bold, | |
); | |
} | |
return TextStyle( | |
color: inactiveColor, | |
); | |
} | |
} | |
class _FAB extends StatelessWidget { | |
const _FAB({ | |
Key key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final _isDark = Theme.of(context).brightness == Brightness.dark; | |
return FloatingActionButton( | |
backgroundColor: _isDark ? null : Colors.white, | |
child: CustomPaint(painter: _FloatingPainter(), child: Container()), | |
onPressed: () {}, | |
); | |
} | |
} | |
class _FloatingPainter extends CustomPainter { | |
@override | |
void paint(Canvas canvas, Size size) { | |
Paint amberPaint = Paint() | |
..color = Colors.amber | |
..strokeWidth = 5; | |
Paint greenPaint = Paint() | |
..color = Colors.green | |
..strokeWidth = 5; | |
Paint bluePaint = Paint() | |
..color = Colors.blue | |
..strokeWidth = 5; | |
Paint redPaint = Paint() | |
..color = Colors.red | |
..strokeWidth = 5; | |
canvas.drawLine(Offset(size.width * 0.27, size.height * 0.5), | |
Offset(size.width * 0.5, size.height * 0.5), amberPaint); | |
canvas.drawLine( | |
Offset(size.width * 0.5, size.height * 0.5), | |
Offset(size.width * 0.5, size.height - (size.height * 0.27)), | |
greenPaint); | |
canvas.drawLine(Offset(size.width * 0.5, size.height * 0.5), | |
Offset(size.width - (size.width * 0.27), size.height * 0.5), bluePaint); | |
canvas.drawLine(Offset(size.width * 0.5, size.height * 0.5), | |
Offset(size.width * 0.5, size.height * 0.27), redPaint); | |
} | |
@override | |
bool shouldRepaint(_FloatingPainter oldDelegate) => false; | |
@override | |
bool shouldRebuildSemantics(_FloatingPainter oldDelegate) => false; | |
} | |
class SliverFloatingBar extends StatefulWidget { | |
const SliverFloatingBar({ | |
Key key, | |
this.leading, | |
this.automaticallyImplyLeading = true, | |
this.title, | |
this.trailing, | |
this.elevation = 5.0, | |
this.backgroundColor, | |
this.floating = false, | |
this.pinned = false, | |
this.snap = false, | |
}) : assert(automaticallyImplyLeading != null), | |
assert(floating != null), | |
assert(pinned != null), | |
assert(snap != null), | |
assert(floating || !snap, | |
'The "snap" argument only makes sense for floating app bars.'), | |
super(key: key); | |
final Widget leading; | |
final bool automaticallyImplyLeading; | |
final Widget title; | |
final Widget trailing; | |
final double elevation; | |
final Color backgroundColor; | |
final bool floating; | |
final bool pinned; | |
final bool snap; | |
@override | |
_SliverFloatingBarState createState() => _SliverFloatingBarState(); | |
} | |
class _SliverFloatingBarState extends State<SliverFloatingBar> | |
with TickerProviderStateMixin { | |
FloatingHeaderSnapConfiguration _snapConfiguration; | |
void _updateSnapConfiguration() { | |
if (widget.snap && widget.floating) { | |
_snapConfiguration = FloatingHeaderSnapConfiguration( | |
vsync: this, | |
curve: Curves.easeOut, | |
duration: const Duration(milliseconds: 200), | |
); | |
} else { | |
_snapConfiguration = null; | |
} | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_updateSnapConfiguration(); | |
} | |
@override | |
void didUpdateWidget(SliverFloatingBar oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating) | |
_updateSnapConfiguration(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final double topPadding = MediaQuery.of(context).padding.top; | |
final double collapsedHeight = | |
(widget.pinned && widget.floating) ? topPadding : null; | |
return MediaQuery.removePadding( | |
context: context, | |
removeBottom: true, | |
child: SliverPersistentHeader( | |
floating: widget.floating, | |
pinned: widget.pinned, | |
delegate: _SliverAppBarDelegate( | |
leading: widget.leading, | |
automaticallyImplyLeading: widget.automaticallyImplyLeading, | |
title: widget.title, | |
trailing: widget.trailing, | |
elevation: widget.elevation, | |
backgroundColor: widget.backgroundColor, | |
floating: widget.floating, | |
pinned: widget.pinned, | |
snapConfiguration: _snapConfiguration, | |
collapsedHeight: collapsedHeight, | |
topPadding: topPadding, | |
), | |
), | |
); | |
} | |
} | |
class _FloatingAppBar extends StatefulWidget { | |
const _FloatingAppBar({Key key, this.child}) : super(key: key); | |
final Widget child; | |
@override | |
_FloatingAppBarState createState() => _FloatingAppBarState(); | |
} | |
class _FloatingAppBarState extends State<_FloatingAppBar> { | |
ScrollPosition _position; | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
if (_position != null) | |
_position.isScrollingNotifier.removeListener(_isScrollingListener); | |
_position = Scrollable.of(context)?.position; | |
if (_position != null) | |
_position.isScrollingNotifier.addListener(_isScrollingListener); | |
} | |
@override | |
void dispose() { | |
if (_position != null) | |
_position.isScrollingNotifier.removeListener(_isScrollingListener); | |
super.dispose(); | |
} | |
RenderSliverFloatingPersistentHeader _headerRenderer() { | |
return context.ancestorRenderObjectOfType( | |
const TypeMatcher<RenderSliverFloatingPersistentHeader>()); | |
} | |
void _isScrollingListener() { | |
if (_position == null) return; | |
final RenderSliverFloatingPersistentHeader header = _headerRenderer(); | |
if (_position.isScrollingNotifier.value) | |
header?.maybeStopSnapAnimation(_position.userScrollDirection); | |
else | |
header?.maybeStartSnapAnimation(_position.userScrollDirection); | |
} | |
@override | |
Widget build(BuildContext context) => widget.child; | |
} | |
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { | |
_SliverAppBarDelegate({ | |
@required this.leading, | |
@required this.automaticallyImplyLeading, | |
@required this.title, | |
@required this.trailing, | |
@required this.elevation, | |
@required this.backgroundColor, | |
@required this.floating, | |
@required this.pinned, | |
@required this.snapConfiguration, | |
@required this.collapsedHeight, | |
@required this.topPadding, | |
}); | |
final Widget trailing; | |
final bool automaticallyImplyLeading; | |
final Color backgroundColor; | |
final double elevation; | |
final bool floating; | |
final Widget leading; | |
final bool pinned; | |
final Widget title; | |
final double collapsedHeight; | |
final double topPadding; | |
@override | |
double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight); | |
@override | |
final FloatingHeaderSnapConfiguration snapConfiguration; | |
@override | |
double get maxExtent => math.max(topPadding + (kToolbarHeight), minExtent); | |
@override | |
bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) { | |
return leading != oldDelegate.leading || | |
automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading || | |
title != oldDelegate.title || | |
trailing != oldDelegate.trailing || | |
elevation != oldDelegate.elevation || | |
topPadding != oldDelegate.topPadding || | |
collapsedHeight != oldDelegate.collapsedHeight || | |
backgroundColor != oldDelegate.backgroundColor || | |
pinned != oldDelegate.pinned || | |
floating != oldDelegate.floating || | |
snapConfiguration != oldDelegate.snapConfiguration; | |
} | |
@override | |
String toString() { | |
return ''; | |
} | |
@override | |
Widget build( | |
BuildContext context, double shrinkOffset, bool overlapsContent) { | |
final double visibleMainHeight = maxExtent - shrinkOffset; | |
final double toolbarOpacity = !pinned || (floating) | |
? ((visibleMainHeight) / kToolbarHeight).clamp(0.0, 1.0) | |
: 1.0; | |
final Widget appBar = FlexibleSpaceBar.createSettings( | |
minExtent: minExtent, | |
maxExtent: maxExtent, | |
currentExtent: math.max(minExtent, maxExtent - shrinkOffset), | |
toolbarOpacity: toolbarOpacity, | |
child: Container( | |
padding: EdgeInsets.symmetric(horizontal: 12.0), | |
child: SafeArea( | |
child: Material( | |
color: backgroundColor, | |
borderRadius: BorderRadius.circular(8.0), | |
elevation: elevation, | |
child: ListTile( | |
leading: leading ?? | |
(Scaffold.of(context).hasDrawer && automaticallyImplyLeading | |
? IconButton( | |
icon: Icon(Icons.menu), | |
onPressed: () { | |
Scaffold.of(context).openDrawer(); | |
}, | |
) | |
: null), | |
title: title, | |
trailing: trailing ?? | |
(Scaffold.of(context).hasEndDrawer && | |
automaticallyImplyLeading | |
? IconButton( | |
icon: Icon(Icons.menu), | |
onPressed: () { | |
Scaffold.of(context).openEndDrawer(); | |
}, | |
) | |
: null), | |
), | |
), | |
), | |
), | |
); | |
return Container(child: floating ? _FloatingAppBar(child: appBar) : appBar); | |
} | |
} | |
// -- Classes -- | |
class Email { | |
Email({ | |
this.recipients = const [], | |
this.dateReceived, | |
this.subject, | |
this.body, | |
this.isFavorite = false, | |
this.isRead = false, | |
this.attachments, | |
this.sender, | |
}); | |
List<EmailContact> recipients; | |
DateTime dateReceived; | |
String subject; | |
String body; | |
bool isFavorite = false; | |
bool isRead = false; | |
List<EmailAttachment> attachments; | |
EmailContact sender; | |
String get time { | |
final past12 = dateReceived.hour > 12; | |
final sb = StringBuffer(); | |
if (past12) { | |
sb.write('${dateReceived.hour - 12}'); | |
} else { | |
sb.write('${dateReceived.hour}'); | |
} | |
sb.write(':'); | |
if (dateReceived.minute < 10) { | |
sb.write('0'); | |
} | |
sb.write('${dateReceived.minute}'); | |
if (past12) { | |
sb.write('pm'); | |
} else { | |
sb.write('am'); | |
} | |
return sb.toString(); | |
} | |
Email copyWith({ | |
List<EmailContact> recipients, | |
DateTime dateReceived, | |
String subject, | |
String body, | |
bool isFavorite, | |
bool isRead, | |
List<EmailAttachment> attachments, | |
EmailContact sender, | |
}) { | |
return Email( | |
recipients: recipients ?? this.recipients, | |
dateReceived: dateReceived ?? this.dateReceived, | |
subject: subject ?? this.subject, | |
body: body ?? this.body, | |
isFavorite: isFavorite ?? this.isFavorite, | |
isRead: isRead ?? this.isRead, | |
attachments: attachments ?? this.attachments, | |
sender: sender ?? this.sender, | |
); | |
} | |
Map<String, dynamic> toMap() { | |
return { | |
'recipients': recipients?.map((x) => x?.toMap())?.toList(), | |
'dateReceived': dateReceived?.millisecondsSinceEpoch, | |
'subject': subject, | |
'body': body, | |
'isFavorite': isFavorite, | |
'isRead': isRead, | |
'attachments': attachments?.map((x) => x?.toMap())?.toList(), | |
'sender': sender?.toMap(), | |
}; | |
} | |
static Email fromMap(Map<String, dynamic> map) { | |
if (map == null) return null; | |
return Email( | |
recipients: List<EmailContact>.from( | |
map['recipients']?.map((x) => EmailContact.fromMap(x))), | |
dateReceived: DateTime.fromMillisecondsSinceEpoch(map['dateReceived']), | |
subject: map['subject'], | |
body: map['body'], | |
isFavorite: map['isFavorite'], | |
isRead: map['isRead'], | |
attachments: List<EmailAttachment>.from( | |
map['attachments']?.map((x) => EmailAttachment.fromMap(x))), | |
sender: EmailContact.fromMap(map['sender']), | |
); | |
} | |
String toJson() => json.encode(toMap()); | |
static Email fromJson(String source) => fromMap(json.decode(source)); | |
@override | |
String toString() { | |
return 'Email(recipients: $recipients, dateReceived: $dateReceived, subject: $subject, body: $body, isFavorite: $isFavorite, isRead: $isRead, attachments: $attachments, sender: $sender)'; | |
} | |
@override | |
bool operator ==(Object o) { | |
if (identical(this, o)) return true; | |
return o is Email && | |
listEquals(o.recipients, recipients) && | |
o.dateReceived == dateReceived && | |
o.subject == subject && | |
o.body == body && | |
o.isFavorite == isFavorite && | |
o.isRead == isRead && | |
listEquals(o.attachments, attachments) && | |
o.sender == sender; | |
} | |
@override | |
int get hashCode { | |
return recipients.hashCode ^ | |
dateReceived.hashCode ^ | |
subject.hashCode ^ | |
body.hashCode ^ | |
isFavorite.hashCode ^ | |
isRead.hashCode ^ | |
attachments.hashCode ^ | |
sender.hashCode; | |
} | |
} | |
class EmailAttachment { | |
String fileName; | |
EmailAttachment({ | |
this.fileName, | |
}); | |
String get ext => fileName.split('.').last; | |
EmailAttachment copyWith({ | |
String fileName, | |
}) { | |
return EmailAttachment( | |
fileName: fileName ?? this.fileName, | |
); | |
} | |
Map<String, dynamic> toMap() { | |
return { | |
'fileName': fileName, | |
}; | |
} | |
static EmailAttachment fromMap(Map<String, dynamic> map) { | |
if (map == null) return null; | |
return EmailAttachment( | |
fileName: map['fileName'], | |
); | |
} | |
String toJson() => json.encode(toMap()); | |
static EmailAttachment fromJson(String source) => | |
fromMap(json.decode(source)); | |
@override | |
String toString() => 'EmailAttachment(fileName: $fileName)'; | |
@override | |
bool operator ==(Object o) { | |
if (identical(this, o)) return true; | |
return o is EmailAttachment && o.fileName == fileName; | |
} | |
@override | |
int get hashCode => fileName.hashCode; | |
} | |
class EmailContact { | |
String firstName; | |
String lastName; | |
String photoUrl; | |
EmailContact({ | |
this.firstName, | |
this.lastName, | |
this.photoUrl, | |
}); | |
bool get hasImage => stringExists(photoUrl); | |
String get displayName => '${firstName ?? ''} ${lastName ?? ''}'.trim(); | |
String get letter => displayName.substring(0, 1); | |
EmailContact copyWith({ | |
String firstName, | |
String lastName, | |
String photoUrl, | |
}) { | |
return EmailContact( | |
firstName: firstName ?? this.firstName, | |
lastName: lastName ?? this.lastName, | |
photoUrl: photoUrl ?? this.photoUrl, | |
); | |
} | |
Map<String, dynamic> toMap() { | |
return { | |
'firstName': firstName, | |
'lastName': lastName, | |
'photoUrl': photoUrl, | |
}; | |
} | |
static EmailContact fromMap(Map<String, dynamic> map) { | |
if (map == null) return null; | |
return EmailContact( | |
firstName: map['firstName'], | |
lastName: map['lastName'], | |
photoUrl: map['photoUrl'], | |
); | |
} | |
String toJson() => json.encode(toMap()); | |
static EmailContact fromJson(String source) => fromMap(json.decode(source)); | |
@override | |
String toString() => | |
'EmailContact(firstName: $firstName, lastName: $lastName, photoUrl: $photoUrl)'; | |
@override | |
bool operator ==(Object o) { | |
if (identical(this, o)) return true; | |
return o is EmailContact && | |
o.firstName == firstName && | |
o.lastName == lastName && | |
o.photoUrl == photoUrl; | |
} | |
@override | |
int get hashCode => | |
firstName.hashCode ^ lastName.hashCode ^ photoUrl.hashCode; | |
} | |
_randomInt(int min, int max) { | |
math.Random rnd = new math.Random(); | |
return rnd.nextInt((max - min) + 1) + min; | |
} | |
/// Creates [numWords] number of random words. | |
String createWord({int numWords = 1}) { | |
math.Random _random; | |
if (numWords > 1) { | |
return createSentence(sentenceLength: numWords, numSentences: 1); | |
} | |
_random = math.Random(); | |
return words[_random.nextInt(words.length)]; | |
} | |
/// Creates random sentences. | |
/// | |
/// Sentences are either exactly [sentenceLength] words in length, or a randomly | |
/// generated length. [numSentences] defines the number of sentences generated. | |
/// Returned sentences are punctuated. | |
String createSentence({int sentenceLength = -1, int numSentences = 1}) { | |
int wordIndex; | |
String sentence; | |
if (numSentences > 1) return createParagraph(numSentences: numSentences); | |
if (sentenceLength < 0) { | |
sentenceLength = _randomInt(5, 20); | |
} | |
wordIndex = _randomInt(0, words.length - sentenceLength - 1); | |
sentence = words.getRange(wordIndex, wordIndex + sentenceLength).join(" "); | |
sentence = sentence[0].toUpperCase() + sentence.substring(1) + "."; | |
return sentence; | |
} | |
/// Creates random paragraphs. | |
/// | |
/// Paragraphs are comprised of a random number of sentences, or explicitly | |
/// [numSentences] long. [numParagraphs] specifies the number of paragraphs | |
/// to generate. | |
String createParagraph({int numSentences = -1, int numParagraphs = 1}) { | |
List<String> sentences = []; | |
if (numParagraphs > 1) | |
return createText(numSentences: numSentences, numParagraphs: numParagraphs); | |
if (numSentences < 0) { | |
numSentences = _randomInt(3, 5); | |
} | |
for (var i = 0; i < numSentences; i++) { | |
sentences.add(createSentence()); | |
} | |
return sentences.getRange(0, sentences.length).join(" "); | |
} | |
/// Creates a text comprised of a number of paragraphs. | |
/// | |
/// Each text is comprised of [numParagraphs] paragraphs, each of which | |
/// contain [numSentences] sentences. If either parameter is omitted, a | |
/// random number is generated. | |
String createText({int numParagraphs = -1, int numSentences = -1}) { | |
List<String> paragraphs = []; | |
if (numParagraphs < 0) { | |
numParagraphs = _randomInt(3, 7); | |
} | |
for (var i = 0; i < numParagraphs; i++) { | |
paragraphs.add('${createParagraph(numSentences: numSentences)}\n'); | |
} | |
return paragraphs.getRange(0, paragraphs.length).join("\n"); | |
} | |
List<String> words = [ | |
"lorem", | |
"ipsum", | |
"dolor", | |
"sit", | |
"amet", | |
"consectetur", | |
"adipiscing", | |
"elit", | |
"ut", | |
"aliquam", | |
"purus", | |
"sit", | |
"amet", | |
"luctus", | |
"venenatis", | |
"lectus", | |
"magna", | |
"fringilla", | |
"urna", | |
"porttitor", | |
"rhoncus", | |
"dolor", | |
"purus", | |
"non", | |
"enim", | |
"praesent", | |
"elementum", | |
"facilisis", | |
"leo", | |
"vel", | |
"fringilla", | |
"est", | |
"ullamcorper", | |
"eget", | |
"nulla", | |
"facilisi", | |
"etiam", | |
"dignissim", | |
"diam", | |
"quis", | |
"enim", | |
"lobortis", | |
"scelerisque", | |
"fermentum", | |
"dui", | |
"faucibus", | |
"in", | |
"ornare", | |
"quam", | |
"viverra", | |
"orci", | |
"sagittis", | |
"eu", | |
"volutpat", | |
"odio", | |
"facilisis", | |
"mauris", | |
"sit", | |
"amet", | |
"massa", | |
"vitae", | |
"tortor", | |
"condimentum", | |
"lacinia", | |
"quis", | |
"vel", | |
"eros", | |
"donec", | |
"ac", | |
"odio", | |
"tempor", | |
"orci", | |
"dapibus", | |
"ultrices", | |
"in", | |
"iaculis", | |
"nunc", | |
"sed", | |
"augue", | |
"lacus", | |
"viverra", | |
"vitae", | |
"congue", | |
"eu", | |
"consequat", | |
"ac", | |
"felis", | |
"donec", | |
"et", | |
"odio", | |
"pellentesque", | |
"diam", | |
"volutpat", | |
"commodo", | |
"sed", | |
"egestas", | |
"egestas", | |
"fringilla", | |
"phasellus", | |
"faucibus", | |
"scelerisque", | |
"eleifend", | |
"donec", | |
"pretium", | |
"vulputate", | |
"sapien", | |
"nec", | |
"sagittis", | |
"aliquam", | |
"malesuada", | |
"bibendum", | |
"arcu", | |
"vitae", | |
"elementum", | |
"curabitur", | |
"vitae", | |
"nunc", | |
"sed", | |
"velit", | |
"dignissim", | |
"sodales", | |
"ut", | |
"eu", | |
"sem", | |
"integer", | |
"vitae", | |
"justo", | |
"eget", | |
"magna", | |
"fermentum", | |
"iaculis", | |
"eu", | |
"non", | |
"diam", | |
"phasellus", | |
"vestibulum", | |
"lorem", | |
"sed", | |
"risus", | |
"ultricies", | |
"tristique", | |
"nulla", | |
"aliquet", | |
"enim", | |
"tortor", | |
"at", | |
"auctor", | |
"urna", | |
"nunc", | |
"id", | |
"cursus", | |
"metus", | |
"aliquam", | |
"eleifend", | |
"mi", | |
"in", | |
"nulla", | |
"posuere", | |
"sollicitudin", | |
"aliquam", | |
"ultrices", | |
"sagittis", | |
"orci", | |
"a", | |
"scelerisque", | |
"purus", | |
"semper", | |
"eget", | |
"duis", | |
"at", | |
"tellus", | |
"at", | |
"urna", | |
"condimentum", | |
"mattis", | |
"pellentesque", | |
"id", | |
"nibh", | |
"tortor", | |
"id", | |
"aliquet", | |
"lectus", | |
"proin", | |
"nibh", | |
"nisl", | |
"condimentum", | |
"id", | |
"venenatis", | |
"a", | |
"condimentum", | |
"vitae", | |
"sapien", | |
"pellentesque", | |
"habitant", | |
"morbi", | |
"tristique", | |
"senectus", | |
"et", | |
"netus", | |
"et", | |
"malesuada", | |
"fames", | |
"ac", | |
"turpis", | |
"egestas", | |
"sed", | |
"tempus", | |
"urna", | |
"et", | |
"pharetra", | |
"pharetra", | |
"massa", | |
"massa", | |
"ultricies", | |
"mi", | |
"quis", | |
"hendrerit", | |
"dolor", | |
"magna", | |
"eget", | |
"est", | |
"lorem", | |
"ipsum", | |
"dolor", | |
"sit", | |
"amet", | |
"consectetur", | |
"adipiscing", | |
"elit", | |
"pellentesque", | |
"habitant", | |
"morbi", | |
"tristique", | |
"senectus", | |
"et", | |
"netus", | |
"et", | |
"malesuada", | |
"fames", | |
"ac", | |
"turpis", | |
"egestas", | |
"integer", | |
"eget", | |
"aliquet", | |
"nibh", | |
"praesent", | |
"tristique", | |
"magna", | |
"sit", | |
"amet", | |
"purus", | |
"gravida", | |
"quis", | |
"blandit", | |
"turpis", | |
"cursus", | |
"in", | |
"hac", | |
"habitasse", | |
"platea", | |
"dictumst", | |
"quisque", | |
"sagittis", | |
"purus", | |
"sit", | |
"amet", | |
"volutpat", | |
"consequat", | |
"mauris", | |
"nunc", | |
"congue", | |
"nisi", | |
"vitae", | |
"suscipit", | |
"tellus", | |
"mauris", | |
"a", | |
"diam", | |
"maecenas", | |
"sed", | |
"enim", | |
"ut", | |
"sem", | |
"viverra", | |
"aliquet", | |
"eget", | |
"sit", | |
"amet", | |
"tellus", | |
"cras", | |
"adipiscing", | |
"enim", | |
"eu", | |
"turpis", | |
"egestas", | |
"pretium", | |
"aenean", | |
"pharetra", | |
"magna", | |
"ac", | |
"placerat", | |
"vestibulum", | |
"lectus", | |
"mauris", | |
"ultrices", | |
"eros", | |
"in", | |
"cursus", | |
"turpis", | |
"massa", | |
"tincidunt", | |
"dui", | |
"ut", | |
"ornare", | |
"lectus", | |
"sit", | |
"amet", | |
"est", | |
"placerat", | |
"in", | |
"egestas", | |
"erat", | |
"imperdiet", | |
"sed", | |
"euismod", | |
"nisi", | |
"porta", | |
"lorem", | |
"mollis", | |
"aliquam", | |
"ut", | |
"porttitor", | |
"leo", | |
"a", | |
"diam", | |
"sollicitudin", | |
"tempor", | |
"id", | |
"eu", | |
"nisl", | |
"nunc", | |
"mi", | |
"ipsum", | |
"faucibus", | |
"vitae", | |
"aliquet", | |
"nec", | |
"ullamcorper", | |
"sit", | |
"amet", | |
"risus", | |
"nullam", | |
"eget", | |
"felis", | |
"eget", | |
"nunc", | |
"lobortis", | |
"mattis", | |
"aliquam", | |
"faucibus", | |
"purus", | |
"in", | |
"massa", | |
"tempor", | |
"nec", | |
"feugiat", | |
"nisl", | |
"pretium", | |
"fusce", | |
"id", | |
"velit", | |
"ut", | |
"tortor", | |
"pretium", | |
"viverra", | |
"suspendisse", | |
"potenti", | |
"nullam", | |
"ac", | |
"tortor", | |
"vitae", | |
"purus", | |
"faucibus", | |
"ornare", | |
"suspendisse", | |
"sed", | |
"nisi", | |
"lacus", | |
"sed", | |
"viverra", | |
"tellus", | |
"in", | |
"hac", | |
"habitasse", | |
"platea", | |
"dictumst", | |
"vestibulum", | |
"rhoncus", | |
"est", | |
"pellentesque", | |
"elit", | |
"ullamcorper", | |
"dignissim", | |
"cras", | |
"tincidunt", | |
"lobortis", | |
"feugiat", | |
"vivamus", | |
"at", | |
"augue", | |
"eget", | |
"arcu", | |
"dictum", | |
"varius", | |
"duis", | |
"at", | |
"consectetur", | |
"lorem", | |
"donec", | |
"massa", | |
"sapien", | |
"faucibus", | |
"et", | |
"molestie", | |
"ac", | |
"feugiat", | |
"sed", | |
"lectus", | |
"vestibulum", | |
"mattis", | |
"ullamcorper", | |
"velit", | |
"sed", | |
"ullamcorper", | |
"morbi", | |
"tincidunt", | |
"ornare", | |
"massa", | |
"eget", | |
"egestas", | |
"purus", | |
"viverra", | |
"accumsan", | |
"in", | |
"nisl", | |
"nisi", | |
"scelerisque", | |
"eu", | |
"ultrices", | |
"vitae", | |
"auctor", | |
"eu", | |
"augue", | |
"ut", | |
"lectus", | |
"arcu", | |
"bibendum", | |
"at", | |
"varius", | |
"vel", | |
"pharetra", | |
"vel", | |
"turpis", | |
"nunc", | |
"eget", | |
"lorem", | |
"dolor", | |
"sed", | |
"viverra", | |
"ipsum", | |
"nunc", | |
"aliquet", | |
"bibendum", | |
"enim", | |
"facilisis", | |
"gravida", | |
"neque", | |
"convallis", | |
"a", | |
"cras", | |
"semper", | |
"auctor", | |
"neque", | |
"vitae", | |
"tempus", | |
"quam", | |
"pellentesque", | |
"nec", | |
"nam", | |
"aliquam", | |
"sem", | |
"et", | |
"tortor", | |
"consequat", | |
"id", | |
"porta", | |
"nibh", | |
"venenatis", | |
"cras", | |
"sed", | |
"felis", | |
"eget", | |
"velit", | |
"aliquet", | |
"sagittis", | |
"id", | |
"consectetur", | |
"purus", | |
"ut", | |
"faucibus", | |
"pulvinar", | |
"elementum", | |
"integer", | |
"enim", | |
"neque", | |
"volutpat", | |
"ac", | |
"tincidunt", | |
"vitae", | |
"semper", | |
"quis", | |
"lectus", | |
"nulla", | |
"at", | |
"volutpat", | |
"diam", | |
"ut", | |
"venenatis", | |
"tellus", | |
"in", | |
"metus", | |
"vulputate", | |
"eu", | |
"scelerisque", | |
"felis", | |
"imperdiet", | |
"proin", | |
"fermentum", | |
"leo", | |
"vel", | |
"orci", | |
"porta", | |
"non", | |
"pulvinar", | |
"neque", | |
"laoreet", | |
"suspendisse", | |
"interdum", | |
"consectetur", | |
"libero", | |
"id", | |
"faucibus", | |
"nisl", | |
"tincidunt", | |
"eget", | |
"nullam", | |
"non", | |
"nisi", | |
"est", | |
"sit", | |
"amet", | |
"facilisis", | |
"magna", | |
"etiam", | |
"tempor", | |
"orci", | |
"eu", | |
"lobortis", | |
"elementum", | |
"nibh", | |
"tellus", | |
"molestie", | |
"nunc", | |
"non", | |
"blandit", | |
"massa", | |
"enim", | |
"nec", | |
"dui", | |
"nunc", | |
"mattis", | |
"enim", | |
"ut", | |
"tellus", | |
"elementum", | |
"sagittis", | |
"vitae", | |
"et", | |
"leo", | |
"duis", | |
"ut", | |
"diam", | |
"quam", | |
"nulla", | |
"porttitor", | |
"massa", | |
"id", | |
"neque", | |
"aliquam", | |
"vestibulum", | |
"morbi", | |
"blandit", | |
"cursus", | |
"risus", | |
"at", | |
"ultrices", | |
"mi", | |
"tempus", | |
"imperdiet", | |
"nulla", | |
"malesuada", | |
"pellentesque", | |
"elit", | |
"eget", | |
"gravida", | |
"cum", | |
"sociis", | |
"natoque", | |
"penatibus", | |
"et", | |
"magnis", | |
"dis", | |
"parturient", | |
"montes", | |
"nascetur", | |
"ridiculus", | |
"mus", | |
"mauris", | |
"vitae", | |
"ultricies", | |
"leo", | |
"integer", | |
"malesuada", | |
"nunc", | |
"vel", | |
"risus", | |
"commodo", | |
"viverra", | |
"maecenas", | |
"accumsan", | |
"lacus", | |
"vel", | |
"facilisis", | |
"volutpat", | |
"est", | |
"velit", | |
"egestas", | |
"dui", | |
"id", | |
"ornare", | |
"arcu", | |
"odio", | |
"ut", | |
"sem", | |
"nulla", | |
"pharetra", | |
"diam", | |
"sit", | |
"amet", | |
"nisl", | |
"suscipit", | |
"adipiscing", | |
"bibendum", | |
"est", | |
"ultricies", | |
"integer", | |
"quis", | |
"auctor", | |
"elit", | |
"sed", | |
"vulputate", | |
"mi", | |
"sit", | |
"amet", | |
"mauris", | |
"commodo", | |
"quis", | |
"imperdiet", | |
"massa", | |
"tincidunt", | |
"nunc", | |
"pulvinar", | |
"sapien", | |
"et", | |
"ligula", | |
"ullamcorper", | |
"malesuada", | |
"proin", | |
"libero", | |
"nunc", | |
"consequat", | |
"interdum", | |
"varius", | |
"sit", | |
"amet", | |
"mattis", | |
"vulputate", | |
"enim", | |
"nulla", | |
"aliquet", | |
"porttitor", | |
"lacus", | |
"luctus", | |
"accumsan", | |
"tortor", | |
"posuere", | |
"ac", | |
"ut", | |
"consequat", | |
"semper", | |
"viverra", | |
"nam", | |
"libero", | |
"justo", | |
"laoreet", | |
"sit", | |
"amet", | |
"cursus", | |
"sit", | |
"amet", | |
"dictum", | |
"sit", | |
"amet", | |
"justo", | |
"donec", | |
"enim", | |
"diam", | |
"vulputate", | |
"ut", | |
"pharetra", | |
"sit", | |
"amet", | |
"aliquam", | |
"id", | |
"diam", | |
"maecenas", | |
"ultricies", | |
"mi", | |
"eget", | |
"mauris", | |
"pharetra", | |
"et", | |
"ultrices", | |
"neque", | |
"ornare", | |
"aenean", | |
"euismod", | |
"elementum", | |
"nisi", | |
"quis", | |
"eleifend", | |
"quam", | |
"adipiscing", | |
"vitae", | |
"proin", | |
"sagittis", | |
"nisl", | |
"rhoncus", | |
"mattis", | |
"rhoncus", | |
"urna", | |
"neque", | |
"viverra", | |
"justo", | |
"nec", | |
"ultrices", | |
"dui", | |
"sapien", | |
"eget", | |
"mi", | |
"proin", | |
"sed", | |
"libero", | |
"enim", | |
"sed", | |
"faucibus", | |
"turpis", | |
"in", | |
"eu", | |
"mi", | |
"bibendum", | |
"neque", | |
"egestas", | |
"congue", | |
"quisque", | |
"egestas", | |
"diam", | |
"in", | |
"arcu", | |
"cursus", | |
"euismod", | |
"quis", | |
"viverra", | |
"nibh", | |
"cras", | |
"pulvinar", | |
"mattis", | |
"nunc", | |
"sed", | |
"blandit", | |
"libero", | |
"volutpat", | |
"sed", | |
"cras", | |
"ornare", | |
"arcu", | |
"dui", | |
"vivamus", | |
"arcu", | |
"felis", | |
"bibendum", | |
"ut", | |
"tristique", | |
"et", | |
"egestas", | |
"quis", | |
"ipsum", | |
"suspendisse", | |
"ultrices", | |
"fusce", | |
"ut", | |
"placerat", | |
"orci", | |
"nulla", | |
"pellentesque", | |
"dignissim", | |
"enim", | |
"sit", | |
"amet", | |
"venenatis", | |
"urna", | |
"cursus", | |
"eget", | |
"nunc", | |
"scelerisque", | |
"viverra", | |
"mauris", | |
"in", | |
"aliquam", | |
"sem", | |
"fringilla", | |
"ut", | |
"morbi", | |
"tincidunt", | |
"augue", | |
"interdum", | |
"velit", | |
"euismod", | |
"in", | |
"pellentesque", | |
"massa", | |
"placerat", | |
"duis", | |
"ultricies", | |
"lacus", | |
"sed", | |
"turpis", | |
"tincidunt", | |
"id", | |
"aliquet", | |
"risus", | |
"feugiat", | |
"in", | |
"ante", | |
"metus", | |
"dictum", | |
"at", | |
"tempor", | |
"commodo", | |
"ullamcorper", | |
"a", | |
"lacus", | |
"vestibulum", | |
"sed", | |
"arcu", | |
"non", | |
"odio", | |
"euismod", | |
"lacinia", | |
"at", | |
"quis", | |
"risus", | |
"sed", | |
"vulputate", | |
"odio", | |
"ut", | |
"enim", | |
"blandit", | |
"volutpat", | |
"maecenas", | |
"volutpat", | |
"blandit", | |
"aliquam", | |
"etiam", | |
"erat", | |
"velit", | |
"scelerisque", | |
"in", | |
"dictum", | |
"non", | |
"consectetur", | |
"a", | |
"erat", | |
"nam", | |
"at", | |
"lectus", | |
"urna", | |
"duis", | |
"convallis", | |
"convallis", | |
"tellus", | |
"id", | |
"interdum", | |
"velit", | |
"laoreet", | |
"id", | |
"donec", | |
"ultrices", | |
"tincidunt", | |
"arcu", | |
"non", | |
"sodales", | |
"neque", | |
"sodales", | |
"ut", | |
"etiam", | |
"sit", | |
"amet", | |
"nisl", | |
"purus", | |
"in", | |
"mollis", | |
"nunc", | |
"sed", | |
"id", | |
"semper", | |
"risus", | |
"in", | |
"hendrerit", | |
"gravida", | |
"rutrum", | |
"quisque", | |
"non", | |
"tellus", | |
"orci", | |
"ac", | |
"auctor", | |
"augue", | |
"mauris", | |
"augue", | |
"neque", | |
"gravida", | |
"in", | |
"fermentum", | |
"et", | |
"sollicitudin", | |
"ac", | |
"orci", | |
"phasellus", | |
"egestas", | |
"tellus", | |
"rutrum", | |
"tellus", | |
"pellentesque", | |
"eu", | |
"tincidunt", | |
"tortor", | |
"aliquam", | |
"nulla", | |
"facilisi", | |
"cras", | |
"fermentum", | |
"odio", | |
"eu", | |
"feugiat", | |
"pretium", | |
"nibh", | |
"ipsum", | |
"consequat", | |
"nisl", | |
"vel", | |
"pretium", | |
"lectus", | |
"quam", | |
"id", | |
"leo", | |
"in", | |
"vitae", | |
"turpis", | |
"massa", | |
"sed", | |
"elementum", | |
"tempus", | |
"egestas", | |
"sed", | |
"sed", | |
"risus", | |
"pretium", | |
"quam", | |
"vulputate", | |
"dignissim", | |
"suspendisse", | |
"in", | |
"est", | |
"ante", | |
"in", | |
"nibh", | |
"mauris", | |
"cursus", | |
"mattis", | |
"molestie", | |
"a", | |
"iaculis", | |
"at", | |
"erat", | |
"pellentesque", | |
"adipiscing", | |
"commodo", | |
"elit", | |
"at", | |
"imperdiet", | |
"dui", | |
"accumsan", | |
"sit", | |
"amet", | |
"nulla", | |
"facilisi", | |
"morbi", | |
"tempus", | |
"iaculis", | |
"urna", | |
"id", | |
"volutpat", | |
"lacus", | |
"laoreet", | |
"non", | |
"curabitur", | |
"gravida", | |
"arcu", | |
"ac", | |
"tortor", | |
"dignissim", | |
"convallis", | |
"aenean", | |
"et", | |
"tortor", | |
"at", | |
"risus", | |
"viverra", | |
"adipiscing", | |
"at", | |
"in", | |
"tellus", | |
"integer", | |
"feugiat", | |
"scelerisque", | |
"varius", | |
"morbi", | |
"enim", | |
"nunc", | |
"faucibus", | |
"a", | |
"pellentesque", | |
"sit", | |
"amet", | |
"porttitor", | |
"eget", | |
"dolor", | |
"morbi", | |
"non", | |
"arcu", | |
"risus", | |
"quis", | |
"varius", | |
"quam", | |
"quisque", | |
"id", | |
"diam", | |
"vel", | |
"quam", | |
"elementum", | |
"pulvinar", | |
"etiam", | |
"non", | |
"quam", | |
"lacus", | |
"suspendisse", | |
"faucibus", | |
"interdum", | |
"posuere", | |
"lorem", | |
"ipsum", | |
"dolor", | |
"sit", | |
"amet", | |
"consectetur", | |
"adipiscing", | |
"elit", | |
"duis", | |
"tristique", | |
"sollicitudin", | |
"nibh", | |
"sit", | |
"amet", | |
"commodo", | |
"nulla", | |
"facilisi", | |
"nullam", | |
"vehicula", | |
"ipsum", | |
"a", | |
"arcu", | |
"cursus", | |
"vitae", | |
"congue", | |
"mauris", | |
"rhoncus", | |
"aenean", | |
"vel", | |
"elit", | |
"scelerisque", | |
"mauris", | |
"pellentesque", | |
"pulvinar", | |
"pellentesque", | |
"habitant", | |
"morbi", | |
"tristique", | |
"senectus", | |
"et", | |
"netus", | |
"et", | |
"malesuada", | |
"fames", | |
"ac", | |
"turpis", | |
"egestas", | |
"maecenas", | |
"pharetra", | |
"convallis", | |
"posuere", | |
"morbi", | |
"leo", | |
"urna", | |
"molestie", | |
"at", | |
"elementum", | |
"eu", | |
"facilisis", | |
"sed", | |
"odio", | |
"morbi", | |
"quis", | |
"commodo", | |
"odio", | |
"aenean", | |
"sed", | |
"adipiscing", | |
"diam", | |
"donec", | |
"adipiscing", | |
"tristique", | |
"risus", | |
"nec", | |
"feugiat", | |
"in", | |
"fermentum", | |
"posuere", | |
"urna", | |
"nec", | |
"tincidunt", | |
"praesent", | |
"semper", | |
"feugiat", | |
"nibh", | |
"sed", | |
"pulvinar", | |
"proin", | |
"gravida", | |
"hendrerit", | |
"lectus", | |
"a", | |
"molestie", | |
"gravida", | |
"dictum" | |
]; | |
// -- Extensions -- | |
const kTabletBreakpoint = 720.0; | |
const kListWidth = 370.0; | |
const kDrawerWidth = 250.0; | |
bool stringExists(String val) { | |
return val != null && val.isNotEmpty; | |
} | |
Color get inactiveColor => Colors.grey[600]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment