Refactors
This commit is contained in:
parent
aa71e8a9e6
commit
fe40731e09
|
@ -1,55 +1,21 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:developer' as dev;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dartboard_resume/dartboard_parser.dart';
|
import 'package:dartboard_resume/dartboard_runner.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<String>? stdinStreamSub;
|
|
||||||
|
|
||||||
Future<void> main(List<String> arguments) async {
|
Future<void> main(List<String> arguments) async {
|
||||||
const String tomlFilePath = "resume.toml";
|
|
||||||
logging.hierarchicalLoggingEnabled = true;
|
logging.hierarchicalLoggingEnabled = true;
|
||||||
HotReloader.logLevel = logging.Level.INFO;
|
HotReloader.logLevel = logging.Level.INFO;
|
||||||
final HotReloader reloader = await HotReloader.create();
|
HotReloader? reloader;
|
||||||
|
if ((await dev.Service.getInfo()).serverUri != null) {
|
||||||
stdin.lineMode = false;
|
reloader = await HotReloader.create();
|
||||||
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.');
|
|
||||||
} else {
|
} 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);
|
||||||
}
|
}
|
||||||
|
|
44
lib/annotation_manager.dart
Normal file
44
lib/annotation_manager.dart
Normal file
|
@ -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<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));
|
||||||
|
return _numUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> get footnotes {
|
||||||
|
final widgets = [..._urlWidgets];
|
||||||
|
reset();
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to reset the number
|
||||||
|
void reset() {
|
||||||
|
_numUrls = 0;
|
||||||
|
_urlWidgets = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<DartboardExperience> exps = tomlData.entries
|
|
||||||
.where((e) => DartboardExperience.filter(e.value))
|
|
||||||
.map((MapEntry<String, dynamic> expsEntry) {
|
|
||||||
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();
|
|
||||||
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<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(
|
|
||||||
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<DartboardExperience> experiences;
|
|
||||||
final List<DartboardMisc> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
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<DartboardText> 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<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
|
|
||||||
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<DartboardText> 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<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':
|
|
||||||
return 'Experience';
|
|
||||||
case 'misc':
|
|
||||||
return 'Miscelaneous';
|
|
||||||
case 'edu':
|
|
||||||
return 'Education';
|
|
||||||
case '':
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
82
lib/dartboard_runner.dart
Normal file
82
lib/dartboard_runner.dart
Normal file
|
@ -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<FileSystemEvent>? fileStreamSub;
|
||||||
|
StreamSubscription<String>? stdinStreamSub;
|
||||||
|
|
||||||
|
Future<void> 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<String> 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();
|
||||||
|
}
|
|
@ -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<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.
|
|
||||||
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: 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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
139
lib/models/dartboard_data.dart
Normal file
139
lib/models/dartboard_data.dart
Normal file
|
@ -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<Experience> exps = tomlData.entries
|
||||||
|
.where((e) => Experience.filter(e.value))
|
||||||
|
.map((MapEntry<String, dynamic> expsEntry) {
|
||||||
|
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();
|
||||||
|
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<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) => 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<Experience> experiences;
|
||||||
|
final List<DartboardMisc> 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<Experience>> get groupedExperiences {
|
||||||
|
final Map<String, List<Experience>> exps = {};
|
||||||
|
for (final Experience exp in experiences) {
|
||||||
|
if (!exps.containsKey(exp.subsection)) {
|
||||||
|
exps[exp.subsection] = <Experience>[];
|
||||||
|
}
|
||||||
|
exps[exp.subsection]!.add(exp);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
}
|
26
lib/models/dartboard_misc.dart
Normal file
26
lib/models/dartboard_misc.dart
Normal file
|
@ -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<DocumentText> 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';
|
||||||
|
}
|
||||||
|
}
|
32
lib/models/date_range.dart
Normal file
32
lib/models/date_range.dart
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
45
lib/models/document_text.dart
Normal file
45
lib/models/document_text.dart
Normal file
|
@ -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<DocumentTextLinkData> toTextLinkList() {
|
||||||
|
final markdownLinkRegex = RegExp(r'\[(.*?)\]\((.*?)\)');
|
||||||
|
if (markdownLinkRegex.hasMatch(content)) {
|
||||||
|
final matches = markdownLinkRegex.allMatches(content).toList();
|
||||||
|
final List<DocumentTextLinkData> 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;
|
||||||
|
}
|
||||||
|
}
|
35
lib/models/experience.dart
Normal file
35
lib/models/experience.dart
Normal file
|
@ -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<DocumentText> 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<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
|
||||||
|
int get hashCode {
|
||||||
|
return Object.hashAll([subsection, title, ...attributes, range]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return super.hashCode == other.hashCode;
|
||||||
|
}
|
||||||
|
}
|
64
lib/models/theme.dart
Normal file
64
lib/models/theme.dart
Normal file
|
@ -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<String, dynamic> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,13 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dartboard_resume/dartboard_parser.dart';
|
import 'package:dartboard_resume/annotation_manager.dart';
|
||||||
import 'package:dartboard_resume/dartboard_widgets.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/pdf.dart';
|
||||||
import 'package:pdf/widgets.dart';
|
import 'package:pdf/widgets.dart';
|
||||||
import 'package:toml/toml.dart';
|
import 'package:toml/toml.dart';
|
||||||
|
@ -29,9 +35,7 @@ Future<void> renderPdf(String tomlFilePath, {bool force = false}) async {
|
||||||
|
|
||||||
stdout.writeln('New pdf file saved.');
|
stdout.writeln('New pdf file saved.');
|
||||||
file.writeAsBytesSync(bytes);
|
file.writeAsBytesSync(bytes);
|
||||||
stdout.writeln('Reloading llpp...');
|
AnnotationManager().reset();
|
||||||
Process.runSync('pkill', ['-HUP', 'llpp']);
|
|
||||||
UrlFootnotes().reset();
|
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
stderr.writeln('Encountered error: $e');
|
stderr.writeln('Encountered error: $e');
|
||||||
stderr.writeln(st);
|
stderr.writeln(st);
|
||||||
|
@ -44,11 +48,11 @@ 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;
|
AnnotationManager().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;
|
||||||
final List<DartboardExperience> experiences = entry.value;
|
final List<Experience> experiences = entry.value;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
@ -68,7 +72,7 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render
|
||||||
),
|
),
|
||||||
SizedBox(height: 3),
|
SizedBox(height: 3),
|
||||||
...experiences.map(
|
...experiences.map(
|
||||||
(DartboardExperience exp) => DartboardExperienceEntry(dartboardData: dartboardData, exp: exp),
|
(Experience exp) => ExperienceEntry(dartboardData: dartboardData, exp: exp),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -101,15 +105,18 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
return FullPage(
|
return Page(
|
||||||
pageTheme: PageTheme(
|
pageTheme: PageTheme(
|
||||||
buildBackground: (_) => Container(color: dartboardData.backgroundColor), pageFormat: PdfPageFormat.standard),
|
buildBackground: (_) => Container(color: dartboardData.backgroundColor),
|
||||||
|
pageFormat: PdfPageFormat.standard,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
),
|
||||||
build: (Context context) {
|
build: (Context context) {
|
||||||
return
|
return FullPage(
|
||||||
// FullPage(
|
ignoreMargins: true,
|
||||||
// ignoreMargins: true,
|
child: Padding(
|
||||||
// child:
|
padding: const EdgeInsets.all(DocumentTheme.cm * 2),
|
||||||
Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (dartboardData.imagePath == null)
|
if (dartboardData.imagePath == null)
|
||||||
Center(
|
Center(
|
||||||
|
@ -162,10 +169,12 @@ Page _generatePdfPage({required DartboardData dartboardData, required int render
|
||||||
...groupedExperienceList,
|
...groupedExperienceList,
|
||||||
...groupedMiscList,
|
...groupedMiscList,
|
||||||
// this is quirky and gets evaluated before the actual footnotes get added up, so the original adding is done in the prerender
|
// this is quirky and gets evaluated before the actual footnotes get added up, so the original adding is done in the prerender
|
||||||
...UrlFootnotes().footnotes,
|
...AnnotationManager().footnotes,
|
||||||
DartboardFooter(dartboardData: dartboardData, renderNs: renderNs),
|
DartboardFooter(dartboardData: dartboardData, renderNs: renderNs),
|
||||||
],
|
],
|
||||||
// ),
|
// ),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
77
lib/widgets/annotated_text.dart
Normal file
77
lib/widgets/annotated_text.dart
Normal file
|
@ -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<DocumentTextLinkData> 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,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
33
lib/widgets/dartboard_misc_entry.dart
Normal file
33
lib/widgets/dartboard_misc_entry.dart
Normal file
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
64
lib/widgets/experience_entry.dart
Normal file
64
lib/widgets/experience_entry.dart
Normal file
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
29
lib/widgets/footer.dart
Normal file
29
lib/widgets/footer.dart
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
29
lib/widgets/footnote.dart
Normal file
29
lib/widgets/footnote.dart
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
pubspec.lock
14
pubspec.lock
|
@ -5,23 +5,23 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3"
|
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "68.0.0"
|
version: "73.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.1.0"
|
version: "0.3.2"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808"
|
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.8.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -218,10 +218,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: macros
|
name: macros
|
||||||
sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79"
|
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0-main.0"
|
version: "0.1.2-main.4"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -8,6 +8,7 @@ environment:
|
||||||
|
|
||||||
# Add regular dependencies here.
|
# Add regular dependencies here.
|
||||||
dependencies:
|
dependencies:
|
||||||
|
hotreloader: ^4.2.0
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
lint: ^2.3.0
|
lint: ^2.3.0
|
||||||
logging: ^1.2.0
|
logging: ^1.2.0
|
||||||
|
@ -18,5 +19,4 @@ dependencies:
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^3.0.0
|
lints: ^3.0.0
|
||||||
test: ^1.24.0
|
test: ^1.24.0
|
||||||
hotreloader: ^4.2.0
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user