From fe40731e09626b6240a8cf3f8b08e2251df326c1 Mon Sep 17 00:00:00 2001 From: Nate Date: Tue, 10 Sep 2024 15:09:15 -0600 Subject: [PATCH] Refactors --- bin/dartboard_resume.dart | 52 +--- lib/annotation_manager.dart | 44 ++++ lib/dartboard_parser.dart | 332 -------------------------- lib/dartboard_runner.dart | 82 +++++++ lib/dartboard_widgets.dart | 259 -------------------- lib/models/dartboard_data.dart | 139 +++++++++++ lib/models/dartboard_misc.dart | 26 ++ lib/models/date_range.dart | 32 +++ lib/models/document_text.dart | 45 ++++ lib/models/experience.dart | 35 +++ lib/models/theme.dart | 64 +++++ lib/render.dart | 141 ++++++----- lib/widgets/annotated_text.dart | 77 ++++++ lib/widgets/dartboard_misc_entry.dart | 33 +++ lib/widgets/experience_entry.dart | 64 +++++ lib/widgets/footer.dart | 29 +++ lib/widgets/footnote.dart | 29 +++ pubspec.lock | 14 +- pubspec.yaml | 2 +- 19 files changed, 791 insertions(+), 708 deletions(-) create mode 100644 lib/annotation_manager.dart delete mode 100644 lib/dartboard_parser.dart create mode 100644 lib/dartboard_runner.dart delete mode 100644 lib/dartboard_widgets.dart create mode 100644 lib/models/dartboard_data.dart create mode 100644 lib/models/dartboard_misc.dart create mode 100644 lib/models/date_range.dart create mode 100644 lib/models/document_text.dart create mode 100644 lib/models/experience.dart create mode 100644 lib/models/theme.dart create mode 100644 lib/widgets/annotated_text.dart create mode 100644 lib/widgets/dartboard_misc_entry.dart create mode 100644 lib/widgets/experience_entry.dart create mode 100644 lib/widgets/footer.dart create mode 100644 lib/widgets/footnote.dart diff --git a/bin/dartboard_resume.dart b/bin/dartboard_resume.dart index d218493..36d04af 100644 --- a/bin/dartboard_resume.dart +++ b/bin/dartboard_resume.dart @@ -1,55 +1,21 @@ import 'dart:async'; -import 'dart:convert'; +import 'dart:developer' as dev; import 'dart:io'; -import 'package:dartboard_resume/dartboard_parser.dart'; -import 'package:dartboard_resume/render.dart'; +import 'package:dartboard_resume/dartboard_runner.dart'; import 'package:hotreloader/hotreloader.dart'; import 'package:logging/logging.dart' as logging; -import 'package:toml/toml.dart'; - -StreamSubscription? fileStreamSub; -StreamSubscription? stdinStreamSub; Future main(List arguments) async { - const String tomlFilePath = "resume.toml"; logging.hierarchicalLoggingEnabled = true; HotReloader.logLevel = logging.Level.INFO; - final HotReloader reloader = await HotReloader.create(); - - stdin.lineMode = false; - stdin.echoMode = false; - stdin.echoNewlineMode = false; - stdinStreamSub = stdin.transform(const Utf8Decoder()).transform(const LineSplitter()).listen( - (event) { - if (event == "r") { - stdout.writeln("Triggering pdf render..."); - 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); - } - }, - ); - - ProcessSignal.sigint.watch().listen((_) { - stdout.writeln('SIGINT received. Exiting gracefully...'); - fileStreamSub?.cancel(); - stdinStreamSub?.cancel(); - // Perform cleanup or other necessary actions here - reloader.stop(); - exit(0); // Exit with code 0 to indicate a successful termination - }); - if (FileSystemEntity.isWatchSupported) { - final fileStream = File(tomlFilePath).watch(events: FileSystemEvent.modify); - fileStreamSub = fileStream.listen((e) { - renderPdf(tomlFilePath); - }); - stdout.writeln('Watching for file changes.'); + HotReloader? reloader; + if ((await dev.Service.getInfo()).serverUri != null) { + reloader = await HotReloader.create(); } else { - renderPdf(tomlFilePath); + stdout.writeln( + "Dartboard can hot reload if run with dart's VM service.\n`dart run --enable-vm-service bin/dartboard_resume.dart`", + ); } + dartboardRun(reloader); } diff --git a/lib/annotation_manager.dart b/lib/annotation_manager.dart new file mode 100644 index 0000000..112bf7b --- /dev/null +++ b/lib/annotation_manager.dart @@ -0,0 +1,44 @@ +import 'package:dartboard_resume/widgets/footnote.dart'; +import 'package:pdf/widgets.dart'; + +class AnnotationManager { + // Factory constructor to return the single instance + factory AnnotationManager() { + return _instance; + } + + // Private constructor + AnnotationManager._privateConstructor(); + + // Static field to hold the single instance of the class + static final AnnotationManager _instance = AnnotationManager._privateConstructor(); + + // Field to hold the number + int _numUrls = 0; + List _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)); + return _numUrls; + } + + List get footnotes { + final widgets = [..._urlWidgets]; + reset(); + return widgets; + } + + // Method to reset the number + void reset() { + _numUrls = 0; + _urlWidgets = []; + } +} diff --git a/lib/dartboard_parser.dart b/lib/dartboard_parser.dart deleted file mode 100644 index c42ca5d..0000000 --- a/lib/dartboard_parser.dart +++ /dev/null @@ -1,332 +0,0 @@ -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 ''; - } -} diff --git a/lib/dartboard_runner.dart b/lib/dartboard_runner.dart new file mode 100644 index 0000000..10f086f --- /dev/null +++ b/lib/dartboard_runner.dart @@ -0,0 +1,82 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:dartboard_resume/render.dart'; +import 'package:hotreloader/hotreloader.dart'; +import 'package:toml/toml.dart'; + +StreamSubscription? fileStreamSub; +StreamSubscription? stdinStreamSub; + +Future dartboardRun(HotReloader? reloader) async { + const String tomlFilePath = "resume.toml"; + + if (reloader != null) { + stdout.writeln('Hot reload enabled!'); + } + + ProcessSignal.sigint.watch().listen((_) { + stdout.writeln('SIGINT received. Exiting gracefully...'); + fileStreamSub?.cancel(); + stdinStreamSub?.cancel(); + // Perform cleanup or other necessary actions here + reloader?.stop(); + exit(0); // Exit with code 0 to indicate a successful termination + }); + + stdinStreamSub = getUserInputStream().listen( + (event) { + if (event == "r") { + stdout.writeln("Triggering pdf render..."); + createDocument(tomlFilePath); + } + if (event == "p") { + stdout.writeln("Current toml map:"); + stdout.writeln(TomlDocument.loadSync(tomlFilePath).toMap()); + } + if (event == "q") { + stdout.writeln('Exiting...'); + fileStreamSub?.cancel(); + stdinStreamSub?.cancel(); + // Perform cleanup or other necessary actions here + reloader?.stop(); + exit(0); // Exit with code 0 to indicate a successful termination + } + }, + ); + + if (FileSystemEntity.isWatchSupported) { + final fileStream = File(tomlFilePath).watch(events: FileSystemEvent.modify); + fileStreamSub = fileStream.listen((e) { + createDocument(tomlFilePath); + }); + stdout.writeln('Watching for file changes.'); + } else { + stdout.writeln('File watch is not supported. Exiting upon completion.'); + createDocument(tomlFilePath); + } +} + +Stream getUserInputStream() { + stdin.lineMode = false; + stdin.echoMode = false; + stdin.echoNewlineMode = false; + return stdin.transform(const Utf8Decoder()).transform(const LineSplitter()); +} + +void refreshViewer() { + final result = Process.runSync('pgrep', ['llpp']); + if (result.exitCode != 0) { + stdout.writeln( + 'Unable to refresh the viewer\nDartboard Resume can refresh your pdf viewer! Here are the supported programs:\n\t- llpp', + ); + return; + } + // Send the HUP signal to `llpp` + Process.runSync('pkill', ['-HUP', 'llpp']); +} + +void createDocument(String tomlFilePath) { + renderPdf(tomlFilePath, force: true); + refreshViewer(); +} diff --git a/lib/dartboard_widgets.dart b/lib/dartboard_widgets.dart deleted file mode 100644 index e6b56ab..0000000 --- a/lib/dartboard_widgets.dart +++ /dev/null @@ -1,259 +0,0 @@ -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 Padding( - padding: const EdgeInsets.only(top: 8.0), - child: 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: [ - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: 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.copyWith(color: const PdfColorGrey(0.42), fontSize: 8.0), - ), - if (exp.location != null) - Text( - exp.location!, - style: dartboardData.defaultTextStyle.copyWith(color: const PdfColorGrey(0.42), fontSize: 8.0), - ), - ], - ), - ], - ), - 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(left: 8.0, bottom: 4.0), - ), - ), - 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 _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 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. -class DartboardTextWithLink extends StatelessWidget { - DartboardTextWithLink({this.bulletString, required this.stringSections, this.style}); - - final String? bulletString; - final List 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: RichText( - text: TextSpan( - children: [ - ...stringSections.map((s) { - return TextSpan(text: s.text, style: style); - // switch (s.type) { - // case DartboardTextType.normal: - // return [Text(s.text, style: style)]; - // case DartboardTextType.linkText: - // final number = UrlFootnotes().add(url: s.url!); - // return [ - // 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), - ), - ], - ); - } -} diff --git a/lib/models/dartboard_data.dart b/lib/models/dartboard_data.dart new file mode 100644 index 0000000..0c293ce --- /dev/null +++ b/lib/models/dartboard_data.dart @@ -0,0 +1,139 @@ +import 'package:dartboard_resume/models/dartboard_misc.dart'; +import 'package:dartboard_resume/models/date_range.dart'; +import 'package:dartboard_resume/models/document_text.dart'; +import 'package:dartboard_resume/models/experience.dart'; +import 'package:dartboard_resume/models/theme.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) => Experience.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 Experience( + 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) => DocumentText(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) => DocumentText(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: DocumentTheme.fromToml(tomlData), + experiences: exps, + miscList: misc, + ); + } + + final String fullName; + final String? phoneNumber; + final String? email; + final String? imagePath; + final DocumentTheme 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 Experience 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, dartboardTheme, ...experiences, ...miscList]); + } + + @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 ''; + } +} diff --git a/lib/models/dartboard_misc.dart b/lib/models/dartboard_misc.dart new file mode 100644 index 0000000..971ed9c --- /dev/null +++ b/lib/models/dartboard_misc.dart @@ -0,0 +1,26 @@ +import 'package:dartboard_resume/models/document_text.dart'; + +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'; + } +} diff --git a/lib/models/date_range.dart b/lib/models/date_range.dart new file mode 100644 index 0000000..1b8be4d --- /dev/null +++ b/lib/models/date_range.dart @@ -0,0 +1,32 @@ +import 'package:intl/intl.dart'; + +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; + } +} diff --git a/lib/models/document_text.dart b/lib/models/document_text.dart new file mode 100644 index 0000000..dac4d47 --- /dev/null +++ b/lib/models/document_text.dart @@ -0,0 +1,45 @@ +enum DocumentTextType { normal, linkText } + +typedef DocumentTextLinkData = ({String text, String? url, DocumentTextType type}); + +/// Automatically detects and parses strings with hypertext in a markdown format +class DocumentText { + DocumentText({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: DocumentTextType.normal)); + stringSections.add((text: match.group(1)!, url: match.group(2), type: DocumentTextType.linkText)); + prevStartIndex = match.end; + } + stringSections + .add((text: content.substring(prevStartIndex, content.length), url: null, type: DocumentTextType.normal)); + return stringSections; + } else { + return [(text: content, url: null, type: DocumentTextType.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; + } +} diff --git a/lib/models/experience.dart b/lib/models/experience.dart new file mode 100644 index 0000000..c71e7a2 --- /dev/null +++ b/lib/models/experience.dart @@ -0,0 +1,35 @@ +import 'package:dartboard_resume/models/date_range.dart'; +import 'package:dartboard_resume/models/document_text.dart'; + +class Experience { + Experience({ + 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; + } +} diff --git a/lib/models/theme.dart b/lib/models/theme.dart new file mode 100644 index 0000000..1e57115 --- /dev/null +++ b/lib/models/theme.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; + +class DocumentTheme { + DocumentTheme({ + required this.primaryHex, + required this.accentHex, + required this.backgroundHex, + required this.fontPath, + required this.bulletPoint, + }); + + factory DocumentTheme.fromToml(Map toml) => DocumentTheme( + 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 DocumentTheme.retro() => DocumentTheme( + 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; + } +} diff --git a/lib/render.dart b/lib/render.dart index 1a4b7a2..30fe34e 100644 --- a/lib/render.dart +++ b/lib/render.dart @@ -1,7 +1,13 @@ import 'dart:io'; -import 'package:dartboard_resume/dartboard_parser.dart'; -import 'package:dartboard_resume/dartboard_widgets.dart'; +import 'package:dartboard_resume/annotation_manager.dart'; +import 'package:dartboard_resume/models/dartboard_data.dart'; +import 'package:dartboard_resume/models/dartboard_misc.dart'; +import 'package:dartboard_resume/models/experience.dart'; +import 'package:dartboard_resume/models/theme.dart'; +import 'package:dartboard_resume/widgets/dartboard_misc_entry.dart'; +import 'package:dartboard_resume/widgets/experience_entry.dart'; +import 'package:dartboard_resume/widgets/footer.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart'; import 'package:toml/toml.dart'; @@ -29,9 +35,7 @@ Future renderPdf(String tomlFilePath, {bool force = false}) async { stdout.writeln('New pdf file saved.'); file.writeAsBytesSync(bytes); - stdout.writeln('Reloading llpp...'); - Process.runSync('pkill', ['-HUP', 'llpp']); - UrlFootnotes().reset(); + AnnotationManager().reset(); } catch (e, st) { stderr.writeln('Encountered error: $e'); stderr.writeln(st); @@ -44,11 +48,11 @@ Future renderPdf(String tomlFilePath, {bool force = false}) async { } Page _generatePdfPage({required DartboardData dartboardData, required int renderNs}) { - UrlFootnotes().style = dartboardData.defaultTextStyle; + AnnotationManager().style = dartboardData.defaultTextStyle; final List groupedExperienceList = dartboardData.groupedExperiences.entries.map( (entry) { final String subsection = entry.key; - final List experiences = entry.value; + final List experiences = entry.value; return Column( children: [ Row( @@ -68,7 +72,7 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render ), SizedBox(height: 3), ...experiences.map( - (DartboardExperience exp) => DartboardExperienceEntry(dartboardData: dartboardData, exp: exp), + (Experience exp) => ExperienceEntry(dartboardData: dartboardData, exp: exp), ), ], ); @@ -101,71 +105,76 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render ], ); }).toList(); - return FullPage( + return Page( pageTheme: PageTheme( - buildBackground: (_) => Container(color: dartboardData.backgroundColor), pageFormat: PdfPageFormat.standard), + buildBackground: (_) => Container(color: dartboardData.backgroundColor), + pageFormat: PdfPageFormat.standard, + margin: EdgeInsets.zero, + ), build: (Context context) { - return - // FullPage( - // ignoreMargins: true, - // child: - Column( - children: [ - if (dartboardData.imagePath == null) - 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), - ], - ), - ) - else - SizedBox( - height: 100, - width: double.infinity, - 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(), + return FullPage( + ignoreMargins: true, + child: Padding( + padding: const EdgeInsets.all(DocumentTheme.cm * 2), + child: Column( + children: [ + if (dartboardData.imagePath == null) + 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), + ], + ), + ) + else + SizedBox( + height: 100, + width: double.infinity, + 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), - if (dartboardData.phoneNumber != null) - Text(dartboardData.phoneNumber!, style: dartboardData.headerTextStyle), - if (dartboardData.email != null) - Text(dartboardData.email!, style: dartboardData.headerTextStyle), - ], - ), + ], ), - ], - ), - ), - ...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), - ], - // ), + ), + ...groupedExperienceList, + ...groupedMiscList, + // this is quirky and gets evaluated before the actual footnotes get added up, so the original adding is done in the prerender + ...AnnotationManager().footnotes, + DartboardFooter(dartboardData: dartboardData, renderNs: renderNs), + ], + // ), + ), + ), ); }, ); diff --git a/lib/widgets/annotated_text.dart b/lib/widgets/annotated_text.dart new file mode 100644 index 0000000..87887c9 --- /dev/null +++ b/lib/widgets/annotated_text.dart @@ -0,0 +1,77 @@ +// TODO this lays out long text lines on a newline after a uilink rather than soft wrapping it. +import 'package:dartboard_resume/annotation_manager.dart'; +import 'package:dartboard_resume/models/document_text.dart'; +import 'package:dartboard_resume/models/theme.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; + +class AnnotatedText extends StatelessWidget { + AnnotatedText({this.bulletString, required this.stringSections, this.style}); + + final String? bulletString; + final List stringSections; + final TextStyle? style; + + @override + Widget build(Context context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(bulletString == null ? '' : '$bulletString ', style: style), + SizedBox( + width: DocumentTheme.width - DocumentTheme.margin * 2 - 20.0, + child: RichText( + text: TextSpan( + children: [ + ...stringSections.map((s) { + return TextSpan(text: s.text, style: style); + // switch (s.type) { + // case DartboardTextType.normal: + // return [Text(s.text, style: style)]; + // case DartboardTextType.linkText: + // final number = UrlFootnotes().add(url: s.url!); + // return [ + // 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 DocumentTextType.normal: + return null; + case DocumentTextType.linkText: + final number = AnnotationManager().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, + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/dartboard_misc_entry.dart b/lib/widgets/dartboard_misc_entry.dart new file mode 100644 index 0000000..aa3cd01 --- /dev/null +++ b/lib/widgets/dartboard_misc_entry.dart @@ -0,0 +1,33 @@ +import 'package:dartboard_resume/models/dartboard_data.dart'; +import 'package:dartboard_resume/models/dartboard_misc.dart'; +import 'package:dartboard_resume/widgets/annotated_text.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; + +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: AnnotatedText( + stringSections: a.toTextLinkList(), + bulletString: dartboardData.dartboardTheme.bulletPoint, + style: dartboardData.defaultTextStyle.apply( + color: const PdfColorGrey(0.55), + ), + ), + ), + ), + SizedBox(height: 12), + ], + ); + } +} diff --git a/lib/widgets/experience_entry.dart b/lib/widgets/experience_entry.dart new file mode 100644 index 0000000..9dcaa3e --- /dev/null +++ b/lib/widgets/experience_entry.dart @@ -0,0 +1,64 @@ +import 'package:dartboard_resume/models/dartboard_data.dart'; +import 'package:dartboard_resume/models/experience.dart'; +import 'package:dartboard_resume/widgets/annotated_text.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; + +class ExperienceEntry extends StatelessWidget { + ExperienceEntry({required this.dartboardData, required this.exp}); + final DartboardData dartboardData; + final Experience exp; + + @override + Widget build(Context context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: 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.copyWith(color: const PdfColorGrey(0.42), fontSize: 8.0), + ), + if (exp.location != null) + Text( + exp.location!, + style: dartboardData.defaultTextStyle.copyWith(color: const PdfColorGrey(0.42), fontSize: 8.0), + ), + ], + ), + ], + ), + SizedBox(height: 6), + ...exp.attributes.map( + (a) => Padding( + child: AnnotatedText( + stringSections: a.toTextLinkList(), + bulletString: dartboardData.dartboardTheme.bulletPoint, + style: dartboardData.defaultTextStyle.apply( + color: const PdfColorGrey(0.55), + ), + ), + padding: const EdgeInsets.only(left: 8.0, bottom: 4.0), + ), + ), + SizedBox(height: 12), + ], + ); + } +} diff --git a/lib/widgets/footer.dart b/lib/widgets/footer.dart new file mode 100644 index 0000000..9821c09 --- /dev/null +++ b/lib/widgets/footer.dart @@ -0,0 +1,29 @@ +import 'package:dartboard_resume/models/dartboard_data.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 Padding( + padding: const EdgeInsets.only(top: 8.0), + child: 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), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/footnote.dart b/lib/widgets/footnote.dart new file mode 100644 index 0000000..3639c6e --- /dev/null +++ b/lib/widgets/footnote.dart @@ -0,0 +1,29 @@ +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart'; + +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), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index c79574f..e2eb040 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" url: "https://pub.dev" source: hosted - version: "68.0.0" + version: "73.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.1.0" + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.8.0" archive: dependency: transitive description: @@ -218,10 +218,10 @@ packages: dependency: transitive description: name: macros - sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.0-main.0" + version: "0.1.2-main.4" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 81adf0b..c5c9a1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: # Add regular dependencies here. dependencies: + hotreloader: ^4.2.0 intl: ^0.19.0 lint: ^2.3.0 logging: ^1.2.0 @@ -18,5 +19,4 @@ dependencies: dev_dependencies: lints: ^3.0.0 test: ^1.24.0 - hotreloader: ^4.2.0