WIP hot reloading enabled, much progress

This commit is contained in:
2024-09-07 14:42:32 -06:00
commit 976c3d0679
17 changed files with 1342 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
import 'dart:io';
import 'package:intl/intl.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
import 'package:toml/toml.dart';
class DartboardData {
DartboardData({
required this.fullName,
required this.phoneNumber,
required this.email,
// required this.address,
required this.imagePath,
required this.dartboardTheme,
required this.experiences,
required this.miscList,
});
factory DartboardData.fromToml(String tomlFilePath) {
final tomlData = TomlDocument.loadSync(tomlFilePath).toMap();
final List<DartboardExperience> exps = tomlData.entries
.where((e) => e.value is List && (e.value as List).firstOrNull is Map)
.map((MapEntry<String, dynamic> expsEntry) {
final String subsection = tomlData['${expsEntry.key}_name'] as String? ?? '';
final exps = (expsEntry.value as List).map((e) => e as Map<String, dynamic>).toList();
return exps.map((exp) {
final TomlLocalDate? start = exp['start'] as TomlLocalDate?;
final TomlLocalDate? end = exp['end'] as TomlLocalDate?;
return DartboardExperience(
title: exp['title'] as String,
subsection: subsection,
range: DateRange(
start: start == null ? null : DateTime(start.date.year, start.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(),
location: exp['location'] as String?,
);
}).toList();
})
.expand((i) => i)
.toList();
return DartboardData(
fullName: tomlData['full_name'] as String,
phoneNumber: tomlData['phone_number'] as String,
email: tomlData['email'] as String,
imagePath: tomlData['image'] as String,
dartboardTheme: DartboardTheme.fromToml(tomlData),
experiences: exps,
miscList: [
DartboardList(subsection: 'Skills & Interest', content: 'Proficient in flutter and other'),
],
);
}
final String fullName;
final String phoneNumber;
final String email;
// final String address;
final String imagePath;
final DartboardTheme dartboardTheme;
final List<DartboardExperience> experiences;
final List<DartboardList> miscList;
Font get font => dartboardTheme.font;
PdfColor get primaryColor => dartboardTheme.primaryColor;
PdfColor get accentColor => dartboardTheme.accentColor;
PdfColor get backgroundColor => dartboardTheme.backgroundColor;
TextStyle get headerTextStyle =>
TextStyle(fontSize: 18, font: font, fontWeight: FontWeight.bold, color: const PdfColorGrey(0.18));
TextStyle get subheaderTextStyle => TextStyle(fontSize: 14, font: font, color: const PdfColorGrey(0.3));
TextStyle get defaultTextStyle => TextStyle(fontSize: 10, font: font, color: const PdfColorGrey(0.45));
Map<String, List<DartboardExperience>> get groupedExperiences {
final Map<String, List<DartboardExperience>> exps = {};
for (final DartboardExperience exp in experiences) {
if (!exps.containsKey(exp.subsection)) {
exps[exp.subsection] = <DartboardExperience>[];
}
exps[exp.subsection]!.add(exp);
}
return exps;
}
@override
int get hashCode {
return Object.hashAll([fullName, phoneNumber, email, imagePath, ...experiences]);
}
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
}
class DartboardExperience {
DartboardExperience({
required this.title,
required this.attributes,
required this.range,
required this.subsection,
this.location,
});
final String subsection;
final String title;
String? location;
final List<DartboardText> attributes;
final DateRange range;
@override
int get hashCode {
return Object.hashAll([subsection, title, ...attributes, range]);
}
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
}
class DateRange {
DateRange({required this.start, required this.end});
final DateTime? start;
final DateTime? end;
@override
String toString() {
final DateFormat dateFormat = DateFormat("MMM yyyy");
if (start == null && end == null) {
return '';
}
if (start == null && end != null) {
return dateFormat.format(end!);
}
if (start != null && end == null) {
return "${dateFormat.format(start!)} - Current";
}
return "${dateFormat.format(start!)} - ${dateFormat.format(end!)}";
}
@override
int get hashCode {
return Object.hashAll([start, end]);
}
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
}
class DartboardList {
DartboardList({required this.subsection, required this.content});
final String subsection;
final String content;
@override
int get hashCode {
return Object.hashAll([subsection, content]);
}
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
}
class DartboardTheme {
DartboardTheme({
required this.primaryHex,
required this.accentHex,
required this.backgroundHex,
required this.fontPath,
required this.bulletPoint,
});
factory DartboardTheme.fromToml(Map<String, dynamic> toml) => DartboardTheme(
primaryHex: (toml['theme'] as Map)['primary_hex'] as String,
accentHex: (toml['theme'] as Map)['accent_hex'] as String,
backgroundHex: (toml['theme'] as Map)['background_hex'] as String,
fontPath: (toml['theme'] as Map)['font'] as String,
bulletPoint: (toml['theme'] as Map)['bullet_point'] as String? ?? '-',
);
factory DartboardTheme.retro() => DartboardTheme(
primaryHex: "00AA00",
accentHex: "44EE66",
backgroundHex: "FFFFFF",
fontPath: "nerd.ttf",
bulletPoint: '-',
);
static const double inch = 72.0;
static const double cm = inch / 2.54;
static const double mm = inch / 25.4;
static const width = 21.0 * cm;
static const height = 29.7 * cm;
static const margin = 2.0 * cm;
final String primaryHex;
final String accentHex;
final String backgroundHex;
final String fontPath;
final String bulletPoint;
Font? _font;
PdfColor get primaryColor => PdfColor.fromHex(primaryHex);
PdfColor get accentColor => PdfColor.fromHex(accentHex);
PdfColor get backgroundColor => PdfColor.fromHex(backgroundHex);
Font get font {
_font ??= Font.ttf(File(fontPath).readAsBytesSync().buffer.asByteData());
return _font!;
}
@override
int get hashCode {
return Object.hashAll([primaryHex, accentHex, backgroundHex, fontPath, bulletPoint]);
}
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
}
enum DartboardTextType { normal, linkText }
typedef DartboardTextLinkData = ({String text, String? url, DartboardTextType type});
/// Automatically detects and parses strings with hypertext in a markdown format
class DartboardText {
DartboardText({required this.content});
final String content;
static final _markdownLinkRegex = RegExp(r'\[(.*?)\]\((.*?)\)');
bool get hasLinkMarkup => _markdownLinkRegex.hasMatch(content);
List<DartboardTextLinkData> toTextLinkList() {
final markdownLinkRegex = RegExp(r'\[(.*?)\]\((.*?)\)');
if (markdownLinkRegex.hasMatch(content)) {
final matches = markdownLinkRegex.allMatches(content).toList();
final List<DartboardTextLinkData> stringSections = [];
int prevStartIndex = 0;
while (matches.isNotEmpty) {
final match = matches.removeAt(0);
stringSections
.add((text: content.substring(prevStartIndex, match.start), url: null, type: DartboardTextType.normal));
stringSections.add((text: match.group(1)!, url: match.group(2), type: DartboardTextType.linkText));
prevStartIndex = match.end;
}
stringSections
.add((text: content.substring(prevStartIndex, content.length), url: null, type: DartboardTextType.normal));
return stringSections;
} else {
return [(text: content, url: null, type: DartboardTextType.normal)];
// return Text("${bulletString != null ? '$bulletString ' : ''}$content", style: style);
}
}
@override
int get hashCode {
return Object.hashAll([content]);
}
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
}
String _getSubsectionFromKey(String key) {
switch (key) {
case 'exp':
}
}
+101
View File
@@ -0,0 +1,101 @@
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,
],
);
},
);
}
+123
View File
@@ -0,0 +1,123 @@
import 'package:dartboard_resume/dartboard_parser.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
class DartboardFooter extends StatelessWidget {
DartboardFooter({required this.dartboardData, required this.renderNs});
final int renderNs;
final DartboardData dartboardData;
@override
Widget build(Context context) {
final double renderTimeMs = renderNs.toDouble() / 1000.0;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
'This resume was generated with DartBoard Resume',
style: dartboardData.defaultTextStyle.copyWith(fontSize: 10),
),
Text(
'Rendered in ${renderTimeMs.toStringAsFixed(2)}ms',
style: dartboardData.defaultTextStyle.copyWith(fontSize: 10),
),
],
);
}
}
class DartboardExperienceEntry extends StatelessWidget {
DartboardExperienceEntry({required this.dartboardData, required this.exp});
final DartboardData dartboardData;
final DartboardExperience exp;
@override
Widget build(Context context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(exp.title, style: dartboardData.subheaderTextStyle.apply(color: const PdfColorGrey(0.3))),
Container(height: 1, width: 60, color: const PdfColorGrey(0.75)),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
exp.range.toString(),
style: dartboardData.defaultTextStyle.apply(color: const PdfColorGrey(0.42)),
),
if (exp.location != null)
Text(
exp.location!,
style: dartboardData.defaultTextStyle.apply(color: const PdfColorGrey(0.42)),
),
],
),
],
),
SizedBox(height: 6),
...exp.attributes.map(
(a) => Padding(
child: DartboardTextWithLink(
stringSections: a.toTextLinkList(),
bulletString: dartboardData.dartboardTheme.bulletPoint,
style: dartboardData.defaultTextStyle.apply(
color: const PdfColorGrey(0.55),
),
),
padding: const EdgeInsets.only(bottom: 4.0),
),
),
SizedBox(height: 20),
],
);
}
}
// TODO this lays out long text lines on a newline after a uilink rather than soft wrapping it.
class DartboardTextWithLink extends StatelessWidget {
DartboardTextWithLink({this.bulletString, required this.stringSections, this.style});
final String? bulletString;
final List<DartboardTextLinkData> stringSections;
final TextStyle? style;
@override
Widget build(Context context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(bulletString == null ? '' : '$bulletString ', style: style),
SizedBox(
width: DartboardTheme.width - DartboardTheme.margin * 2 - 20.0,
child: Wrap(
children: [
...stringSections.map((s) {
switch (s.type) {
case DartboardTextType.normal:
return Text(s.text, style: style);
case DartboardTextType.linkText:
return UrlLink(
destination: s.url!,
child: Text(
s.text,
style: style?.copyWith(color: PdfColors.blue),
),
);
}
}),
],
),
),
],
);
}
}
+118
View File
@@ -0,0 +1,118 @@
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';
import 'package:toml/toml.dart';
int? lastDartboardHash;
Future<void> renderPdf(String tomlFilePath, {bool force = false}) async {
try {
final start = DateTime.now().microsecondsSinceEpoch;
final dartboardData = DartboardData.fromToml(tomlFilePath);
if (lastDartboardHash == dartboardData.hashCode && !force) {
return;
}
lastDartboardHash = dartboardData.hashCode;
stdout.writeln("Detected change:\nRendering with new dartboard data: $lastDartboardHash");
final pdfFuture = Document()..addPage(_generatePdfPage(dartboardData: dartboardData, renderNs: 0));
await pdfFuture.save();
final renderNs = DateTime.now().microsecondsSinceEpoch - start;
final pdf = Document();
pdf.addPage(_generatePdfPage(dartboardData: dartboardData, renderNs: renderNs));
final file = File("example.pdf");
final bytes = await pdf.save();
stdout.writeln('New pdf file saved.');
file.writeAsBytesSync(bytes);
stdout.writeln('Reloading llpp...');
Process.runSync('pkill', ['-HUP', 'llpp']);
} catch (e) {
stderr.writeln('Encountered error: $e');
try {
stderr.writeln('Current toml map:\n${TomlDocument.loadSync(tomlFilePath).toMap()}');
} catch (_) {
stderr.writeln('Cannot display current toml map');
}
}
}
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) => DartboardExperienceEntry(dartboardData: dartboardData, exp: exp),
),
DartboardFooter(dartboardData: dartboardData, renderNs: renderNs),
],
);
},
).toList();
return Page(
pageTheme: const PageTheme(pageFormat: PdfPageFormat.standard),
build: (Context context) {
return
// FullPage(
// ignoreMargins: true,
// child:
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,
],
// ),
);
},
);
}
+5
View File
@@ -0,0 +1,5 @@
extension StringUtils on String {
String capitalize() {
return substring(0, 1).toUpperCase() + substring(1);
}
}