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 exps = tomlData.entries .where((e) => DartboardExperience.filter(e.value)) .map((MapEntry expsEntry) { final String subsection = tomlData['${expsEntry.key}_name'] as String? ?? _getSubsectionFromKey(expsEntry.key); final exps = (expsEntry.value as List).map((e) => e as Map).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) .where((e) => e is String && e.isNotEmpty) .map((e) => DartboardText(content: e as String)) .toList(), location: exp['location'] as String?, ); }).toList(); }) .expand((i) => i) .toList(); final List misc = tomlData.entries.where((e) => DartboardMisc.filter(e.value)).map((MapEntry 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)['attributes'] as List) .expand((i) => i) .where((e) => e is String && e.isNotEmpty) .map((e) => DartboardText(content: e as String)) .toList(), ); }).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: misc, ); } final String fullName; final String? phoneNumber; final String? email; final String? imagePath; final DartboardTheme dartboardTheme; final List experiences; final List 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> get groupedExperiences { final Map> exps = {}; for (final DartboardExperience exp in experiences) { if (!exps.containsKey(exp.subsection)) { exps[exp.subsection] = []; } exps[exp.subsection]!.add(exp); } return exps; } Map> get groupedMisc { final Map> miscs = {}; for (final DartboardMisc misc in miscList) { if (!miscs.containsKey(misc.subsection)) { miscs[misc.subsection] = []; } miscs[misc.subsection]!.add(misc); } return miscs; } @override int get hashCode { return Object.hashAll([fullName, phoneNumber, email, imagePath, ...experiences, ...miscList]); } @override bool operator ==(Object other) { return super.hashCode == other.hashCode; } } class DartboardExperience { DartboardExperience({ required this.subsection, required this.title, required this.attributes, this.range, this.location, }); final String subsection; final String title; final List attributes; final DateRange? range; String? location; static bool filter(dynamic e) { if (e is! List || !e.every((f) => f is Map)) { return false; } final List> entries = e.map((f) => f as Map).toList(); return entries.every((f) => f['title'] is String && f['attributes'] is List && f.keys.length >= 2); } @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 DartboardMisc { DartboardMisc({required this.subsection, required this.attributes}); final String subsection; final List attributes; @override int get hashCode { return Object.hashAll([subsection, ...attributes]); } @override bool operator ==(Object other) { 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 { DartboardTheme({ required this.primaryHex, required this.accentHex, required this.backgroundHex, required this.fontPath, required this.bulletPoint, }); factory DartboardTheme.fromToml(Map 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 toTextLinkList() { final markdownLinkRegex = RegExp(r'\[(.*?)\]\((.*?)\)'); if (markdownLinkRegex.hasMatch(content)) { final matches = markdownLinkRegex.allMatches(content).toList(); final List 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': return 'Experience'; case 'misc': return 'Miscelaneous'; case 'edu': return 'Education'; case '': default: return ''; } }