Added support for arbitrary lists and experience in markup
This commit is contained in:
parent
976c3d0679
commit
aa71e8a9e6
|
@ -2,9 +2,11 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dartboard_resume/dartboard_parser.dart';
|
||||||
import 'package:dartboard_resume/render.dart';
|
import 'package:dartboard_resume/render.dart';
|
||||||
import 'package:hotreloader/hotreloader.dart';
|
import 'package:hotreloader/hotreloader.dart';
|
||||||
import 'package:logging/logging.dart' as logging;
|
import 'package:logging/logging.dart' as logging;
|
||||||
|
import 'package:toml/toml.dart';
|
||||||
|
|
||||||
StreamSubscription<FileSystemEvent>? fileStreamSub;
|
StreamSubscription<FileSystemEvent>? fileStreamSub;
|
||||||
StreamSubscription<String>? stdinStreamSub;
|
StreamSubscription<String>? stdinStreamSub;
|
||||||
|
@ -24,6 +26,12 @@ Future<void> main(List<String> arguments) async {
|
||||||
stdout.writeln("Triggering pdf render...");
|
stdout.writeln("Triggering pdf render...");
|
||||||
renderPdf(tomlFilePath, force: true);
|
renderPdf(tomlFilePath, force: true);
|
||||||
}
|
}
|
||||||
|
if (event == "p") {
|
||||||
|
stdout.writeln("Current toml map:");
|
||||||
|
stdout.writeln(TomlDocument.loadSync(tomlFilePath).toMap());
|
||||||
|
final dartboardData = DartboardData.fromToml(tomlFilePath);
|
||||||
|
dartboardData.miscList.forEach(stdout.writeln);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,10 @@ class DartboardData {
|
||||||
final tomlData = TomlDocument.loadSync(tomlFilePath).toMap();
|
final tomlData = TomlDocument.loadSync(tomlFilePath).toMap();
|
||||||
|
|
||||||
final List<DartboardExperience> exps = tomlData.entries
|
final List<DartboardExperience> exps = tomlData.entries
|
||||||
.where((e) => e.value is List && (e.value as List).firstOrNull is Map)
|
.where((e) => DartboardExperience.filter(e.value))
|
||||||
.map((MapEntry<String, dynamic> expsEntry) {
|
.map((MapEntry<String, dynamic> expsEntry) {
|
||||||
final String subsection = tomlData['${expsEntry.key}_name'] as String? ?? '';
|
final String subsection =
|
||||||
|
tomlData['${expsEntry.key}_name'] as String? ?? _getSubsectionFromKey(expsEntry.key);
|
||||||
final exps = (expsEntry.value as List).map((e) => e as Map<String, dynamic>).toList();
|
final exps = (expsEntry.value as List).map((e) => e as Map<String, dynamic>).toList();
|
||||||
return exps.map((exp) {
|
return exps.map((exp) {
|
||||||
final TomlLocalDate? start = exp['start'] as TomlLocalDate?;
|
final TomlLocalDate? start = exp['start'] as TomlLocalDate?;
|
||||||
|
@ -35,34 +36,47 @@ class DartboardData {
|
||||||
start: start == null ? null : DateTime(start.date.year, start.date.month),
|
start: start == null ? null : DateTime(start.date.year, start.date.month),
|
||||||
end: end == null ? null : DateTime(end.date.year, end.date.month),
|
end: end == null ? null : DateTime(end.date.year, end.date.month),
|
||||||
),
|
),
|
||||||
attributes: (exp['attributes'] as List).map((e) => DartboardText(content: e as String)).toList(),
|
attributes: (exp['attributes'] as List)
|
||||||
|
.where((e) => e is String && e.isNotEmpty)
|
||||||
|
.map((e) => DartboardText(content: e as String))
|
||||||
|
.toList(),
|
||||||
location: exp['location'] as String?,
|
location: exp['location'] as String?,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
})
|
})
|
||||||
.expand((i) => i)
|
.expand((i) => i)
|
||||||
.toList();
|
.toList();
|
||||||
|
final List<DartboardMisc> misc =
|
||||||
|
tomlData.entries.where((e) => DartboardMisc.filter(e.value)).map((MapEntry<String, dynamic> listEntry) {
|
||||||
|
final String subsection = tomlData['${listEntry.key}_name'] as String? ?? _getSubsectionFromKey(listEntry.key);
|
||||||
|
return DartboardMisc(
|
||||||
|
subsection: subsection,
|
||||||
|
attributes: (listEntry.value as List)
|
||||||
|
.map((e) => (e as Map<String, dynamic>)['attributes'] as List)
|
||||||
|
.expand((i) => i)
|
||||||
|
.where((e) => e is String && e.isNotEmpty)
|
||||||
|
.map((e) => DartboardText(content: e as String))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
return DartboardData(
|
return DartboardData(
|
||||||
fullName: tomlData['full_name'] as String,
|
fullName: tomlData['full_name'] as String,
|
||||||
phoneNumber: tomlData['phone_number'] as String,
|
phoneNumber: tomlData['phone_number'] as String?,
|
||||||
email: tomlData['email'] as String,
|
email: tomlData['email'] as String?,
|
||||||
imagePath: tomlData['image'] as String,
|
imagePath: tomlData['image'] as String?,
|
||||||
dartboardTheme: DartboardTheme.fromToml(tomlData),
|
dartboardTheme: DartboardTheme.fromToml(tomlData),
|
||||||
experiences: exps,
|
experiences: exps,
|
||||||
miscList: [
|
miscList: misc,
|
||||||
DartboardList(subsection: 'Skills & Interest', content: 'Proficient in flutter and other'),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String fullName;
|
final String fullName;
|
||||||
final String phoneNumber;
|
final String? phoneNumber;
|
||||||
final String email;
|
final String? email;
|
||||||
// final String address;
|
final String? imagePath;
|
||||||
final String imagePath;
|
|
||||||
final DartboardTheme dartboardTheme;
|
final DartboardTheme dartboardTheme;
|
||||||
final List<DartboardExperience> experiences;
|
final List<DartboardExperience> experiences;
|
||||||
final List<DartboardList> miscList;
|
final List<DartboardMisc> miscList;
|
||||||
|
|
||||||
Font get font => dartboardTheme.font;
|
Font get font => dartboardTheme.font;
|
||||||
|
|
||||||
|
@ -86,9 +100,20 @@ class DartboardData {
|
||||||
return exps;
|
return exps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, List<DartboardMisc>> get groupedMisc {
|
||||||
|
final Map<String, List<DartboardMisc>> miscs = {};
|
||||||
|
for (final DartboardMisc misc in miscList) {
|
||||||
|
if (!miscs.containsKey(misc.subsection)) {
|
||||||
|
miscs[misc.subsection] = <DartboardMisc>[];
|
||||||
|
}
|
||||||
|
miscs[misc.subsection]!.add(misc);
|
||||||
|
}
|
||||||
|
return miscs;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
return Object.hashAll([fullName, phoneNumber, email, imagePath, ...experiences]);
|
return Object.hashAll([fullName, phoneNumber, email, imagePath, ...experiences, ...miscList]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -99,17 +124,25 @@ class DartboardData {
|
||||||
|
|
||||||
class DartboardExperience {
|
class DartboardExperience {
|
||||||
DartboardExperience({
|
DartboardExperience({
|
||||||
|
required this.subsection,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.attributes,
|
required this.attributes,
|
||||||
required this.range,
|
this.range,
|
||||||
required this.subsection,
|
|
||||||
this.location,
|
this.location,
|
||||||
});
|
});
|
||||||
final String subsection;
|
final String subsection;
|
||||||
final String title;
|
final String title;
|
||||||
String? location;
|
|
||||||
final List<DartboardText> attributes;
|
final List<DartboardText> attributes;
|
||||||
final DateRange range;
|
final DateRange? range;
|
||||||
|
String? location;
|
||||||
|
|
||||||
|
static bool filter(dynamic e) {
|
||||||
|
if (e is! List || !e.every((f) => f is Map)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final List<Map<String, dynamic>> entries = e.map((f) => f as Map<String, dynamic>).toList();
|
||||||
|
return entries.every((f) => f['title'] is String && f['attributes'] is List && f.keys.length >= 2);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
|
@ -153,19 +186,29 @@ class DateRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DartboardList {
|
class DartboardMisc {
|
||||||
DartboardList({required this.subsection, required this.content});
|
DartboardMisc({required this.subsection, required this.attributes});
|
||||||
|
|
||||||
final String subsection;
|
final String subsection;
|
||||||
final String content;
|
final List<DartboardText> attributes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
return Object.hashAll([subsection, content]);
|
return Object.hashAll([subsection, ...attributes]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return super.hashCode == other.hashCode;
|
return super.hashCode == other.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool filter(dynamic e) {
|
||||||
|
if (e is! List || e.firstOrNull is! Map) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final entries = (e.first as Map).entries;
|
||||||
|
return entries.length == 1 && entries.first.key == 'attributes';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DartboardTheme {
|
class DartboardTheme {
|
||||||
|
@ -277,5 +320,13 @@ class DartboardText {
|
||||||
String _getSubsectionFromKey(String key) {
|
String _getSubsectionFromKey(String key) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'exp':
|
case 'exp':
|
||||||
|
return 'Experience';
|
||||||
|
case 'misc':
|
||||||
|
return 'Miscelaneous';
|
||||||
|
case 'edu':
|
||||||
|
return 'Education';
|
||||||
|
case '':
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dartboard_resume/dartboard_parser.dart';
|
|
||||||
import 'package:dartboard_resume/dartboard_widgets.dart';
|
|
||||||
import 'package:pdf/pdf.dart';
|
|
||||||
import 'package:pdf/widgets.dart';
|
|
||||||
|
|
||||||
Page generatePdfPage({required DartboardData dartboardData, required int renderNs}) {
|
|
||||||
final List<Widget> groupedExperienceList = dartboardData.groupedExperiences.entries.map<Widget>(
|
|
||||||
(entry) {
|
|
||||||
final String subsection = entry.key;
|
|
||||||
final List<DartboardExperience> experiences = entry.value;
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
subsection,
|
|
||||||
style: dartboardData.subheaderTextStyle
|
|
||||||
.merge(const TextStyle(fontSize: 18))
|
|
||||||
.apply(color: const PdfColorGrey(0.2)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Container(height: 2, width: 200, color: const PdfColorGrey(0.7)),
|
|
||||||
...experiences.map(
|
|
||||||
(DartboardExperience exp) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(exp.title, style: dartboardData.subheaderTextStyle.apply(color: const PdfColorGrey(0.3))),
|
|
||||||
Text(
|
|
||||||
exp.range.toString(),
|
|
||||||
style: dartboardData.subheaderTextStyle.apply(color: const PdfColorGrey(0.42)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
...exp.attributes.map(
|
|
||||||
(a) => Text(
|
|
||||||
"${dartboardData.dartboardTheme.bulletPoint} $a",
|
|
||||||
style: dartboardData.defaultTextStyle.apply(
|
|
||||||
color: const PdfColorGrey(0.55),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
DartboardFooter(dartboardData: dartboardData, renderNs: renderNs),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toList();
|
|
||||||
return Page(
|
|
||||||
pageTheme: const PageTheme(pageFormat: PdfPageFormat.standard),
|
|
||||||
build: (Context context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 120,
|
|
||||||
width: double.infinity,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
child: Container(
|
|
||||||
height: 100,
|
|
||||||
width: 100,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
image: DecorationImage(
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
image: MemoryImage(
|
|
||||||
File(dartboardData.imagePath).readAsBytesSync(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(dartboardData.fullName, style: dartboardData.headerTextStyle),
|
|
||||||
Text(dartboardData.phoneNumber, style: dartboardData.headerTextStyle),
|
|
||||||
Text(dartboardData.email, style: dartboardData.headerTextStyle),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(height: 20),
|
|
||||||
...groupedExperienceList,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -10,18 +10,21 @@ class DartboardFooter extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(Context context) {
|
Widget build(Context context) {
|
||||||
final double renderTimeMs = renderNs.toDouble() / 1000.0;
|
final double renderTimeMs = renderNs.toDouble() / 1000.0;
|
||||||
return Row(
|
return Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
children: [
|
child: Row(
|
||||||
Text(
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
'This resume was generated with DartBoard Resume',
|
children: [
|
||||||
style: dartboardData.defaultTextStyle.copyWith(fontSize: 10),
|
Text(
|
||||||
),
|
'This resume was generated with DartBoard Resume',
|
||||||
Text(
|
style: dartboardData.defaultTextStyle.copyWith(fontSize: 10),
|
||||||
'Rendered in ${renderTimeMs.toStringAsFixed(2)}ms',
|
),
|
||||||
style: dartboardData.defaultTextStyle.copyWith(fontSize: 10),
|
Text(
|
||||||
),
|
'Rendered in ${renderTimeMs.toStringAsFixed(2)}ms',
|
||||||
],
|
style: dartboardData.defaultTextStyle.copyWith(fontSize: 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,24 +43,27 @@ class DartboardExperienceEntry extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
children: [
|
child: Column(
|
||||||
Text(exp.title, style: dartboardData.subheaderTextStyle.apply(color: const PdfColorGrey(0.3))),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Container(height: 1, width: 60, color: const PdfColorGrey(0.75)),
|
children: [
|
||||||
],
|
Text(exp.title, style: dartboardData.subheaderTextStyle.apply(color: const PdfColorGrey(0.3))),
|
||||||
|
Container(height: 1, width: 60, color: const PdfColorGrey(0.75)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
exp.range.toString(),
|
exp.range.toString(),
|
||||||
style: dartboardData.defaultTextStyle.apply(color: const PdfColorGrey(0.42)),
|
style: dartboardData.defaultTextStyle.copyWith(color: const PdfColorGrey(0.42), fontSize: 8.0),
|
||||||
),
|
),
|
||||||
if (exp.location != null)
|
if (exp.location != null)
|
||||||
Text(
|
Text(
|
||||||
exp.location!,
|
exp.location!,
|
||||||
style: dartboardData.defaultTextStyle.apply(color: const PdfColorGrey(0.42)),
|
style: dartboardData.defaultTextStyle.copyWith(color: const PdfColorGrey(0.42), fontSize: 8.0),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -73,15 +79,86 @@ class DartboardExperienceEntry extends StatelessWidget {
|
||||||
color: const PdfColorGrey(0.55),
|
color: const PdfColorGrey(0.55),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
padding: const EdgeInsets.only(left: 8.0, bottom: 4.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DartboardMiscEntry extends StatelessWidget {
|
||||||
|
DartboardMiscEntry({required this.dartboardData, required this.misc});
|
||||||
|
final DartboardData dartboardData;
|
||||||
|
final DartboardMisc misc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(Context context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
...misc.attributes.map(
|
||||||
|
(a) => Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, bottom: 4.0),
|
||||||
|
child: DartboardTextWithLink(
|
||||||
|
stringSections: a.toTextLinkList(),
|
||||||
|
bulletString: dartboardData.dartboardTheme.bulletPoint,
|
||||||
|
style: dartboardData.defaultTextStyle.apply(
|
||||||
|
color: const PdfColorGrey(0.55),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UrlFootnotes {
|
||||||
|
// Factory constructor to return the single instance
|
||||||
|
factory UrlFootnotes() {
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private constructor
|
||||||
|
UrlFootnotes._privateConstructor();
|
||||||
|
|
||||||
|
// Static field to hold the single instance of the class
|
||||||
|
static final UrlFootnotes _instance = UrlFootnotes._privateConstructor();
|
||||||
|
|
||||||
|
// Field to hold the number
|
||||||
|
int _numUrls = 0;
|
||||||
|
List<Widget> _urlWidgets = [];
|
||||||
|
TextStyle? _style;
|
||||||
|
|
||||||
|
// ignore: avoid_setters_without_getters
|
||||||
|
set style(TextStyle style) => _style = style;
|
||||||
|
|
||||||
|
int add({required String url}) {
|
||||||
|
_numUrls += 1;
|
||||||
|
if (_style == null) {
|
||||||
|
throw Exception('Must provide text style for urls');
|
||||||
|
}
|
||||||
|
_urlWidgets.add(Footnote(number: _numUrls, style: _style!, url: url));
|
||||||
|
print(_urlWidgets);
|
||||||
|
return _numUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> get footnotes {
|
||||||
|
final widgets = [..._urlWidgets];
|
||||||
|
reset();
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to reset the number
|
||||||
|
void reset() {
|
||||||
|
_numUrls = 0;
|
||||||
|
_urlWidgets = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO this lays out long text lines on a newline after a uilink rather than soft wrapping it.
|
// TODO this lays out long text lines on a newline after a uilink rather than soft wrapping it.
|
||||||
class DartboardTextWithLink extends StatelessWidget {
|
class DartboardTextWithLink extends StatelessWidget {
|
||||||
DartboardTextWithLink({this.bulletString, required this.stringSections, this.style});
|
DartboardTextWithLink({this.bulletString, required this.stringSections, this.style});
|
||||||
|
@ -98,26 +175,85 @@ class DartboardTextWithLink extends StatelessWidget {
|
||||||
Text(bulletString == null ? '' : '$bulletString ', style: style),
|
Text(bulletString == null ? '' : '$bulletString ', style: style),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: DartboardTheme.width - DartboardTheme.margin * 2 - 20.0,
|
width: DartboardTheme.width - DartboardTheme.margin * 2 - 20.0,
|
||||||
child: Wrap(
|
child: RichText(
|
||||||
children: [
|
text: TextSpan(
|
||||||
...stringSections.map((s) {
|
children: [
|
||||||
switch (s.type) {
|
...stringSections.map((s) {
|
||||||
case DartboardTextType.normal:
|
return TextSpan(text: s.text, style: style);
|
||||||
return Text(s.text, style: style);
|
// switch (s.type) {
|
||||||
case DartboardTextType.linkText:
|
// case DartboardTextType.normal:
|
||||||
return UrlLink(
|
// return [Text(s.text, style: style)];
|
||||||
destination: s.url!,
|
// case DartboardTextType.linkText:
|
||||||
child: Text(
|
// final number = UrlFootnotes().add(url: s.url!);
|
||||||
s.text,
|
// return [
|
||||||
style: style?.copyWith(color: PdfColors.blue),
|
// Text(s.text, style: style),
|
||||||
),
|
// UrlLink(
|
||||||
);
|
// destination: s.url!,
|
||||||
}
|
// child: Padding(
|
||||||
}),
|
// padding: const EdgeInsets.only(bottom: 4.0, left: 2.0, right: 2.0),
|
||||||
],
|
// child: Text(
|
||||||
|
// number.toString(),
|
||||||
|
// style: style?.copyWith(color: PdfColors.blue, fontSize: 8),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
}),
|
||||||
|
...stringSections.map((s) {
|
||||||
|
switch (s.type) {
|
||||||
|
case DartboardTextType.normal:
|
||||||
|
return null;
|
||||||
|
case DartboardTextType.linkText:
|
||||||
|
final number = UrlFootnotes().add(url: s.url!);
|
||||||
|
return TextSpan(
|
||||||
|
text: ' [$number]',
|
||||||
|
style: style?.copyWith(color: PdfColors.blue, fontSize: 8),
|
||||||
|
);
|
||||||
|
// return UrlLink(
|
||||||
|
// destination: s.url!,
|
||||||
|
// child: Padding(
|
||||||
|
// padding: const EdgeInsets.only(bottom: 4.0, left: 2.0, right: 2.0),
|
||||||
|
// child: Text(
|
||||||
|
// number.toString(),
|
||||||
|
// style: style?.copyWith(color: PdfColors.blue, fontSize: 8),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}).nonNulls,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Footnote extends StatelessWidget {
|
||||||
|
Footnote({required this.number, required this.url, required this.style});
|
||||||
|
|
||||||
|
final String url;
|
||||||
|
final int number;
|
||||||
|
final TextStyle style;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(Context context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
UrlLink(
|
||||||
|
destination: url,
|
||||||
|
child: Text(
|
||||||
|
'[$number]',
|
||||||
|
style: style.copyWith(color: PdfColors.blue, fontSize: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
' $url',
|
||||||
|
style: style.copyWith(fontSize: 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
124
lib/render.dart
124
lib/render.dart
|
@ -19,10 +19,11 @@ Future<void> renderPdf(String tomlFilePath, {bool force = false}) async {
|
||||||
stdout.writeln("Detected change:\nRendering with new dartboard data: $lastDartboardHash");
|
stdout.writeln("Detected change:\nRendering with new dartboard data: $lastDartboardHash");
|
||||||
final pdfFuture = Document()..addPage(_generatePdfPage(dartboardData: dartboardData, renderNs: 0));
|
final pdfFuture = Document()..addPage(_generatePdfPage(dartboardData: dartboardData, renderNs: 0));
|
||||||
await pdfFuture.save();
|
await pdfFuture.save();
|
||||||
|
|
||||||
final renderNs = DateTime.now().microsecondsSinceEpoch - start;
|
final renderNs = DateTime.now().microsecondsSinceEpoch - start;
|
||||||
|
|
||||||
final pdf = Document();
|
final pdf = Document();
|
||||||
pdf.addPage(_generatePdfPage(dartboardData: dartboardData, renderNs: renderNs));
|
pdf.addPage(_generatePdfPage(dartboardData: dartboardData, renderNs: renderNs));
|
||||||
|
|
||||||
final file = File("example.pdf");
|
final file = File("example.pdf");
|
||||||
final bytes = await pdf.save();
|
final bytes = await pdf.save();
|
||||||
|
|
||||||
|
@ -30,8 +31,10 @@ Future<void> renderPdf(String tomlFilePath, {bool force = false}) async {
|
||||||
file.writeAsBytesSync(bytes);
|
file.writeAsBytesSync(bytes);
|
||||||
stdout.writeln('Reloading llpp...');
|
stdout.writeln('Reloading llpp...');
|
||||||
Process.runSync('pkill', ['-HUP', 'llpp']);
|
Process.runSync('pkill', ['-HUP', 'llpp']);
|
||||||
} catch (e) {
|
UrlFootnotes().reset();
|
||||||
|
} catch (e, st) {
|
||||||
stderr.writeln('Encountered error: $e');
|
stderr.writeln('Encountered error: $e');
|
||||||
|
stderr.writeln(st);
|
||||||
try {
|
try {
|
||||||
stderr.writeln('Current toml map:\n${TomlDocument.loadSync(tomlFilePath).toMap()}');
|
stderr.writeln('Current toml map:\n${TomlDocument.loadSync(tomlFilePath).toMap()}');
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
@ -41,6 +44,7 @@ Future<void> renderPdf(String tomlFilePath, {bool force = false}) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Page _generatePdfPage({required DartboardData dartboardData, required int renderNs}) {
|
Page _generatePdfPage({required DartboardData dartboardData, required int renderNs}) {
|
||||||
|
UrlFootnotes().style = dartboardData.defaultTextStyle;
|
||||||
final List<Widget> groupedExperienceList = dartboardData.groupedExperiences.entries.map<Widget>(
|
final List<Widget> groupedExperienceList = dartboardData.groupedExperiences.entries.map<Widget>(
|
||||||
(entry) {
|
(entry) {
|
||||||
final String subsection = entry.key;
|
final String subsection = entry.key;
|
||||||
|
@ -57,17 +61,49 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Container(height: 2, width: 200, color: const PdfColorGrey(0.7)),
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(height: 2, width: 200, color: const PdfColorGrey(0.7)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 3),
|
||||||
...experiences.map(
|
...experiences.map(
|
||||||
(DartboardExperience exp) => DartboardExperienceEntry(dartboardData: dartboardData, exp: exp),
|
(DartboardExperience exp) => DartboardExperienceEntry(dartboardData: dartboardData, exp: exp),
|
||||||
),
|
),
|
||||||
DartboardFooter(dartboardData: dartboardData, renderNs: renderNs),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).toList();
|
).toList();
|
||||||
return Page(
|
final List<Widget> groupedMiscList = dartboardData.groupedMisc.entries.map<Widget>((entry) {
|
||||||
pageTheme: const PageTheme(pageFormat: PdfPageFormat.standard),
|
final String subsection = entry.key;
|
||||||
|
final List<DartboardMisc> miscs = entry.value;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
subsection,
|
||||||
|
style: dartboardData.subheaderTextStyle
|
||||||
|
.merge(const TextStyle(fontSize: 18))
|
||||||
|
.apply(color: const PdfColorGrey(0.2)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(height: 2, width: 200, color: const PdfColorGrey(0.7)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 3),
|
||||||
|
...miscs.map(
|
||||||
|
(DartboardMisc misc) => DartboardMiscEntry(dartboardData: dartboardData, misc: misc),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
return FullPage(
|
||||||
|
pageTheme: PageTheme(
|
||||||
|
buildBackground: (_) => Container(color: dartboardData.backgroundColor), pageFormat: PdfPageFormat.standard),
|
||||||
build: (Context context) {
|
build: (Context context) {
|
||||||
return
|
return
|
||||||
// FullPage(
|
// FullPage(
|
||||||
|
@ -75,41 +111,59 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render
|
||||||
// child:
|
// child:
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
if (dartboardData.imagePath == null)
|
||||||
height: 120,
|
Center(
|
||||||
width: double.infinity,
|
child: Column(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
Text(dartboardData.fullName, style: dartboardData.headerTextStyle),
|
||||||
Positioned(
|
if (dartboardData.phoneNumber != null)
|
||||||
left: 0,
|
Text(dartboardData.phoneNumber!, style: dartboardData.headerTextStyle),
|
||||||
child: Container(
|
if (dartboardData.email != null) Text(dartboardData.email!, style: dartboardData.headerTextStyle),
|
||||||
height: 100,
|
],
|
||||||
width: 100,
|
),
|
||||||
decoration: BoxDecoration(
|
)
|
||||||
shape: BoxShape.circle,
|
else
|
||||||
image: DecorationImage(
|
SizedBox(
|
||||||
fit: BoxFit.contain,
|
height: 100,
|
||||||
image: MemoryImage(
|
width: double.infinity,
|
||||||
File(dartboardData.imagePath).readAsBytesSync(),
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (dartboardData.imagePath != null)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
child: Container(
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
image: DecorationImage(
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
image: MemoryImage(
|
||||||
|
File(dartboardData.imagePath!).readAsBytesSync(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(dartboardData.fullName, style: dartboardData.headerTextStyle),
|
||||||
|
if (dartboardData.phoneNumber != null)
|
||||||
|
Text(dartboardData.phoneNumber!, style: dartboardData.headerTextStyle),
|
||||||
|
if (dartboardData.email != null)
|
||||||
|
Text(dartboardData.email!, style: dartboardData.headerTextStyle),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
Center(
|
),
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(dartboardData.fullName, style: dartboardData.headerTextStyle),
|
|
||||||
Text(dartboardData.phoneNumber, style: dartboardData.headerTextStyle),
|
|
||||||
Text(dartboardData.email, style: dartboardData.headerTextStyle),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Container(height: 20),
|
|
||||||
...groupedExperienceList,
|
...groupedExperienceList,
|
||||||
|
...groupedMiscList,
|
||||||
|
// this is quirky and gets evaluated before the actual footnotes get added up, so the original adding is done in the prerender
|
||||||
|
...UrlFootnotes().footnotes,
|
||||||
|
DartboardFooter(dartboardData: dartboardData, renderNs: renderNs),
|
||||||
],
|
],
|
||||||
// ),
|
// ),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user